Commit Graph

10 Commits

Author SHA1 Message Date
Fabian @ Blax Software 964bb394db feat(examples): docker-mailserver "fake VPS" scoped to one directory
A disposable jail container that bind-mounts only one directory; the
bastion's FORCE_COMMAND drops every SSH session into an interactive shell
inside it. The jail's own root fs is throwaway image data, so the only host
data reachable over the session is the mounted directory. Documents the
docker-socket tradeoff and the read-only / no-socket hardened variants.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 13:35:47 +02:00
Fabian @ Blax Software 8eb57e5a77 feat(broker): allowlist-gated command broker mode
Add a second operating mode alongside FORCE_COMMAND: the client supplies
the command and it runs only if it matches a regex allowlist
(ALLOWED_COMMANDS / mounted allowed-commands.list), optionally behind a
trusted COMMAND_PREFIX. Matching is whole-line anchored (grep -Eqx) and
multi-line requests are rejected; execution is shell-free word-split, so
; | & $() are literal args and a sloppy rule can't become injection.

Works over SSH (SSH_ORIGINAL_COMMAND) and HTTP (X-Bastion-Command).
FORCE_COMMAND mode is unchanged and remains the default when no allowlist
is set; config is read from boot-written files since sshd does not pass
the daemon env to a ForceCommand session.

- scripts/bastion-broker: the allowlist gate + no-shell exec
- scripts/start-container: mode detection, broker wrapper + CGI, banner
- Dockerfile / config/sshd_config: wire in the broker, document env vars
- examples/docker-mailserver: ready-to-run broker config + allowlist
2026-06-02 19:32:57 +02:00
Fabian @ Blax Software 3ec02cea7b feat(start-container): forward args + X-Deploy-Bump to FORCE_COMMAND
Back-fills into git what is already live in blaxsoftware/bastion:latest
(the deployed image was built 2026-05-29 from this then-uncommitted working
tree; git HEAD was behind it).

- ForceCommand wrapper forwards positional args: exec sh -c "..." sh "$@"
- CGI maps X-Deploy-Bump: patch|minor|major -> --patch|--minor|--major and
  passes it as one positional arg to the FORCE_COMMAND.

This is the server side of the learn-atc deploy '"$@"' passthrough and the
/<service>/minor-style URL-suffix version bump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-01 12:18:49 +02:00
Fabian @ Blax Software c90206045f I deploy-bastion ergonomics: HTTP_AS_ROOT, auto GIT_SSH_COMMAND, safe.directory, chown soft-fail
Caught while wiring up an HTTP-driven deploy that runs the user's
canonical deploy.sh from a CGI:

- HTTP_AS_ROOT=1 — opt out of busybox httpd's `-u USER` drop so the
  CGI (and the deploy.sh it runs) keep root supplementary groups.
  Required because busybox httpd does setuid/setgid but not
  setgroups; dropping to agent loses the dockerhost group and the
  CGI can't reach /var/run/docker.sock. Bastion already has the
  socket = host root, so this doesn't widen the envelope.
- chown -R …/.ssh — make it best-effort. With ssh creds mounted
  read-only (id_rsa, known_hosts), the chown -R failed under
  `set -e` and killed boot. The dir + the file we wrote are what
  matter; anything bind-mounted in is the caller's business.
- git config --system --add safe.directory '*' — silence
  'detected dubious ownership' when the CGI runs as root over a
  host-uid-owned repo (standard bind-mount-into-bastion case).
- GIT_SSH_COMMAND auto-export — when a key is mounted at
  /home/agent/.ssh/id_rsa, the wrapper sets git's ssh invocation
  to point at it explicitly. Required because setting HOME alone
  doesn't make root-uid ssh follow ~/.ssh/{id_rsa,known_hosts}.
