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.
This commit is contained in:
parent
74b3983ff4
commit
8c5b89b5af
16
Dockerfile
16
Dockerfile
|
|
@ -36,12 +36,24 @@ RUN apk add --no-cache \
|
|||
tzdata
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Bastion user — UID/GID 1000, /sbin/nologin shell (ForceCommand is the only path)
|
||||
# Bastion user — UID/GID 1000. Login shell = /bin/sh.
|
||||
#
|
||||
# Why not /sbin/nologin?
|
||||
# sshd's ForceCommand wraps the command in `<user-shell> -c "..."`. With
|
||||
# nologin as the shell, every ForceCommand invocation just prints
|
||||
# "This account is not available" and exits — defeating the whole point.
|
||||
# Security comes from ForceCommand + sshd_config, not from the shell;
|
||||
# clients cannot bypass ForceCommand to ask for an interactive shell.
|
||||
# ---------------------------------------------------------------------------
|
||||
ARG SSH_UID=1000
|
||||
ARG SSH_GID=1000
|
||||
RUN addgroup -g ${SSH_GID} agent && \
|
||||
adduser -D -u ${SSH_UID} -G agent -s /sbin/nologin agent && \
|
||||
adduser -D -u ${SSH_UID} -G agent -s /bin/sh agent && \
|
||||
# Alpine's `adduser -D` leaves the shadow password field as `!`, which
|
||||
# OpenSSH 9.x interprets as "account locked" and refuses even for pubkey
|
||||
# auth (logs: "User agent not allowed because account is locked").
|
||||
# Replace with `*` — no password set, but account NOT locked.
|
||||
sed -i 's/^agent:!:/agent:*:/' /etc/shadow && \
|
||||
mkdir -p /home/agent/.ssh && \
|
||||
chown -R agent:agent /home/agent/.ssh && \
|
||||
chmod 700 /home/agent/.ssh
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ Mount one, both, or neither — though neither = startup failure.
|
|||
|
||||
## What's inside
|
||||
|
||||
- **openssh-server** — hardened config: key-only auth, no forwarding, no PAM, no user env, `/sbin/nologin` login shell, global `ForceCommand` directive.
|
||||
- **openssh-server** — hardened config: key-only auth, no forwarding, no PAM, no user env, global `ForceCommand` directive (clients cannot bypass).
|
||||
- **busybox httpd** (busybox-extras) — minimal HTTP listener for the URL path; CGI-driven; only starts when `HTTP_TOKEN` is set.
|
||||
- **docker-cli + docker-cli-compose** — so `FORCE_COMMAND` can target containers through a mounted docker socket. Group membership is auto-aligned to the host socket's GID at boot.
|
||||
- **tini** — PID 1, signal handling, zombie reaping.
|
||||
|
|
@ -217,7 +217,7 @@ Practical checklist:
|
|||
1. **Key-only SSH, no passwords** — enforced in `sshd_config`.
|
||||
2. **Token-only HTTP** — no path is open without `Authorization: Bearer`.
|
||||
3. **No agent / TCP / X11 forwarding, no port tunnels** — enforced in `sshd_config`.
|
||||
4. **Login shell is `/sbin/nologin`** — no fallback if `ForceCommand` somehow misfires.
|
||||
4. **ForceCommand cannot be bypassed.** Clients can request any command (`ssh user@host arbitrary-thing`); sshd ignores it and runs `/etc/bastion/force-command`. `SSH_ORIGINAL_COMMAND` is dropped. The bastion user's login shell is `/bin/sh` (not `nologin` — that would break ForceCommand itself, since sshd invokes the user's shell as `shell -c "<forced-command>"`), but it has no path to anything other than the wrapper.
|
||||
5. **`PermitUserEnvironment no`, `PermitUserRC no`** — clients cannot inject env vars or rc files.
|
||||
6. **Bind host ports to `127.0.0.1` or hide them behind traefik+TLS unless you genuinely need them publicly open on raw TCP.** The traefik path with `entrypoints: websecure` and `tls: true` is the recommended public exposure.
|
||||
7. **Rotate `HTTP_TOKEN` regularly.** Generate with `openssl rand -hex 32`, store in `.env`, never commit.
|
||||
|
|
|
|||
Loading…
Reference in New Issue