Security
SSH trust model, deploy key setup, webhook secrets, and operational security practices.
SSH trust model
The daemon connects to target hosts using public key authentication. The private key path is set via the DEEPLO_SSH_KEY_FILE environment variable.
Host key checking is controlled by DEEPLO_SSH_HOST_KEY_POLICY:
| Policy | Behavior |
|---|---|
accept-new (default) | Automatically accepts and remembers new host keys on first connection (TOFU). Subsequent connections verify against the stored key. |
strict | Only connects to hosts already in the known_hosts file. Connections to unknown hosts are rejected. |
The known_hosts file path defaults to DEEPLO_DATA_DIR/known_hosts. Override with DEEPLO_SSH_KNOWN_HOSTS.
To pre-populate known_hosts for strict mode:
ssh-keyscan 10.0.0.10 10.0.0.11 >> /path/to/known_hostsDeploy key
Use a dedicated SSH key pair for deployments. Do not reuse your personal key.
Recommended setup:
- Generate a deploy key:
ssh-keygen -t ed25519 -f secrets/deploy_key -N "" - Add the public key (
deploy_key.pub) to~/.ssh/authorized_keyson each target host, restricted to the deploy user - Set
DEEPLO_SSH_KEY_FILE=/run/secrets/deploy_keyin the daemon's environment
The key needs only enough access to: SSH in as the deploy user, read from the home directory, and run docker compose commands.
Do not mount your personal SSH key. Use a key dedicated to this role with no other permissions.
No agent on targets
The daemon SSHes into target hosts at deploy time and runs commands over the SSH session. Nothing runs permanently on target hosts between deploys.
This is intentional. There is no persistent agent to:
- Secure or update separately
- Compromise via a vulnerability in the agent software
- Be exploited to pivot from one host to others
The only attack surface on target hosts is the deploy user's SSH access.
Deploy user privileges
The deploy user on target hosts needs:
- SSH access
- Permission to run
docker composecommands
The simplest setup is adding the user to the docker group:
usermod -aG docker deployBe aware that membership in the docker group is effectively equivalent to root access on that host — a container with a bind mount to / can access the full filesystem. This is a Docker limitation, not specific to deeplo.
If this is a concern, use sudo with a restricted policy that allows only specific docker compose commands, or run Docker in rootless mode.
GitHub webhook secret
Set DEEPLO_GITHUB_WEBHOOK_SECRET_FILE to a file containing a random secret, and configure the same secret in GitHub's webhook settings. The daemon verifies the HMAC-SHA256 signature on every incoming webhook request.
Without a secret, any POST to /webhooks/github that looks like a valid push event will trigger a deploy. This is acceptable on a private network but not on a public endpoint.
Generate a strong secret:
openssl rand -hex 32 > secrets/github_webhook_secretNetwork exposure
The daemon's HTTP server (DEEPLO_WEBHOOK_PORT=8080 by default) needs to be reachable from GitHub for webhooks. If you're using poll-only mode, you can block all inbound traffic to port 8080.
The /healthz endpoint is always available and requires no authentication. It returns 200 ok.
There is no authentication on the webhook endpoint beyond HMAC signature validation. The endpoint should not be rate-limited in ways that would cause GitHub retries to be missed.
Secrets model
The YAML config stores no secrets or paths. All credentials are passed via environment variables pointing to secret files. This means:
- The config YAML can be checked into version control (it contains no secrets)
- Secrets are read from files at runtime (compatible with Docker secrets, Vault agent, etc.)
- Rotating a secret means replacing the file and restarting the daemon; no config change is needed
Operational practices
- Run the daemon as a non-root, dedicated user (
deeplo) - Mount the private key read-only (
rovolume flag) - Mount the config file read-only
- Keep
DEEPLO_DATA_DIRon a local filesystem with appropriate permissions - Rotate the deploy key periodically; update known_hosts when target host keys change
- If you remove a host, also remove its key from known_hosts — stale entries are harmless but good hygiene