2026-05-29 09:19:09 +02:00
Fabian @ Blax Software 0262f677c1 A users.d/ drop-in directory for live-read authorized keys
sshd now consults /etc/bastion/users.d/*.pub on every authentication
attempt via AuthorizedKeysCommand, so adding or removing a user
takes effect immediately without restarting the container — just
drop `alice.pub` (or any *.pub file) into the host-bound dir,
sshd picks it up on the next login.

Implementation:
- /usr/local/bin/bastion-list-keys: minimal POSIX-sh script that
  cats $AUTHORIZED_KEYS_DIR/*.pub. Runs as the agent user (per
  AuthorizedKeysCommandUser), reads world-readable pubkeys.
- sshd_config: AuthorizedKeysCommand alongside the existing
  AuthorizedKeysFile — both checked, so the boot-merged
  file (AUTHORIZED_KEYS_HOST/_REPO) still works for single-file UX.
- start-container: 'zero key sources' is now a WARN, not a fatal.
  Bastion comes up empty; SSH attempts fail with 'publickey denied'
  until you drop a key. Lets users `docker compose up` first and
  add keys later.

Bug fix on the way through: `grep -c` exits non-zero when no
lines match, which under `set -eu` killed the boot script
silently after '[2/5] Authorized keys...'. Switched to
`awk … | wc -l` which exits 0 cleanly on empty input.

README updated with the new source priority and env var.
2026-05-28 12:31:51 +02:00
Fabian @ Blax Software 58492c80ec A HTTP basic auth + git/ssh tooling for deploy.sh-shape FORCE_COMMANDs
HTTP basic auth via HTTP_BASIC_AUTH=user:password. Mutually
exclusive with HTTP_TOKEN (basic takes precedence if both set).

Implementation note: busybox httpd strips Authorization: Basic
headers before invoking CGI scripts (it expects to handle basic
auth itself), so a CGI-side check for Basic doesn't work. We let
busybox handle Basic via its -c httpd.conf rule (`/cgi-bin/:user:pass`)
and keep the CGI-side Bearer check for HTTP_TOKEN. httpd.conf is
chowned to the agent user because httpd drops privileges before
reading -c.

Image additions for canonical deploy.sh patterns:
- git (apk add git) — for git pull/tag/push.
- openssh-client (apk add openssh-client) — provides /usr/bin/ssh,
  which git invokes for ssh:// remote transports. Without it
  `git push origin` fails with 'error: cannot run ssh: No such
  file or directory'.
- HOME=/home/agent exported in the force-command wrapper — busybox
  httpd doesn't set HOME for CGI, leaving git/ssh/xdg lookups
  pointing at /root and producing 'Permission denied' warnings.

README updated with HTTP_BASIC_AUTH env var, URL syntax examples,
and the mutual-exclusion note.
2026-05-28 12:11:20 +02:00
Fabian @ Blax Software 8c5b89b5af I unlock agent account + use /bin/sh shell so ForceCommand actually fires
Two OpenSSH-on-alpine quirks caught by a real ssh attempt:

1) alpine's `adduser -D` leaves shadow password as `!`, which
   OpenSSH 9.x treats as 'account locked' and refuses even for
   pubkey auth (logs: 'User agent not allowed because account is
   locked'). Sed-replace `!` with `*` post-create — no password
   set, but account NOT locked.

2) Setting the login shell to /sbin/nologin defeats ForceCommand,
   because sshd executes the forced command as
   `<login-shell> -c "<command>"`. nologin then prints
   'This account is not available' and exits. Use /bin/sh instead;
   the security boundary is ForceCommand + sshd_config, not the
   shell — clients cannot bypass ForceCommand to ask for an
   interactive shell.

README security section updated to reflect both points.
2026-05-28 11:35:54 +02:00
Fabian @ Blax Software 74b3983ff4 A optional HTTP listener + polished README
- HTTP path: opt-in via $HTTP_TOKEN; busybox httpd binds $HTTP_PORT
  (default 8080) and serves /cgi-bin/run, which validates the
  'Authorization: Bearer …' header and exec's the same force-command
  wrapper SSH uses. Output streams chunked. GET and POST both work.
  Without HTTP_TOKEN the bastion stays SSH-only.

- README rewritten with shields.io badges, two complete quickstart
  examples (WordPress drop-in + nginx config-reload webhook), inline
  comments on every yaml line marking required/optional, traefik
  integration in both examples, and star-history footer matching
  Blax OSS convention.

- Dockerfile: add busybox-extras (the httpd applet was split out of
  the core busybox binary in alpine 3.21); EXPOSE 8080; document
  HTTP_TOKEN/HTTP_PORT env vars.
2026-05-28 11:25:34 +02:00
Fabian @ Blax Software a9e02398eb I README: bind mounts under ./docker-data/, no named volumes
Named volumes get wiped by 'docker compose down -v' — that command
is in too many people's muscle memory for ssh host keys to live
behind it. Bind-mount /etc/ssh/keys to ./docker-data/bastion-*/keys
instead, matching the laravel-workkit §6 convention.
2026-05-28 10:56:37 +02:00
Fabian @ Blax Software 86b8966130 A initial docker-bastion image
Minimal SSH bastion (alpine + openssh-server + docker-cli) that
authenticates by key and runs exactly one preconfigured command
(FORCE_COMMAND) per session. authorized_keys can be merged from
both a host-mounted source and a repo-mounted source. Host keys
persist via /etc/ssh/keys volume; docker socket group membership
is aligned at boot.
2026-05-28 10:50:06 +02:00