# =========================================================================== # docker-mailserver management bastion (broker mode) # =========================================================================== # A single bastion that lets a sidecar app (e.g. a Nuxt "mail manager") run a # WHITELIST of docker-mailserver `setup` sub-commands — and nothing else. # # The app talks to the bastion over the internal docker network via SSH: # ssh agent@bastion-mail "email add jane@example.com " # The bastion validates the request against ALLOWED_COMMANDS, and on a match # runs docker exec -i mailserver setup email add jane@example.com # against the host docker socket. No match → refused, nothing runs. # # Drop this `bastion-mail` service into the same compose project as your # `mailserver` container (or any compose project on the same external # network), then `docker compose up -d bastion-mail`. # =========================================================================== services: bastion-mail: image: blaxsoftware/bastion:latest restart: unless-stopped environment: # ---------------------------------------------------------------- # COMMAND_PREFIX — trusted, operator-set. Prepended to every # validated request so clients send clean `email add …` commands # and never see the docker plumbing. `setup` is docker-mailserver's # in-container admin CLI; `-i` (not `-it`) because the caller has no # TTY over a scripted SSH/HTTP call. # ---------------------------------------------------------------- COMMAND_PREFIX: "docker exec -i mailserver setup" # ---------------------------------------------------------------- # ALLOWED_COMMANDS — the whitelist. A YAML block scalar (`|`) reads # like a string array: one extended-regex (ERE) rule per line. A # request is permitted only if it matches a rule WHOLE-LINE. # # Matched commands run WITHOUT a shell (; | & $() are literal args), # so the regex is the entire authorization boundary — keep argument # classes tight ([^ ]+ rather than .*). Values that must arrive # intact can't contain spaces; generate passwords space-free. # # Prefer the mounted file (see volumes) if you'd rather edit rules # without redeploying — the two sources are additive. # ---------------------------------------------------------------- ALLOWED_COMMANDS: | email add [^ ]+@[^ ]+ [^ ]+ email update [^ ]+@[^ ]+ [^ ]+ email del [^ ]+@[^ ]+ email list alias add [^ ]+@[^ ]+ [^ ]+ alias del [^ ]+@[^ ]+ [^ ]+ alias list quota set [^ ]+@[^ ]+ [0-9]+[KMGT]? quota del [^ ]+@[^ ]+ volumes: # REQUIRED — the host docker socket, so `docker exec` can reach the # mailserver container. This is host-root-equivalent; the allowlist # is what keeps it scoped. - /var/run/docker.sock:/var/run/docker.sock # Authorized clients — drop one pubkey per identity. The mail-manager # app's key goes here; read live, no restart to add/revoke. - ./docker-data/bastion/users.d:/etc/bastion/users.d # Persist the bastion's SSH host identity across rebuilds (bind mount, # never a named volume — `down -v` would wipe it and clients would see # a changed host key). - ./docker-data/bastion/keys:/etc/ssh/keys # OPTIONAL — live-editable allowlist (additive with ALLOWED_COMMANDS # above). Edit the file and the next request picks it up; no restart. # - ./allowed-commands.list:/etc/bastion/allowed-commands.list:ro # No host port published: the mail-manager reaches the bastion by # service name on the shared network (ssh agent@bastion-mail). Uncomment # to also expose SSH on the host for debugging — bind to localhost. # ports: # - "127.0.0.1:2222:22" networks: [web] # Your docker-mailserver container — referenced by name in COMMAND_PREFIX. # Shown here for context; usually it already lives in its own compose file. # mailserver: # image: ghcr.io/docker-mailserver/docker-mailserver:latest # container_name: mailserver # hostname: mail.example.com # networks: [web] # # …ports/volumes/env per the docker-mailserver docs… networks: web: external: true