docker-bastion/examples/docker-mailserver/docker-compose.yml

96 lines
4.3 KiB
YAML
Raw Normal View History

# ===========================================================================
# 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 <password>"
# The bastion validates the request against ALLOWED_COMMANDS, and on a match
# runs docker exec -i mailserver setup email add jane@example.com <password>
# 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