cipi api

Cipi can optionally enable a REST API layer on the server via cipi api <domain>. The API exposes operations on apps, aliases, databases, and SSL with Bearer token authentication and granular permissions. The package requires PHP 8.2+ and Laravel 12+.

Commands

bash
$ cipi api <domain>           # configure API at root (e.g. api.myhosting.com)
$ cipi api ssl                  # install Let's Encrypt certificate for API domain
$ cipi api token list           # list tokens
$ cipi api token create         # create a new token (choose abilities)
$ cipi api token revoke <id>    # revoke a token
$ cipi api status               # Laravel version, queue worker status, pending jobs
$ cipi api update               # soft update: composer update on Laravel and API packages
$ cipi api upgrade              # full rebuild with rollback at /opt/cipi/api.old

Token creation and granular permissions

Authentication uses Sanctum. Each token can have one or more abilities that limit allowed operations:

  • apps-view — read apps
  • apps-create — create apps
  • apps-edit — edit apps
  • apps-delete — delete apps
  • deploy-manage — deploy, rollback, unlock
  • ssl-manage — install and manage SSL certificates
  • aliases-view — read aliases
  • aliases-create — add aliases
  • aliases-delete — remove aliases
  • dbs-view — list databases
  • dbs-create — create databases
  • dbs-delete — delete databases
  • dbs-manage — backup, restore, regenerate password
  • mcp-access — access the MCP server

REST endpoints

All endpoints require the Authorization: Bearer <token> header. Write operations (create, edit, delete, deploy, rollback, unlock, SSL, alias, database) are asynchronous: they return 202 Accepted with a job_id to poll via GET /api/jobs/{id}. POST /api/apps accepts optional custom (boolean) and docroot (string) parameters for creating custom apps with classic deployment. The repository field is required for Laravel apps and optional for custom apps: omit it (or send empty) to provision an SFTP-only site aligned with Cipi v4.4.13+. When no repository is set, branch is omitted. The MCP tool AppCreate follows the same rules. Update the API package to 1.6.1+ on the server (cipi api update / cipi api upgrade) so validation and OpenAPI match this behavior.

Method Endpoint Required ability
GET /api/apps apps-view
GET /api/apps/{name} apps-view
POST /api/apps apps-create
PUT /api/apps/{name} apps-edit
DELETE /api/apps/{name} apps-delete
GET /api/apps/{name}/aliases aliases-view
POST /api/apps/{name}/aliases aliases-create
DELETE /api/apps/{name}/aliases aliases-delete
POST /api/apps/{name}/deploy deploy-manage
POST /api/apps/{name}/deploy/rollback deploy-manage
POST /api/apps/{name}/deploy/unlock deploy-manage
POST /api/apps/{name}/ssl ssl-manage
GET /api/dbs dbs-view
POST /api/dbs dbs-create
DELETE /api/dbs/{name} dbs-delete
POST /api/dbs/{name}/backup dbs-manage
POST /api/dbs/{name}/restore dbs-manage
POST /api/dbs/{name}/password dbs-manage
GET /api/jobs/{id}

Swagger / OpenAPI

Interactive documentation is available at /docs (Swagger UI, OpenAPI spec 2.0.0).

MCP server

An MCP (Model Context Protocol) server is exposed at /mcp via Streamable HTTP. It requires a token with the mcp-access ability. It exposes 19 tools for app, alias, database, deploy, and SSL management — with async dispatch and job_id return for polling:

  • Applications: AppList, AppShow, AppCreate, AppEdit, AppDelete, AppDeploy, AppDeployRollback, AppDeployUnlock
  • Aliases: AliasList, AliasAdd, AliasRemove
  • Databases: DbList, DbCreate, DbDelete, DbBackup, DbRestore, DbPassword
  • SSL: SslInstall

Installing the MCP server

The MCP endpoint is optional and loads only when the required MCP package is installed. To use it from VS Code, Cursor, or Claude Desktop:

  1. Configure the API with cipi api <domain> and cipi api ssl
  2. Create a token with cipi api token create and select at least mcp-access
  3. Add the MCP server to your client config (see below)

Cursor

Add to ~/.cursor/mcp.json (or Cursor → Settings → MCP):

