Setup & Configuration¶
Last update: June 4, 2026
This page covers every configuration topic for AI-Bridge for Cisco UC β environment variables, authentication modes, client management, RBAC profiles, TLS configuration, and backup settings.
The .env File¶
All runtime configuration lives in a single .env file at the project root. It is created automatically by the setup wizard and preserved intact across upgrades.
Protect your .env
The .env file contains RSA key paths, client definitions, and other security-sensitive values. Ensure it is readable only by the service account running AI-Bridge (chmod 600 .env).
A commented template with all available keys is provided in .env.example.
Only .env is read β os.environ overrides are NOT honoured
The server loads its configuration exclusively from the .env file at
the project root (via python-dotenv's dotenv_values). Process-level
environment variables exported by the shell (e.g. export AUTH_ISSUER=β¦)
or injected by Docker (-e AUTH_ISSUER=β¦, environment: block in
docker-compose.yml) are deliberately ignored.
Rationale: single source of truth, full
auditability (one file on disk, captured by the integrity manifest, the
backups, and show_tech reports), restart parity between bare-metal and
Docker deployments. To override a value, edit .env (or template it from
your CI/CD) β never rely on os.environ injection.
Environment Variables Reference¶
Project Metadata¶
These keys identify the installation. They are managed automatically by the upgrade runner β do not edit them manually.
| Key | Description | Example |
|---|---|---|
PROJECT_NAME |
Internal identifier | ai-bridge-for-cisco-uc |
PROJECT_CREATOR |
Publisher | Pierre SOURDEAU |
PROJECT_VERSION |
Build version YYYYMM |
202605 |
Auto-managed keys
PROJECT_NAME, PROJECT_CREATOR, and PROJECT_VERSION are overwritten automatically during every upgrade. Any manual edits will be lost.
MCP Server¶
| Key | Description | Default |
|---|---|---|
MCP_SERVER_NAME |
Human-readable name shown to AI agents | AI-Bridge for Cisco UC |
MCP_SERVER_INSTRUCTIONS |
Description injected into agent context | MCP SERVER for Cisco UC |
MCP_SERVER_TRANSPORT |
Transport type | streamable-http |
MCP_SERVER_HOST |
Bind IP address inside the container | 0.0.0.0 |
MCP_SERVER_PORT |
HTTPS listening port | 8443 |
MCP_SERVER_URL_PATH |
MCP endpoint path | /mcp |
Docker deployment
MCP_SERVER_HOST must remain 0.0.0.0 for Docker port mapping to work. The container's network namespace is isolated β binding to a host IP address would fail. Control external access with your host firewall and MCP_SERVER_SECURITY_ALLOWED_HOSTS.
Docker Settings¶
| Key | Description | Default |
|---|---|---|
UID |
UID of the host user β the container process runs as this UID | 1000 |
GID |
GID of the host user β the container process runs as this GID | 1000 |
Run id -u && id -g on the Docker host to find your values. These must match the owner of the bind-mounted volume directories (secrets/, clients/, logs/, etc.).
Transport
streamable-http is currently the only supported transport. SSE transport is not offered (legacy).
Authentication & Cryptography¶
| Key | Description | Default |
|---|---|---|
AUTH_RSA_PUBLIC_KEY_FILE |
Path to RSA public key (PEM) β used for JWT signature verification | ./secrets/rsa_public.pem |
AUTH_RSA_PRIVATE_KEY_FILE |
Path to RSA private key (PEM) β used for JWT signing and backup decryption. | ./secrets/rsa_private.pem |
AUTH_ISSUER |
JWT issuer claim and OAuth 2.1 Authorization Server base URL β must be the publicly reachable HTTPS URL of the server | https://mcp.domain.com:8443 |
AUTH_AUDIENCE |
Expected JWT aud claim β must match the value configured in AI agent clients |
mcp-clients |
AUTH_CLIENTS |
Client definitions (see Client Management section below) | (must be configured) |
CRYPTO_SECRETS_DIR |
Directory for auto-generated secrets (tokens, keys, bcrypt hashes) | ./secrets/ |
AUTH_MODE |
Global authentication mode | jwt |
AUTH_RSA_KEY_SIZE |
RSA key size in bits used when generating a new key pair | 4096 |
AUTH_TOKEN_EXPIRY_DAYS |
JWT token validity in days | 365 |
AUTH_OAUTH_TOKEN_EXPIRY_SECONDS |
OAuth 2.1 access token validity in seconds | 3600 |
AUTH_FAIL_LIMIT |
Failed authentication attempts before IP is blocked | 10 |
AUTH_FAIL_WINDOW_SECONDS |
Sliding window duration for failure counting (seconds) | 300 |
AUTH_BLACKLIST_DURATION_SECONDS |
IP block duration after exceeding fail limit (seconds) | 900 |
AUTH_OAUTH_REDIRECT_URIS |
JSON dict {"client": ["uri", ...]} β per-client allowlist of OAuth 2.1 redirect_uri values (wildcard * allowed) |
(empty β no whitelist) |
AUTH_OAUTH_REDIRECT_URI_ENFORCEMENT |
off (default, backwards-compat) or strict (recommended in production β rejects unknown redirect_uri) |
off |
INTEGRITY_CHECK_INTERVAL_HOURS |
Interval between two integrity-manifest sweeps performed by the watchdog | 24 |
BACKUP_SFTP_KNOWN_HOSTS |
OpenSSH known_hosts pinning the SFTP destination hostkey (see Backup Configuration below) |
./secrets/sftp_known_hosts |
Valid AUTH_MODE values: jwt / oauth2 / jwt+oauth2
Valid AUTH_RSA_KEY_SIZE values: 2048 / 3072 / 4096
Logging¶
| Key | Description | Default |
|---|---|---|
LOGGING_LEVEL |
Log verbosity | info |
LOGGING_DIR |
Log file directory | ./logs/ |
LOGGING_ROTATE_SIZE |
Max log file size before rotation (bytes) | 5000000 (~5 MB) |
LOGGING_ROTATE_FILES |
Number of rotated archives to keep | 3 |
LOGGING_CONSOLE_ENABLED |
Enable colored console output | 1 |
Valid LOGGING_LEVEL values: debug / info / warning / error / critical
Report Charts¶
| Key | Default |
|---|---|
REPORT_CHARTS_COLORS |
PASS=#78F5AE,WARN=#FFBE54,FAIL=#FF6F61,INFO=#69C2FF,N/A=#BACCCC |
CUCM Server¶
| Key | Default |
|---|---|
SERVER_CUCM_AXL_PORT |
8443 |
SERVER_CUCM_RIS_PORT |
8443 |
SERVER_CUCM_SSH_PORT |
22 |
SERVER_CUCM_SSH_TIMEOUT |
30 |
SERVER_CUCM_ZEEP_TIMEOUT |
30 |
SERVER_CUCM_AXL_SCHEMAS_DIR |
./servers/cucm/axl-schemas/ |
SERVER_CUCM_RIS_SCHEMAS_DIR |
./servers/cucm/ris-schemas/ |
SERVER_CUCM_AXL_WSDL_TIMEOUT |
10 |
SERVER_CUCM_RIS_WSDL_TIMEOUT |
15 |
SERVER_CUCM_SSH_TERM |
dumb |
SERVER_CUCM_SSH_TERM_WIDTH |
220 |
SERVER_CUCM_SSH_TERM_HEIGHT |
50 |
SERVER_CUCM_PHONE_SCREENSHOT_TIMEOUT |
10 |
SERVER_CUCM_ZEEP_CACHE_MAX |
64 |
SERVER_CUCM_WSDL_CACHE_MAX |
32 |
SERVER_IMP_ZEEP_CACHE_MAX |
64 |
SERVER_IMP_WSDL_CACHE_MAX |
32 |
PROFILE_SCHEMA_STRICT |
1 |
Phone SSL verification
SSL certificate verification is automatically disabled for phone web connections.
Cisco IP Phone certificates (MIC/LSC) are not compliant with Python 3.13+ strict X.509 validation:
they lack the Authority Key Identifier (AKI) extension and use MAC-based CN (e.g. SEP001122334455)
instead of the IP address. Security for phone connections relies on managed VLAN isolation,
HTTP Basic Auth credentials, and the phone IP being obtained from a trusted source (CUCM RIS/AXL).
IMP Server¶
| Key | Default |
|---|---|
SERVER_IMP_AXL_PORT |
8443 |
SERVER_IMP_SSH_PORT |
22 |
SERVER_IMP_SSH_TIMEOUT |
120 |
SERVER_IMP_ZEEP_TIMEOUT |
30 |
SERVER_IMP_AXL_SCHEMAS_DIR |
./servers/imp/axl-schemas/ |
SERVER_IMP_AXL_WSDL_TIMEOUT |
10 |
SERVER_IMP_SSH_TERM |
dumb |
IMP SSH timeout
IMP SSH sessions may run longer diagnostics than CUCM. The default SERVER_IMP_SSH_TIMEOUT=120 is intentionally higher.
CUC Server¶
| Key | Default |
|---|---|
SERVER_CUC_CUPI_PORT |
8443 |
SERVER_CUC_CUPI_TIMEOUT |
30 |
SERVER_CUC_SSH_PORT |
22 |
SERVER_CUC_SSH_TIMEOUT |
60 |
SERVER_CUC_SSH_TERM |
dumb |
SERVER_CUC_SSH_TERM_WIDTH |
220 |
SERVER_CUC_SSH_TERM_HEIGHT |
50 |
No AXL / RIS / cluster on CUC
CUC exposes only CUPI (REST on tcp/8443) and SSH (tcp/22). There is no AXL toolkit, no RIS, and no cluster concept β credentials and operations are scoped per host.
Transport Security¶
| Key | Description | Default |
|---|---|---|
MCP_SERVER_SECURITY_TRANSPORT_ENABLED |
Enable DNS rebinding protection | 1 |
MCP_SERVER_SECURITY_ALLOWED_HOSTS |
Comma-separated allowed Host header values |
localhost:*,127.0.0.1:*,mcp.domain.com:* |
MCP_SERVER_SECURITY_ALLOWED_ORIGINS |
Comma-separated allowed Origin header values |
https://localhost:8443,https://127.0.0.1:8443,https://mcp.domain.com:8443 |
The * wildcard in ALLOWED_HOSTS matches any port on that hostname (e.g., localhost:* allows localhost:8443, localhost:4433, etc.).
SSL Verification per Product¶
The same pattern applies to CUCM, IMP, and CUC:
| Key | Description | Default |
|---|---|---|
SERVER_CUCM_SSL_VERIFY |
Verify TLS certificate of CUCM nodes | True |
SERVER_CUCM_SSL_CA_BUNDLE |
Path to CA bundle for CUCM TLS verification (empty = system trusted CA), or product certificates bundle | (empty) |
| Key | Description | Default |
|---|---|---|
SERVER_IMP_SSL_VERIFY |
Verify TLS certificate of IMP nodes | True |
SERVER_IMP_SSL_CA_BUNDLE |
Path to CA bundle for IMP TLS verification (empty = system trusted CA), or product certificates bundle | (empty) |
| Key | Description | Default |
|---|---|---|
SERVER_CUC_SSL_VERIFY |
Verify TLS certificate of CUC nodes (CUPI/REST) | True |
SERVER_CUC_SSL_CA_BUNDLE |
Path to CA bundle for CUC TLS verification (empty = system trusted CA), or product certificates bundle | (empty) |
Custom CA for Cisco UC connections
To trust an internal CA for outbound AXL/CUPI/HTTPS connections to Cisco UC nodes, specify the CA bundle path in SERVER_CUCM_SSL_CA_BUNDLE / SERVER_IMP_SSL_CA_BUNDLE / SERVER_CUC_SSL_CA_BUNDLE. Files placed in certs-trust-store/ are not automatically applied to these connections.
Backup Configuration¶
| Key | Description | Default |
|---|---|---|
BACKUP_ENABLED |
Enable scheduled backups (0 / 1) |
0 |
BACKUP_DIR |
Local backup directory | ./backups/ |
BACKUP_EXCLUDE_DIRS |
Comma-separated directories excluded from the archive | ./backups/,./.git/,./__pycache__/ |
BACKUP_RETENTION |
Number of local archives to retain | 4 |
BACKUP_INTERVAL_HOURS |
Backup interval (hours) | 168 |
BACKUP_SFTP_HOST |
SFTP server FQDN or IP (optional) | (empty) |
BACKUP_SFTP_PORT |
SFTP server TCP port | 22 |
BACKUP_SFTP_USER |
SFTP username (optional) | (empty) |
BACKUP_SFTP_AUTH |
SFTP auth method: key or password |
key |
BACKUP_SFTP_KEY_FILE |
Path to the SSH private key (when BACKUP_SFTP_AUTH=key) |
./secrets/backup_ssh_key |
BACKUP_SFTP_KNOWN_HOSTS |
OpenSSH known_hosts file used to verify the SFTP server hostkey |
./secrets/sftp_known_hosts |
BACKUP_SFTP_PASSWORD_ENC |
Encrypted SFTP password (generated by encrypt_sftp_password.py) |
(empty) |
BACKUP_SFTP_REMOTE_PATH |
Remote directory for SFTP uploads | /backups/ai-bridge/ |
BACKUP_SFTP_RETENTION |
Number of remote archives to retain | 52 |
!!! "SFTP hostkey verification is mandatory"
Before the very first SFTP transfer, populate BACKUP_SFTP_KNOWN_HOSTS with the trusted hostkey of the remote server. If the file is missing or contains no entry for the host, the backup transfer aborts immediately β credentials are never sent to an unverified server (anti-MITM).
```bash
# Default location: ./secrets/sftp_known_hosts
ssh-keyscan -p 22 sftp.example.com >> ./secrets/sftp_known_hosts
# Non-default port: the entry is stored as "[host]:port"
ssh-keyscan -p 2222 sftp.example.com >> ./secrets/sftp_known_hosts
```
**Verify the fingerprint out-of-band** (phone call to the SFTP admin, internal wiki, etc.) before trusting the file. The format is standard OpenSSH `known_hosts`.
SFTP password encryption
When using BACKUP_SFTP_AUTH=password, the password must be encrypted before being stored in .env. Use the dedicated script:
.env as BACKUP_SFTP_PASSWORD_ENC. It is never stored in plaintext. See the Operations page for details.
Authentication Modes¶
JWT Mode (Default)¶
JWT is the simplest deployment mode. Tokens are pre-generated at startup and stored on disk.
How it works:
- A signed RS256 JWT is generated for each client at startup
- Tokens are written to
clients/<name>/secrets/.token - Token lifetime is controlled by
AUTH_TOKEN_EXPIRY_DAYS(default: 365 days) - Tokens are automatically renewed at startup when: missing, corrupted, invalid signature, claims changed, or subject changed
JWT claims structure:
| Claim | Description |
|---|---|
sub |
Client subject (from AUTH_CLIENTS) |
iss |
Issuer (AUTH_ISSUER) |
aud |
Audience (AUTH_AUDIENCE) |
iat |
Issued-at timestamp |
exp |
Expiration timestamp |
jti |
Unique token ID (for revocation) |
client_id |
Client name |
Revoking a JWT token:
OAuth 2.1 Mode¶
OAuth 2.1 uses short-lived dynamic tokens, better suited for automated pipelines and environments with stricter credential hygiene.
How it works:
- Two grant types are supported:
authorization_code+ PKCE (RFC 7636) β for interactive clients such as AI IDEs (OpenCode, Claude Desktop). The client opens a browser login page, the user authenticates, and a short-lived token is issued automatically.client_credentials(RFC 6749 Β§4.4) β for headless M2M clients usingcurlor scripts.
- Token lifetime is controlled by
AUTH_OAUTH_TOKEN_EXPIRY_SECONDS(default: 3600 s) client_id= client name;client_secretis stored as a bcrypt hash in.secret_hash- The plaintext
.secretfile is automatically deleted after the first successful authentication - Token revocation:
POST /revoke(RFC 7009) - Authorization Server Metadata:
GET /.well-known/oauth-authorization-server(RFC 8414) - Protected Resource Metadata:
GET /.well-known/oauth-protected-resource(RFC 9728) β required by MCP 2025-03-26 compliant clients
Interactive login flow (AI IDEs β authorization_code + PKCE):
When a compliant MCP client (OpenCode, etc.) connects to the MCP endpoint, it automatically:
- Detects the 401 response and performs OAuth discovery
- Opens the AI Bridge login page in your browser (
GET /authorize) - You enter your
client_idandclient_secret - The browser is redirected back to the client with an authorization code
- The client exchanges the code for a Bearer token (
POST /token)
No manual steps required after initial configuration.
M2M token request (client_credentials grant):
curl -sk -X POST https://mcp.domain.com:8443/token \
-u "<client_id>:<client_secret>" \
-d "grant_type=client_credentials"
One-time secret
The plaintext .secret file is a one-time credential. Copy it before the client's first authentication β it will be deleted automatically on first use. Only the bcrypt hash (.secret_hash) is retained.
Hybrid JWT + OAuth 2.1 Mode¶
Setting AUTH_MODE=jwt+oauth2 enables both modes simultaneously. Each client independently declares its own auth mode as the 4th field in AUTH_CLIENTS.
This allows mixed environments where some clients use static JWT tokens (e.g., interactive AI agents) while others use dynamic OAuth tokens (e.g., automated pipelines).
Client Management¶
AUTH_CLIENTS Syntax¶
| Key | Description | Default |
|---|---|---|
CLIENTS_DIR |
Base directory for client data | ./clients/ |
# JWT or OAuth 2.1 mode (3 fields)
AUTH_CLIENTS='name:subject:profile'
# Hybrid mode (4 fields β only valid when AUTH_MODE=jwt+oauth2)
AUTH_CLIENTS='name:subject:profile:auth_mode'
# Multiple clients (comma-separated)
AUTH_CLIENTS='alice:sub-alice:admin:jwt,bob:sub-bob:operator:oauth2,carol:sub-carol:auditor:jwt+oauth2'
Field definitions:
| Field | Rules |
|---|---|
name |
Unique identifier; used as the client directory name. Alphanumeric + hyphens. |
subject |
JWT sub claim. Recommended format: sub-<name>-YYYYMMDD. Immutable once tokens have been distributed β changing it invalidates all issued tokens. |
profile |
RBAC profile name. Must match a .json file in profiles/. |
auth_mode |
jwt / oauth2 / jwt+oauth2. Only valid in hybrid global mode. |
Subject immutability
The subject field is embedded in every issued JWT token. Changing a client's subject invalidates all previously issued tokens and forces re-issuance. Plan subject values carefully before distributing tokens.
Client Directory Structure¶
Each client gets a dedicated directory under CLIENTS_DIR:
clients/<name>/
βββ secrets/
β βββ .uuid # Stable UUID used for credential encryption key derivation
β βββ .token # JWT bearer token (jwt mode)
β βββ .secret # OAuth client secret β plaintext, one-time use
β βββ .secret_hash # OAuth client secret β bcrypt hash (permanent)
β βββ ...
βββ reports/ # Markdown reports and PDF exports
βββ custom/
βββ logo.png # Custom logo embedded in PDF reports
Client Lifecycle at Startup¶
At every startup, the server synchronizes the clients/ directory against AUTH_CLIENTS:
| Condition | Action |
|---|---|
Client in AUTH_CLIENTS but no directory |
Directory created, credentials generated |
| Client exists, token valid | No action |
| Client exists, token invalid (expired, bad sig, claims changed) | Token renewed automatically |
Client in directory but not in AUTH_CLIENTS |
Directory purged |
Automatic purge
Removing a client from AUTH_CLIENTS and restarting the server permanently deletes the client's directory, including all reports and credentials. Export any needed data before removing a client.
RBAC Profiles¶
Predefined Profiles¶
Three profiles are shipped in the profiles/_default/ directory:
| Profile | Product Scope | AXL Operations | SSH Access | RIS Access | Phone | CUPI Access (CUC) | Rate Limits (req/min) |
|---|---|---|---|---|---|---|---|
admin |
All | All prefixes + SQL Update | All commands | All device classes | Screenshot | All verbs (GET/POST/PUT/DELETE) |
60 / 5 / 20 / 10 / 60 |
operator |
cucm, imp, cuc | get, list + phone write + SQLQuery | show only |
Phone only | Screenshot | GET + targeted PUT (users, callhandlers) |
30 / 5 / 20 / 10 / 30 |
auditor |
cucm, imp, cuc | get, list + SQLQuery | show + audit utils |
All device classes | Screenshot | GET /vmrest/ only (read-only) |
30 / 5 / 20 / 5 / 30 |
Rate limit columns
Rate limit columns represent: AXL / SSH / RIS / Phone / Common (report) requests per minute, per client.
Default profile sync
At startup, the three default profiles are automatically copied from profiles/_default/ into profiles/. This ensures they stay up-to-date after an upgrade. Custom profiles created in profiles/ are never overwritten.
Custom Profile JSON Structure¶
Create a .json file in profiles/ with the following structure:
{
"description": "Profile description",
"product_scope": ["cucm", "imp", "all"],
"cucm": {
"axl": {
"rate_limit": 30,
"allowed_prefixes": ["get", "list"],
"allowed_exact": ["executeSQLQuery"]
},
"ris": {
"rate_limit": 10,
"allowed_device_classes": ["Phone"],
"allowed_statuses": ["Any"]
},
"ssh": {
"rate_limit": 20,
"allowed_prefixes": ["show"]
}
},
"imp": {
"axl": { "rate_limit": 30, "allowed_prefixes": ["get", "list"] },
"ssh": { "rate_limit": 20, "allowed_prefixes": ["show"] }
},
"cuc": {
"cupi": {
"rate_limit": 30,
"allowed_prefixes": ["GET /vmrest/", "PUT /vmrest/users"]
},
"ssh": {
"rate_limit": 5,
"allowed_prefixes": ["show"]
}
},
"common": {
"report": { "rate_limit": 30 }
}
}
Matching rules:
| Scope | Rule | Example |
|---|---|---|
| AXL prefix | Any operation starting with the prefix is allowed | list β allows listPhone, listLine, listUser, β¦ |
| AXL exact | Exact operation name match only | executeSQLQuery β allows only that exact call |
| SSH prefix | Command must equal the prefix or start with <prefix> (space-separated) |
show β allows show version, show interface, β¦ |
| RIS filters | allowed_device_classes and allowed_statuses are optional; if absent, no filter is applied |
["Phone"] β restricts to phone device registrations |
| CUPI prefix | Allowed CUPI calls β full <METHOD> <path> prefix match (CUC only) |
GET /vmrest/ β allows any GET under /vmrest/ |
SSH section omission
If the ssh section is absent from a product in the profile, SSH access is denied for that product.
Deploying a custom profile:
- Create
profiles/my_profile.json - Assign it in
AUTH_CLIENTS:name:subject:my_profile - Profiles are hot-reloaded on file change β no server restart required
TLS & Transport Security¶
Self-Signed Certificate (Default)¶
At first startup, if no CA-signed certificate is already deployed (TLS_CA_SIGNED_CERT_FILE / TLS_CA_SIGNED_KEY_FILE), AI-Bridge auto-generates a self-signed TLS certificate with SAN entries derived from MCP_SERVER_SECURITY_ALLOWED_HOSTS. The certificate is stored in secrets/server.crt and secrets/server.key.
Midlife renewal: the certificate is automatically renewed when less than 50% of its validity period remains.
CA-Signed Certificate¶
To replace the self-signed certificate with a CA-issued one:
# Place your certificate and private key:
secrets/server.crt # PEM-encoded certificate (include full chain)
secrets/server.key # PEM-encoded private key
If both files exist at startup, the server uses them instead of the auto-generated certificate.
Recommended for production
Using a CA-signed certificate eliminates TLS trust warnings on AI agent clients and is strongly recommended for any production deployment.
Transport Security Settings¶
| Key | Description | Example / Default |
|---|---|---|
MCP_SERVER_SECURITY_TRANSPORT_ENABLED |
Enable DNS rebinding protection (validates Host and Origin headers) |
1 |
MCP_SERVER_SECURITY_ALLOWED_HOSTS |
Comma-separated allowed Host header values. Supports * as port wildcard |
mcp.domain.com:8443,localhost:* |
MCP_SERVER_SECURITY_ALLOWED_ORIGINS |
Comma-separated allowed Origin header values |
https://mcp.domain.com:8443 |
DNS rebinding protection
Setting MCP_SERVER_SECURITY_TRANSPORT_ENABLED=0 disables Host/Origin header validation. Only disable this in fully isolated network environments where DNS rebinding attacks are not a concern.
Backup Configuration¶
Encryption Model¶
AI-Bridge backups use hybrid encryption:
- A random AES-256-GCM session key encrypts the backup archive
- The session key is RSA-4096 OAEP wrapped using the server's public key
- An SHA-512 sidecar ensures integrity before restore
This means backups can be created by any process with access to the public key, but decryption requires the RSA private key.
Enabling Backups¶
SFTP Export (Optional)¶
Push backups to a remote SFTP server automatically after each local backup:
BACKUP_SFTP_HOST=sftp.domain.com
BACKUP_SFTP_PORT=22
BACKUP_SFTP_USER=aibridge
BACKUP_SFTP_AUTH=key # or: password
BACKUP_SFTP_KEY_FILE=./secrets/backup_ssh_key
BACKUP_SFTP_KNOWN_HOSTS=./secrets/sftp_known_hosts
BACKUP_SFTP_REMOTE_PATH=/backups/ai-bridge/
BACKUP_SFTP_RETENTION=52
Trust the SFTP server hostkey first
The very first thing to do is to register the remote SFTP server hostkey in BACKUP_SFTP_KNOWN_HOSTS. Without this entry, no transfer will ever happen β the server aborts before sending any credential (anti-MITM protection).
ssh-keyscan -p ${BACKUP_SFTP_PORT:-22} ${BACKUP_SFTP_HOST} >> ./secrets/sftp_known_hosts
chmod 600 ./secrets/sftp_known_hosts
Compare the printed fingerprint with the one published by the SFTP administrator (out-of-band) before keeping the file.
Place the SSH private key at secrets/backup_ssh_key (or override the path with BACKUP_SFTP_KEY_FILE). The corresponding public key must be authorized on the SFTP server.
Set BACKUP_SFTP_AUTH=password. The password must be encrypted before storage using:
The resulting token is automatically copied into .env as BACKUP_SFTP_PASSWORD_ENC. It is never stored in plaintext. To verify an existing encrypted token without storing a new one:
See the Operations page for details.
SFTP key permissions
The secrets/backup_ssh_key private key file should be chmod 600 and owned by the service account.