Advanced
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
$ 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 appsapps-create— create appsapps-edit— edit appsapps-delete— delete appsdeploy-manage— deploy, rollback, unlockssl-manage— install and manage SSL certificatesaliases-view— read aliasesaliases-create— add aliasesaliases-delete— remove aliasesdbs-view— list databasesdbs-create— create databasesdbs-delete— delete databasesdbs-manage— backup, restore, regenerate passwordmcp-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:
- Configure the API with
cipi api <domain>andcipi api ssl - Create a token with
cipi api token createand select at leastmcp-access - Add the MCP server to your client config (see below)
Cursor
Add to ~/.cursor/mcp.json (or Cursor → Settings → MCP):
{
"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:
{
"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:
$ 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:
{
"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>andcipi api ssl - Sanctum Bearer token with the required abilities:
| Ability | Required for |
|---|---|
apps-view | Test Connection, App Info |
apps-create | Create Account |
apps-edit | Change Package |
apps-delete | Terminate Account |
deploy-manage | Deploy, Rollback, Unlock |
ssl-manage | Install SSL, Auto-SSL |
Installation
- Copy
modules/servers/cipi/into your WHMCS root:your-whmcs/ └── modules/ └── servers/ └── cipi/ ├── cipi.php └── lib/ └── CipiApiClient.php - 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)
- Create a hosting product linked to this server and configure Module Settings:
| Setting | Description | Default |
|---|---|---|
| PHP Version | 8.2 / 8.3 / 8.4 / 8.5 | 8.5 |
| App Type | laravel or custom | laravel |
| Git Repository (SSH) | Required for Laravel; optional for custom | — |
| Git Branch | Branch to deploy | main |
| Auto SSL | Install Let’s Encrypt after creation | No |
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 Connection | GET /api/apps | Validates token and API reachability |
| Create Account | POST /api/apps | Provisions a Cipi app (Laravel or custom); waits for async jobs; optionally installs SSL |
| Suspend / Unsuspend | none | Returns success so WHMCS updates billing state (Cipi has no suspend endpoint) |
| Terminate Account | DELETE /api/apps/{name} | Removes the app; waits for async jobs |
| Change Package | PUT /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 SSL | POST /api/apps/{name}/ssl | Install a Let’s Encrypt certificate |
| Deploy | POST /api/apps/{name}/deploy | Trigger a zero-downtime deployment |
| Rollback Deploy | POST /api/apps/{name}/deploy/rollback | Revert to the previous release |
| Unlock Deploy | POST /api/apps/{name}/deploy/unlock | Unlock a stuck deployment |
| App Info | GET /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 |
|---|---|
| Apps | listApps, getApp, createApp, editApp, deleteApp |
| Deploy | deployApp, rollbackDeploy, unlockDeploy |
| SSL | installSsl |
| Aliases | listAliases, addAlias, removeAlias |
| Databases | listDatabases, createDatabase, deleteDatabase, backupDatabase, restoreDatabase, resetDatabasePassword |
| Jobs | getJob, waitForJob |
Extending the module
// 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.
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
$ 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.
# 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"
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.
# 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.
# 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:
- Linux user — Creates a new user with a random password
- Directories — Creates
/home/<app>/shared/,logs/,.ssh/,.deployer/ - SSH deploy key — Restores from archive (same key works with GitHub/GitLab without reconfiguration)
- MariaDB database — Creates database + user with a new random password
- Database data — Imports the dump if
--with-dbwas used during export .env— Copies from archive, then overwritesDB_PASSWORD,DB_USERNAME,DB_DATABASE,DB_HOSTwith the new server's values. Everything else (APP_KEY,MAIL_*,REDIS_*, custom vars) stays as-is- PHP-FPM pool, Nginx vhost, Supervisor workers, Crontab, Deployer — Fully configured from archive data
Safety checks before import
The import runs pre-flight checks before touching anything:
- App already exists — blocked unless
--updateis 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.
$ cipi sync import /tmp/archive.tar.gz.enc --update --passphrase="MyStr0ngP@ss"
What update does for an existing app
.envsync — The archive.envreplaces the local one, butDB_PASSWORD,DB_USERNAME,DB_DATABASE, andDB_HOSTare 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
.envautomatically. - Nginx vhost, Supervisor workers, Deployer config — Regenerated from archive data.
- Deploy — If
--deployis passed, runsdep deployto 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 installseparately)
List (inspect archive)
View what is inside an archive without importing anything.
$ 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.
# 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
- Step 1: Runs
cipi sync exportlocally (encrypts with passphrase) - Step 2: Transfers the encrypted archive to the target via rsync
- Step 3: If
--importis passed, runscipi sync import --update --yeson 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:
# 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.
# 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:
# 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.
# 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
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
# 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)
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 installafter 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
# 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.
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
$ 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-notifywrapper) - 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
sudoorsu, including who ran it, target user (forsu), SSH key, client IP, and TTY - Privileged SSH login — notifies when
rootor 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
cipiuser, 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
sudoorsu. The notification includes the username, target user (forsu), the TTY, SSH key, client IP, and timestamp. - Privileged SSH login — triggered when
rootor any user in thesudogroup logs in via SSH. The notification includes the username, source IP address, SSH key fingerprint, and key comment (resolved from/var/log/auth.logfingerprint matching againstauthorized_keys). - SSH key changes — triggered when an SSH key is added to, removed from, or
renamed on the
cipiuser viacipi ssh add,cipi ssh remove, orcipi 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 |
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:
REDIS_HOST=127.0.0.1 REDIS_PASSWORD=your-password-from-server-json REDIS_PORT=6379
Then set the drivers for each use case:
- Cache —
CACHE_STORE=redis - Session —
SESSION_DRIVER=redis - Queue —
QUEUE_CONNECTION=redis(thencipi worker restart myapp) - Broadcasting —
BROADCAST_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.
$ cipi self-update --check # check for a new version $ cipi self-update # update to latest
Update process
- Downloads the latest version from GitHub
- Backs up the current installation to
/opt/cipi.bak.YYYYMMDDHHMMSS/ - Replaces CLI and lib scripts
- Runs any pending migration scripts in order (e.g. new Nginx directives, new packages)
- 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:
$ 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.
# 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:
$ nginx -t && systemctl reload nginx
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.
$ 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.
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.
# 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
$ userdel -r cipi $ groupdel cipi-ssh 2>/dev/null $ groupdel cipi-apps 2>/dev/null
3 — Remove Cipi binaries, libraries, and data
$ 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)
$ 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
# 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
# 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.
$ 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
$ 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
$ rm -f /usr/local/bin/composer $ rm -f /usr/local/bin/dep
10 — Remove swap file
$ 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:
# 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
$ ufw disable $ ufw reset