json
{
  "mcpServers": {
    "cipi-api": {
      "type": "http",
      "url": "https://<your-api-domain>/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}

Cursor connects natively over HTTP — no bridge needed.

VS Code

VS Code (with GitHub Copilot) supports MCP natively since 1.102. Add to .vscode/mcp.json or run MCP: Open User Configuration for a global setup:

json
{
  "servers": {
    "cipi-api": {
      "type": "http",
      "url": "https://<your-api-domain>/mcp",
      "headers": {
        "Authorization": "Bearer <your-token>"
      }
    }
  }
}

Use MCP: Add Server from the Command Palette for a guided setup. VS Code connects over HTTP — no bridge needed.

Claude Code

Add the MCP server directly from the CLI:

bash
$ claude mcp add --transport http cipi-api https://<your-api-domain>/mcp \
    --header "Authorization: Bearer <your-token>"

Claude Desktop

Claude Desktop requires the mcp-remote bridge to convert stdio to HTTP. Add to ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or the equivalent config path on your OS:

json
{
  "mcpServers": {
    "cipi-api": {
      "command": "npx",
      "args": [
        "-y",
        "mcp-remote",
        "https://<your-api-domain>/mcp",
        "--header",
        "Authorization: Bearer <your-token>"
      ]
    }
  }
}

Install mcp-remote once with npm install -g mcp-remote.

Replace <your-api-domain> with your API domain (e.g. api.myhosting.com) and <your-token> with the token created in step 2.

WHMCS Module

An official WHMCS provisioning module is available at github.com/cipi-sh/whmcs. It bridges the WHMCS provisioning lifecycle to the Cipi REST API — automating app creation, deletion, SSL certificates, deployments, and configuration changes for your hosting customers. No Composer dependencies; the module is a self-contained drop-in.

Requirements

  • WHMCS 8.x (provisioning module type “Server”)
  • Cipi server with API enabled: cipi api <domain> and cipi api ssl
  • Sanctum Bearer token with the required abilities:
Ability Required for
apps-viewTest Connection, App Info
apps-createCreate Account
apps-editChange Package
apps-deleteTerminate Account
deploy-manageDeploy, Rollback, Unlock
ssl-manageInstall SSL, Auto-SSL

Installation

  1. Copy modules/servers/cipi/ into your WHMCS root:
    your-whmcs/
    └── modules/
        └── servers/
            └── cipi/
                ├── cipi.php
                └── lib/
                    └── CipiApiClient.php
  2. In WHMCS Admin → System Settings → Servers → Add New Server:
    • Type: Cipi (Laravel hosting)
    • Hostname: API base URL (e.g. https://api.example.com, no trailing slash)
    • Password: Bearer token from cipi api token create
    • Secure: Yes (recommended — enables TLS verification)
  3. Create a hosting product linked to this server and configure Module Settings:
Setting Description Default
PHP Version8.2 / 8.3 / 8.4 / 8.58.5
App Typelaravel or customlaravel
Git Repository (SSH)Required for Laravel; optional for custom
Git BranchBranch to deploymain
Auto SSLInstall Let’s Encrypt after creationNo

App types

App type Cipi equivalent Stack Git / Deploy
laravel (default) cipi app create Isolated Linux user, PHP-FPM pool, Nginx vhost, MariaDB, Supervisor workers, Deployer releases SSH repository URL required; branch from settings
custom cipi app create --custom htdocs/ directory, Nginx + PHP — ideal for static sites, SPAs, WordPress, or generic PHP apps Optional: leave Git Repository empty for SFTP-only hosting, or set a repo for Git-based deploy

Provisioning lifecycle

WHMCS action API call Behaviour
Test ConnectionGET /api/appsValidates token and API reachability
Create AccountPOST /api/appsProvisions a Cipi app (Laravel or custom); waits for async jobs; optionally installs SSL
Suspend / UnsuspendnoneReturns success so WHMCS updates billing state (Cipi has no suspend endpoint)
Terminate AccountDELETE /api/apps/{name}Removes the app; waits for async jobs
Change PackagePUT /api/apps/{name}Updates PHP version, Git repository, or branch

Admin buttons

From the WHMCS admin service view, operators can trigger one-click actions:

Button API call Description
Install SSLPOST /api/apps/{name}/sslInstall a Let’s Encrypt certificate
DeployPOST /api/apps/{name}/deployTrigger a zero-downtime deployment
Rollback DeployPOST /api/apps/{name}/deploy/rollbackRevert to the previous release
Unlock DeployPOST /api/apps/{name}/deploy/unlockUnlock a stuck deployment
App InfoGET /api/apps/{name}Fetch current app details into the Module Log

Auto-SSL on creation

Enable Auto SSL in the product module settings to automatically install a Let’s Encrypt certificate right after provisioning. If SSL installation fails, the app is still created successfully and a warning is logged.

Full API client

The bundled CipiApiClient covers the entire Cipi REST API surface. Even if a feature is not wired to a WHMCS hook, you can use the client in custom hooks or addons:

Area Methods
AppslistApps, getApp, createApp, editApp, deleteApp
DeploydeployApp, rollbackDeploy, unlockDeploy
SSLinstallSsl
AliaseslistAliases, addAlias, removeAlias
DatabaseslistDatabases, createDatabase, deleteDatabase, backupDatabase, restoreDatabase, resetDatabasePassword
JobsgetJob, waitForJob

Extending the module

php
// Example: add an alias from a WHMCS hook
require_once ROOTDIR . '/modules/servers/cipi/lib/CipiApiClient.php';

$client = new CipiApiClient('https://api.example.com', $token);
$client->addAlias('myapp', 'alias.example.com');

// Example: create an extra database
$client->createDatabase('myapp_extra');

// Example: backup a database
$client->backupDatabase('myapp');

Customer-facing behaviour

The module does not add a Client Area tab, custom buttons, or live status from Cipi. Customers see the standard WHMCS service view (domain, status, renewal dates). When Cipi provisions an app it generates one-time secrets (SSH password, DB password, deploy key, webhook URL). The REST API does not automatically push those secrets into WHMCS — you should extend the module, write a hook, or deliver credentials through your support workflow.

Module logging

All API calls are logged via logModuleCall() — Create, Terminate, Change Package, SSL, Deploy, Rollback, Unlock, and App Info. Enable Utilities → Logs → Module Log in WHMCS Admin for full visibility.

Full source code, project structure, and contribution guidelines are available on GitHub. The module is open-source under the MIT License.

cipi sync

Transfer, replicate, and back up entire Laravel applications between Cipi servers — including configuration, database dumps, storage files, SSH keys, workers, and crontabs. Every archive is encrypted with AES-256-CBC and protected by a user-defined passphrase, so credentials and sensitive data are safe at rest and during transfer.

Commands overview

bash
$ cipi sync export  [app ...] [--with-db] [--with-storage] [--output=<path>] [--passphrase=<secret>]
$ cipi sync import  <archive.tar.gz.enc> [app ...] [--update] [--deploy] [--yes] [--passphrase=<secret>]
$ cipi sync push    [app ...] [--host=IP] [--port=22] [--with-db] [--with-storage] [--import] [--passphrase=<secret>]
$ cipi sync list    <archive.tar.gz.enc> [--passphrase=<secret>]
$ cipi sync pubkey  # display the server's sync public key for inter-server trust
$ cipi sync trust   # add a remote server's public key to cipi's authorized_keys

Archive encryption

All sync archives are encrypted by default with AES-256-CBC. During export you are prompted for a passphrase (minimum 8 characters) that protects the archive. The same passphrase is required to import or inspect it. This protects SSH keys, .env files, database dumps, and credentials at rest and during transfer.

bash
# Interactive mode (default) — prompted for passphrase
$ cipi sync export --with-db
#   Enter passphrase to encrypt the archive: ********
#   Confirm passphrase: ********

# Non-interactive mode — for cron jobs and scripts
$ cipi sync export --with-db --passphrase="MyStr0ngP@ss"
For automated setups, store the passphrase in a secure file and reference it in your scripts: echo "MyStr0ngP@ss" > /etc/cipi/.sync_passphrase && chmod 400 /etc/cipi/.sync_passphrase. Then use --passphrase="$(cat /etc/cipi/.sync_passphrase)" in cron jobs.

Export

Packs app configs into an encrypted .tar.gz.enc archive. Optionally includes database dumps and storage files.

bash
# Export all apps (config only)
$ cipi sync export

# Export three specific apps with database + storage
$ cipi sync export shop blog api --with-db --with-storage

# Export to a custom path (non-interactive)
$ cipi sync export --with-db --output=/root/backups/cipi-march.tar.gz --passphrase="MyStr0ngP@ss"

What goes into the archive

File Description Included
env The app's .env from /home/<app>/shared/.env Always
auth.json Composer auth credentials (if exists) Always
deploy.php Deployer config Always
ssh/* Deploy key, known_hosts, authorized_keys, SSH config Always
supervisor.conf Queue workers config Always
crontab App's crontab (scheduler + deploy trigger) Always
db.sql.gz Gzipped MariaDB dump (schema + data + routines) --with-db
storage.tar.gz Archive of /home/<app>/shared/storage/ --with-storage

Plus global configs: apps.json (filtered to selected apps), databases.json, backup.json, api.json.

Import

Restores apps from an archive onto the current server.

bash
# Import all apps from archive
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc

# Import only two apps from an archive that contains ten
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc shop blog --passphrase="MyStr0ngP@ss"

# Import and deploy code from Git immediately
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc --deploy

# Non-interactive (skip all prompts)
$ cipi sync import /tmp/cipi-sync-aws01-20260306.tar.gz.enc --yes --passphrase="MyStr0ngP@ss"

What import does for a NEW app

When an app does not exist on the target server, import creates it from scratch — equivalent to cipi app create with all configs pre-filled from the archive:

  1. Linux user — Creates a new user with a random password
  2. Directories — Creates /home/<app>/shared/, logs/, .ssh/, .deployer/
  3. SSH deploy key — Restores from archive (same key works with GitHub/GitLab without reconfiguration)
  4. MariaDB database — Creates database + user with a new random password
  5. Database data — Imports the dump if --with-db was used during export
  6. .env — Copies from archive, then overwrites DB_PASSWORD, DB_USERNAME, DB_DATABASE, DB_HOST with the new server's values. Everything else (APP_KEY, MAIL_*, REDIS_*, custom vars) stays as-is
  7. PHP-FPM pool, Nginx vhost, Supervisor workers, Crontab, Deployer — Fully configured from archive data
At the end of import, Cipi prints the new SSH and DB passwords. Save them — they are shown only once.

Safety checks before import

The import runs pre-flight checks before touching anything:

  • App already exists — blocked unless --update is passed
  • Domain conflict — blocked if another app already uses the same domain
  • Missing PHP version — warning (the app is skipped; install the version first with cipi php install)

Update mode (--update)

The key feature for repeated sync (e.g. failover replication). Without --update, import refuses to touch apps that already exist. With --update, it updates existing apps and creates new ones.

bash
$ cipi sync import /tmp/archive.tar.gz.enc --update --passphrase="MyStr0ngP@ss"

What update does for an existing app

  • .env sync — The archive .env replaces the local one, but DB_PASSWORD, DB_USERNAME, DB_DATABASE, and DB_HOST are preserved from the local server. Everything else (APP_KEY, MAIL_*, REDIS_*, custom vars) comes from the source.
  • Database data — If the archive has a dump, drops all tables (with SET FOREIGN_KEY_CHECKS=0) and reimports. Uses local root credentials.
  • Storage — If the archive has storage, extracts over the existing directory (new files added, existing overwritten).
  • PHP version migration — If the source uses a different PHP version, the update migrates FPM pool, supervisor, crontab, deployer, and .env automatically.
  • Nginx vhost, Supervisor workers, Deployer config — Regenerated from archive data.
  • Deploy — If --deploy is passed, runs dep deploy to pull latest code.

What update does NOT change

  • Linux user password
  • SSH deploy keys (kept from first import)
  • MariaDB user credentials (target keeps its own)
  • SSL certificates (run cipi ssl install separately)

List (inspect archive)

View what is inside an archive without importing anything.

bash
$ cipi sync list /tmp/cipi-sync-aws01-20260306.tar.gz.enc --passphrase="MyStr0ngP@ss"

Cipi Sync Archive
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  Cipi           v4.4.13
  Exported       2026-03-06T15:00:00Z
  Source         aws01 (3.120.xx.xx)
  Database       true
  Storage        true
 
  Apps
  APP            DOMAIN                       PHP    DB       STORAGE
  shop           shop.example.com             8.4    yes      yes
  blog           blog.example.com             8.4    yes      yes
  api            api.example.com              8.5    yes      yes
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Push (export + transfer + import)

Combines export, rsync transfer, and remote import in one command. Runs entirely from the source server.

bash
# Interactive push — prompted for target IP and passphrase
$ cipi sync push --with-db --with-storage --import

# Non-interactive push (for cron and scripts)
$ cipi sync push --host=51.195.xx.xx --port=22 --with-db --with-storage --import --passphrase="MyStr0ngP@ss"

# Push specific apps only
$ cipi sync push shop blog --host=51.195.xx.xx --with-db --import --passphrase="MyStr0ngP@ss"

# Push without auto-import (transfer only — import manually on remote)
$ cipi sync push --host=51.195.xx.xx --with-db --passphrase="MyStr0ngP@ss"

How push works

  1. Step 1: Runs cipi sync export locally (encrypts with passphrase)
  2. Step 2: Transfers the encrypted archive to the target via rsync
  3. Step 3: If --import is passed, runs cipi sync import --update --yes on the target via SSH

Push always adds --update and --yes when calling import on the remote. This means: first run creates all apps, subsequent runs incrementally update them. This is what makes push safe to run repeatedly via cron.

SSH setup for push

The source server needs SSH access to the target as the cipi user. Use the built-in trust mechanism for passwordless, key-based authentication between Cipi servers:

bash
# On the SOURCE server — display its sync public key
$ cipi sync pubkey

# On the TARGET server — add the source's public key to cipi's authorized_keys
$ cipi sync trust

Once trusted, cipi sync push connects as the cipi user automatically — no root access required.

Practical scenarios

Scenario 1: Migrate all apps from AWS to OVH

You have 20 apps on AWS. You bought an OVH VPS and installed Cipi on it.

bash
# On AWS (source server)
$ cipi sync push --host=51.195.xx.xx --with-db --with-storage --import

On the OVH target: 20 Linux users, 20 databases, 20 nginx vhosts, PHP-FPM pools, supervisor configs, crontabs — all created automatically. DB data imported, storage extracted, .env files copied with OVH's DB passwords, SSH deploy keys preserved (same keys work with GitHub). After import, install SSL and update DNS:

bash
# On OVH (target server)
$ cipi ssl install shop
$ cipi ssl install blog
# ... then update DNS A records to OVH IP

Scenario 2: Scheduled failover replication (cron)

Every 6 hours, Server 1 syncs all apps to Server 2. If Server 1 dies, change DNS and go live on Server 2.

bash
# One-time setup on Server 1 — trust Server 2 using cipi sync trust
$ cipi sync pubkey  # copy this key, then run "cipi sync trust" on Server 2
$ echo "YourStr0ngPassphrase!" > /etc/cipi/.sync_passphrase
$ chmod 400 /etc/cipi/.sync_passphrase

# First push (manual, to verify)
$ cipi sync push --host=server2-ip --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)"

# Add to crontab for automatic replication
$ crontab -e
cron
0 */6 * * * /usr/local/bin/cipi sync push --host=51.195.xx.xx --port=22 --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-replica.log 2>&1

Data loss window equals the cron interval (6 hours in this example). When Server 1 goes down: change DNS to Server 2, run cipi ssl install for each app, and you are live.

Scenario 3: Replicate to multiple servers

cron
# Stagger by 30 minutes so exports don't run simultaneously
0 */6 * * * /usr/local/bin/cipi sync push --host=51.195.xx.xx --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-ovh.log 2>&1
30 */6 * * * /usr/local/bin/cipi sync push --host=164.90.xx.xx --with-db --with-storage --import --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/sync-do.log 2>&1

Scenario 4: Daily encrypted backup (no transfer)

cron
0 3 * * * /usr/local/bin/cipi sync export --with-db --with-storage --output=/root/backups/cipi-$(date +\%Y\%m\%d).tar.gz --passphrase="$(cat /etc/cipi/.sync_passphrase)" >> /var/log/cipi/export.log 2>&1

Creates an encrypted portable archive every night. Restore on any Cipi server at any time with cipi sync import.

Limitations

  • SSL certificates are not included in the archive. Run cipi ssl install after importing on a new server.
  • DB sync is full replace, not incremental. Each update drops all tables and reimports.
  • Storage sync is full extract, not incremental rsync. Deleted files on the source remain on the target.
  • Deploy keys are the same on source and target — no GitHub/GitLab reconfiguration needed.

Vault & Encryption

Cipi encrypts all configuration files at rest using AES-256-CBC. The Vault system provides transparent encryption and decryption so that sensitive data — database passwords, API tokens, SSH keys, .env contents — is never stored in plaintext on disk.

Architecture

The system is built on two layers:

  • Vault — transparent encryption of JSON configuration files on disk (server.json, apps.json, databases.json, backup.json, smtp.json, api.json)
  • Sync encryption — passphrase-based encryption of export archives for secure transfer between servers

How Vault works

A master key is generated during installation with openssl rand -base64 32 and stored at /etc/cipi/.vault_key (chmod 400, root-only). Every JSON config file is encrypted on disk with openssl enc -aes-256-cbc -salt -pbkdf2. Files keep the .json extension — the content is simply an encrypted blob instead of readable JSON.

The vault_read function auto-detects whether a file is plaintext or encrypted (backward compatibility), so existing servers migrate seamlessly during update.

Vault functions

bash
# Core functions in lib/vault.sh
vault_init          # Generate .vault_key if not present
vault_read <file>   # Decrypt and output JSON to stdout (auto-detect plain/encrypted)
vault_write <file>  # Read JSON from stdin, encrypt and write to disk
vault_seal <file>   # Encrypt an existing plaintext file in-place
vault_get <file> <jq_query>  # Shortcut: vault_read | jq

Public projection

Cipi generates an apps-public.json file containing only non-sensitive fields (domain, aliases, PHP version, branch, repository, user, creation timestamp). The cipi-api group reads this plaintext projection instead of the encrypted file, keeping the vault key restricted to root.

Sync archive encryption

When you run cipi sync export, configs are decrypted from the vault into a staging area, then the entire archive is encrypted with your passphrase. On import, the archive is decrypted with the passphrase, and configs are re-encrypted with the destination server's vault key.

If the vault key is lost, configuration files become irrecoverable. The key is protected by chmod 400 and included in server backups. Consider exporting it manually for additional safety.

Email Notifications

Cipi can send email alerts when backup errors, deploy failures, system cron job failures, or security-relevant authentication events occur. SMTP configuration is stored encrypted in /etc/cipi/smtp.json and included in sync exports.

Commands

bash
$ cipi smtp configure      # interactive setup (Gmail, SendGrid, Mailgun, custom)
$ cipi smtp status          # display current notification settings
$ cipi smtp test            # send a verification email
$ cipi smtp enable          # enable notifications
$ cipi smtp disable         # disable without losing settings
$ cipi smtp delete          # remove SMTP configuration entirely

Automatic alerts

Once configured, Cipi sends email notifications on:

  • Backup errors (S3 upload failures, dump errors)
  • Deploy failures (Deployer errors, rollback triggers)
  • System cron job failures (via the cipi-cron-notify wrapper)
  • App lifecycle events — notifies when an app is created, edited, or deleted, including server hostname, app name, domain, and PHP version
  • Sudo and su elevation — notifies when any user successfully elevates via sudo or su, including who ran it, target user (for su), SSH key, client IP, and TTY
  • Privileged SSH login — notifies when root or any sudoer logs in via SSH, including source IP, SSH key fingerprint, and key comment
  • SSH key changes — notifies when an SSH key is added to, removed from, or renamed on the cipi user, including hostname, IP, fingerprint, key comment, timestamp, and remaining key count. Rename alerts also include the old and new key name.

Every email notification includes a footer with the client IP (SSH_CLIENT) and the SSH key name used to authenticate, when applicable. The key name is resolved via SSH_USER_AUTH with an auth.log fallback when needed.

Security auth notifications

Cipi integrates PAM-based authentication notifications via pam_exec.so with ExposeAuthInfo enabled. When SMTP is configured, the system automatically sends email alerts on these security-relevant events:

  • Sudo and su elevation — triggered when any user successfully runs sudo or su. The notification includes the username, target user (for su), the TTY, SSH key, client IP, and timestamp.
  • Privileged SSH login — triggered when root or any user in the sudo group logs in via SSH. The notification includes the username, source IP address, SSH key fingerprint, and key comment (resolved from /var/log/auth.log fingerprint matching against authorized_keys).
  • SSH key changes — triggered when an SSH key is added to, removed from, or renamed on the cipi user via cipi ssh add, cipi ssh remove, or cipi ssh rename. The notification includes the hostname, server IP, key fingerprint, key comment, timestamp, and remaining key count. Rename alerts also include the old and new key name.
  • App lifecycle events — triggered when an app is created, edited, or deleted. The notification includes the server hostname, app name, domain, and PHP version.

Notifications run asynchronously in the background so they never delay login or command execution. If SMTP is not configured, the hooks fail silently with no impact on the system.

Security event log

Regardless of SMTP configuration, all notification events (SSH key changes, app lifecycle, password resets, sudo/su/SSH login, cron failures) are always logged to /var/log/cipi/events.log in a compact one-line format. The log is rotated daily with 1-year retention via logrotate.

Cron wrapper

The cipi-cron-notify utility wraps system cron jobs and sends a notification if the job exits with a non-zero code. This is useful for monitoring critical scheduled tasks.

Log Retention (GDPR)

Cipi enforces automatic log rotation policies designed to meet GDPR and general data protection requirements. Logs are rotated and deleted automatically — no manual cleanup needed.

Category Logs Retention
Application Laravel, PHP-FPM, workers, deploy, system 12 months
Security Fail2ban, UFW firewall, authentication, Cipi events (events.log) 12 months
HTTP / Navigation Nginx access and error logs 90 days
HTTP/navigation logs (nginx access logs) contain IP addresses, which are personal data under GDPR. The 90-day retention ensures compliance with the data minimization principle while preserving enough history for debugging and security analysis. Application and security logs are retained for 12 months to support audit trails and incident investigation.

Redis

Redis is an in-memory data store that Cipi installs as part of the default stack (from version 4.0.4). It excels at caching, session storage, message queues, real-time broadcasting, and rate limiting.

System configuration

Redis binds to localhost only, uses a password, and runs as a system service. Credentials (user, password) are in /etc/cipi/server.json. Host: 127.0.0.1, Port: 6379.

Laravel integration

Add these variables to your .env via cipi app env myapp:

env
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=your-password-from-server-json
REDIS_PORT=6379

Then set the drivers for each use case:

  • CacheCACHE_STORE=redis
  • SessionSESSION_DRIVER=redis
  • QueueQUEUE_CONNECTION=redis (then cipi worker restart myapp)
  • BroadcastingBROADCAST_CONNECTION=redis

Install the phpredis PHP extension for best performance, or use predis/predis as a pure-PHP fallback.

Self-Update

Cipi can update itself from GitHub without affecting any app, database, or configuration.

bash
$ cipi self-update --check   # check for a new version
$ cipi self-update           # update to latest

Update process

  1. Downloads the latest version from GitHub
  2. Backs up the current installation to /opt/cipi.bak.YYYYMMDDHHMMSS/
  3. Replaces CLI and lib scripts
  4. Runs any pending migration scripts in order (e.g. new Nginx directives, new packages)
  5. Updates the version file

Migration scripts live in lib/migrations/ and are named by version (e.g. 4.1.0.sh). When updating from v4.0.0 to v4.2.0, Cipi automatically runs 4.1.0.sh and 4.2.0.sh in order. Your apps, databases, and configurations are never touched.

Wildcard domains

Cipi does not support wildcard domains (*.myapp.com) natively. The block is twofold and architectural — not a configuration detail.

Why wildcards are not supported

1 — Domain validation rejects *
Every domain passed to cipi alias add (and cipi app create) is validated against a strict regex that requires the string to start with [a-zA-Z0-9]. The asterisk fails immediately, before nginx or certbot are ever touched.

2 — Certbot uses HTTP-01 challenge, which cannot issue wildcard certs
cipi ssl install calls certbot --nginx, which relies on the HTTP-01 (or TLS-ALPN-01) challenge — placing a verification file on disk and serving it over port 80. Let's Encrypt only issues wildcard certificates via the DNS-01 challenge, which requires programmatic access to your DNS provider's API. Cipi does not integrate with any DNS provider, so even if the validation were bypassed, certbot would refuse to issue the wildcard cert.

Recommended alternative — Multi-SAN certificate

If your subdomains are fixed and enumerable (e.g. api, admin, www, staging), the correct approach is to add each one as an explicit alias and let Cipi issue a single SAN certificate covering all of them:

bash
$ cipi alias add myapp api.myapp.com
$ cipi alias add myapp admin.myapp.com
$ cipi alias add myapp www.myapp.com
$ cipi ssl install myapp   # single cert, SAN covers all domains

Certbot's --expand flag (used internally by Cipi) adds the new SANs to the existing certificate without issuing a new one. The SAN list has no meaningful limit for typical use.

Manual wildcard certificate (outside Cipi)

If you need dynamic subdomains (e.g. <tenant>.saas.com), you can obtain a wildcard certificate manually using a DNS plugin for certbot and place it on the server. Cipi will not manage, renew, or track it — you own the lifecycle entirely.

bash
# example with the Cloudflare DNS plugin
$ pip install certbot-dns-cloudflare
$ certbot certonly --dns-cloudflare \
    --dns-cloudflare-credentials /root/.cloudflare.ini \
    -d "*.myapp.com" -d "myapp.com"

After obtaining the certificate, edit the nginx vhost for the app directly (/etc/nginx/sites-available/myapp) to reference the wildcard cert paths and add server_name *.myapp.com myapp.com;. Then reload nginx:

bash
$ nginx -t && systemctl reload nginx
Running cipi ssl install myapp after manual wildcard setup will overwrite your custom nginx SSL directives with a Let's Encrypt HTTP-01 certificate. If you manage a wildcard cert manually, avoid running cipi ssl install on that app.

Edit Nginx config

To customise the Nginx vhost for an app, edit the site config directly. After changes, test and reload Nginx.

bash
$ sudo nano /etc/nginx/sites-available/<app>
$ sudo nginx -t && sudo systemctl reload nginx

Uninstall Cipi

Cipi does not provide a built-in uninstall command. If you need to completely remove Cipi from a server, follow the steps below in order. This procedure removes every component that Cipi installs — users, services, packages, configs, and data.

This is a destructive and irreversible operation. All apps, databases, SSL certificates, and server configurations managed by Cipi will be permanently deleted. Back up everything you need before proceeding. After uninstalling, the recommended approach is to reprovision the server from scratch.

1 — Stop and remove all apps

For each app managed by Cipi, remove its system user, home directory, database, nginx vhost, PHP-FPM pool, and supervisor config.

bash
# List all app users (members of cipi-apps group)
$ grep cipi-apps /etc/group

# For EACH app user, remove everything
$ supervisorctl stop <app_user>:*
$ rm -f /etc/supervisor/conf.d/<app_user>.conf
$ rm -f /etc/nginx/sites-enabled/<app_user>
$ rm -f /etc/nginx/sites-available/<app_user>
$ rm -f /etc/php/*/fpm/pool.d/<app_user>.conf
$ rm -f /etc/sudoers.d/cipi-<app_user>
$ mysql -e "DROP DATABASE IF EXISTS <app_user>; DROP USER IF EXISTS '<app_user>'@'localhost'; DROP USER IF EXISTS '<app_user>'@'127.0.0.1';"
$ userdel -r <app_user>

2 — Remove the Cipi user and groups

bash
$ userdel -r cipi
$ groupdel cipi-ssh 2>/dev/null
$ groupdel cipi-apps 2>/dev/null

3 — Remove Cipi binaries, libraries, and data

bash
$ rm -f /usr/local/bin/cipi
$ rm -f /usr/local/bin/cipi-worker
$ rm -f /usr/local/bin/cipi-cron-notify
$ rm -f /usr/local/bin/cipi-auth-notify
$ rm -rf /opt/cipi
$ rm -rf /etc/cipi
$ rm -rf /var/log/cipi

4 — Remove Cipi API (if installed)

bash
$ systemctl stop cipi-queue 2>/dev/null
$ systemctl disable cipi-queue 2>/dev/null
$ rm -f /etc/systemd/system/cipi-queue.service
$ systemctl daemon-reload

5 — Remove Cipi cron jobs

bash
# Edit root crontab and remove all Cipi entries
$ crontab -e
# Remove lines referencing: cipi self-update, certbot renewal, cache cleanup, RAM drop

6 — Remove Cipi configuration files

bash
# Sudoers
$ rm -f /etc/sudoers.d/cipi-sudo
$ rm -f /etc/sudoers.d/cipi-api

# Logrotate
$ rm -f /etc/logrotate.d/cipi-app-logs
$ rm -f /etc/logrotate.d/cipi-http-logs
$ rm -f /etc/logrotate.d/cipi-security-logs

# Unattended upgrades
$ rm -f /etc/apt/apt.conf.d/50cipi-unattended-upgrades
$ rm -f /etc/apt/apt.conf.d/20cipi-auto-upgrades

# System profile and MOTD
$ rm -f /etc/profile.d/cipi-env.sh
$ echo "" > /etc/motd

# MariaDB custom config
$ rm -f /etc/mysql/mariadb.conf.d/99-cipi.cnf

# PHP custom config (all versions)
$ rm -f /etc/php/*/fpm/conf.d/99-cipi.ini

# Nginx default page
$ rm -f /etc/nginx/sites-available/default
$ rm -f /etc/nginx/sites-enabled/default

7 — Purge installed packages

Remove all packages that Cipi installed. Skip any package you want to keep for other purposes.

bash
$ systemctl stop nginx mariadb redis-server fail2ban supervisor
$ systemctl stop php*-fpm

$ apt purge -y nginx* mariadb-server mariadb-client redis-server \
    fail2ban supervisor certbot python3-certbot-nginx \
    php8.4* php8.5* nodejs

$ apt autoremove -y
$ apt autoclean

8 — Remove APT repositories

bash
$ add-apt-repository --remove ppa:ondrej/php -y
$ rm -f /etc/apt/sources.list.d/mariadb.list
$ rm -f /etc/apt/sources.list.d/nodesource.list
$ rm -f /etc/apt/keyrings/mariadb-keyring.pgp
$ apt update

9 — Remove Composer and Deployer

bash
$ rm -f /usr/local/bin/composer
$ rm -f /usr/local/bin/dep

10 — Remove swap file

bash
$ swapoff /var/swap.1
$ rm -f /var/swap.1
# Remove the swap entry from /etc/fstab
$ sed -i '/swap\.1/d' /etc/fstab

11 — Restore SSH and PAM defaults

Cipi hardens SSH (disables root login and password auth) and adds PAM hooks. If you need to restore defaults:

bash
# Restore sshd_config to allow password auth (if needed)
$ sed -i 's/^PasswordAuthentication no/PasswordAuthentication yes/' /etc/ssh/sshd_config
$ sed -i 's/^PermitRootLogin no/PermitRootLogin yes/' /etc/ssh/sshd_config

# Remove Cipi PAM hooks
$ sed -i '/cipi-auth-notify/d' /etc/pam.d/sshd
$ sed -i '/cipi-auth-notify/d' /etc/pam.d/sudo

# Restore sysctl
$ sed -i '/vm.swappiness/d' /etc/sysctl.conf
$ sysctl -p

$ systemctl restart sshd

12 — Reset firewall

bash
$ ufw disable
$ ufw reset
After a full uninstall, the server will be stripped of its web stack and security hardening. The recommended approach is to reprovision the server from a clean OS image rather than trying to reconfigure the same machine. Use this guide primarily to clean up before a fresh start, or to selectively remove Cipi components while keeping packages you still need.