deeplo
Guides

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:

PolicyBehavior
accept-new (default)Automatically accepts and remembers new host keys on first connection (TOFU). Subsequent connections verify against the stored key.
strictOnly 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_hosts

Deploy key

Use a dedicated SSH key pair for deployments. Do not reuse your personal key.

Recommended setup:

  1. Generate a deploy key: ssh-keygen -t ed25519 -f secrets/deploy_key -N ""
  2. Add the public key (deploy_key.pub) to ~/.ssh/authorized_keys on each target host, restricted to the deploy user
  3. Set DEEPLO_SSH_KEY_FILE=/run/secrets/deploy_key in 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 compose commands

The simplest setup is adding the user to the docker group:

usermod -aG docker deploy

Be 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_secret

Network 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 (ro volume flag)
  • Mount the config file read-only
  • Keep DEEPLO_DATA_DIR on 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

On this page