# =========================================================================== # docker-mailserver "fake VPS" — one directory, nothing else # =========================================================================== # Goal: hand someone (or an agent) an SSH session that *feels* like a VPS, but # the only real thing on it is your docker-mailserver directory. The rest of # the host filesystem is simply not there — there is nothing else mounted to # see. # # How it works: # ssh agent@your-host # └─ bastion-vps (key auth; forces every session into the jail) # └─ docker exec -it dms-jail bash (interactive login shell) # └─ dms-jail container # • base image = disposable alpine + tooling (NOT your host) # • ONLY host data mounted: the docker-mailserver directory # • lands you in that directory with a real shell # # ─── SECURITY NOTE — READ THIS ──────────────────────────────────────────── # This setup gives the jail the docker socket so you can run `docker compose` # / restart the mailserver from inside. A docker socket is *host-root # equivalent*: from that shell, `docker run -v /:/host alpine ...` would expose # the entire host. So this is a *practical* one-directory VPS for trusted # operators, NOT a hard security sandbox. If you ever want a true "only this # directory exists, no escape" jail, drop the docker.sock mount from BOTH # services below — you lose `docker compose`/restart but gain a real boundary, # and manage the stack from a separate broker-mode bastion instead (see # ../docker-mailserver/). # ─────────────────────────────────────────────────────────────────────────── # # Setup: # 1. Paths are preset to /srv/docker-mailserver (host AND in-jail, identical). # If your directory lives elsewhere, change BOTH occurrences below and keep # the two sides of the colon IDENTICAL — that is what lets `docker compose` # inside the jail find the stack's bind mounts at the paths the host daemon # expects. A different in-jail path (e.g. .../dms-vps) still lets you read # and edit files, but `docker compose up` from the jail would create empty # dirs on the host at the wrong path and bring the stack up with no data. # 2. Drop your SSH public key: # mkdir -p docker-data/bastion/users.d # cp ~/.ssh/id_ed25519.pub docker-data/bastion/users.d/me.pub # 3. docker compose up -d --build # 4. ssh -p 2222 agent@your-host # you land in the directory, in bash # =========================================================================== services: # ---- The jail: a disposable shell box ----------------------------------- dms-jail: build: . image: dms-vps-jail:latest container_name: dms-jail restart: unless-stopped working_dir: /srv/docker-mailserver volumes: # THE one directory. Host path : in-jail path — keep them identical. # This is the ONLY real host data the SSH session can touch (read-write). - /srv/docker-mailserver:/srv/docker-mailserver # Docker control: the host daemon socket, so `docker compose up/down`, # `docker exec mailserver setup …`, logs, restarts all work from the # shell. Host-root-equivalent — see the SECURITY NOTE above. - /var/run/docker.sock:/var/run/docker.sock networks: [web] # ---- The bastion: SSH front door, forces every session into the jail ---- bastion-vps: image: blaxsoftware/bastion:latest restart: unless-stopped depends_on: [dms-jail] environment: # Every authenticated SSH session becomes an interactive bash shell # inside the jail, in the docker-mailserver directory. `-it` allocates a # TTY (this is an interactive shell → use SSH, not the HTTP path). FORCE_COMMAND: "docker exec -it -w /srv/docker-mailserver dms-jail bash" volumes: # The bastion needs the socket too — only to `docker exec` into the jail. - /var/run/docker.sock:/var/run/docker.sock # Authorized clients — drop one *.pub per identity. Read live, no restart. - ./docker-data/bastion/users.d:/etc/bastion/users.d # Persist the 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 ports: # SSH on host port 2222. Bind to 127.0.0.1 and front with a tunnel/VPN # if you don't want it on the public internet. - "2222:22" networks: [web] networks: web: external: true