# ===========================================================================
# docker-bastion — Minimal SSH bastion for ForceCommand routing
#
# Public sshd → key-only auth → runs ONE preconfigured command (set via
# $FORCE_COMMAND). The bastion user's login shell is /sbin/nologin, so
# there is no fallback shell even if ForceCommand somehow fails to fire.
#
# Build args:
#   ALPINE_VERSION — Alpine base version (default: 3.21)
# ===========================================================================
ARG ALPINE_VERSION=3.21
FROM alpine:${ALPINE_VERSION}

LABEL maintainer="docker-bastion"
LABEL description="Minimal SSH bastion: public ssh → ForceCommand → docker exec (or anything else)"

ENV TZ=UTC
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

# ---------------------------------------------------------------------------
# Packages
#   openssh-server / openssh-keygen — the daemon + ssh-keygen for host keys
#   docker-cli / docker-cli-compose — so FORCE_COMMAND can target containers
#   tini                             — proper PID 1 / signal handling
#   bash                             — startup script + interactive sessions
# ---------------------------------------------------------------------------
RUN apk add --no-cache \
        openssh-server \
        openssh-keygen \
        docker-cli \
        docker-cli-compose \
        busybox-extras \
        bash \
        tini \
        ca-certificates \
        tzdata \
        # git so FORCE_COMMAND scripts can run `git pull` / `git tag` as
        # canonical deploy.sh patterns expect.
        git \
        # openssh-client provides the `ssh` binary git uses for
        # `git push origin` over the ssh:// transport. Without it,
        # deploy.sh fails with `error: cannot run ssh: No such file…`.
        openssh-client

# ---------------------------------------------------------------------------
# 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 /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

# ---------------------------------------------------------------------------
# sshd config + entrypoint
#   /etc/ssh/keys/      — host keys (generated on first boot; mount as a
#                          volume to persist them across rebuilds)
#   /etc/bastion/       — runtime-generated ForceCommand wrapper
# ---------------------------------------------------------------------------
COPY config/sshd_config /etc/ssh/sshd_config
COPY scripts/start-container /usr/local/bin/start-container
COPY scripts/bastion-list-keys /usr/local/bin/bastion-list-keys
# AuthorizedKeysCommand requires its script and *every* parent dir to be
# root-owned and not group/world-writable. /usr/local/bin satisfies that
# by default; chmod 0755 is the safe canonical mode for the script itself.
RUN chmod 0755 /usr/local/bin/start-container /usr/local/bin/bastion-list-keys && \
    mkdir -p /etc/bastion /etc/bastion/users.d /etc/ssh/keys /var/empty && \
    chmod 700 /etc/ssh/keys && \
    chmod 755 /etc/bastion/users.d && \
    chmod 711 /var/empty

# ---------------------------------------------------------------------------
# Environment
#   FORCE_COMMAND          — REQUIRED. The single command run on every login.
#                            Shell metacharacters are supported.
#                            Examples:
#                              docker exec -it app bash
#                              docker compose -f /workspace/compose.yml exec app bash
#                              cd /workspace && ./deploy.sh
#   HTTP_TOKEN             — optional. Enables HTTP listener with Bearer
#                            auth: `Authorization: Bearer <HTTP_TOKEN>`.
#   HTTP_BASIC_AUTH        — optional. Enables HTTP listener with Basic
#                            auth. Value is "user:password". Works with
#                            `curl https://user:password@host/…`.
#                            (Either, both, or neither — neither = SSH only.)
#   HTTP_PORT              — HTTP listen port (default 8080).
#   SSH_PORT               — sshd listen port inside the container (default 22).
#   AUTHORIZED_KEYS_DIR    — directory of *.pub files (default
#                            /etc/bastion/users.d). Read LIVE by sshd via
#                            AuthorizedKeysCommand on every auth attempt —
#                            drop a file, the next login picks it up
#                            without restarting the container.
#   AUTHORIZED_KEYS_HOST   — single-file path; merged into authorized_keys
#                            at boot if present (backward-compat / single-file UX).
#   AUTHORIZED_KEYS_REPO   — same, second source for boot-time merge.
#                            (All three sources are additive. Mount any
#                            combination — none = bastion still starts,
#                            but every SSH attempt fails with "publickey
#                            denied" until you drop a key in.)
# ---------------------------------------------------------------------------
ENV FORCE_COMMAND=""
ENV HTTP_TOKEN=""
ENV HTTP_BASIC_AUTH=""
ENV HTTP_PORT=8080
ENV SSH_PORT=22
ENV AUTHORIZED_KEYS_DIR=/etc/bastion/users.d
ENV AUTHORIZED_KEYS_HOST=/etc/bastion/authorized_keys.host
ENV AUTHORIZED_KEYS_REPO=/etc/bastion/authorized_keys.repo

EXPOSE 22 8080

ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/start-container"]
