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.
This commit is contained in:
commit
86b8966130
|
|
@ -0,0 +1,11 @@
|
||||||
|
.git
|
||||||
|
.github
|
||||||
|
.gitignore
|
||||||
|
*.md
|
||||||
|
!README.md
|
||||||
|
LICENSE
|
||||||
|
docker-bake.hcl
|
||||||
|
docker-compose*.yml
|
||||||
|
.env*
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
|
@ -0,0 +1,82 @@
|
||||||
|
# ===========================================================================
|
||||||
|
# 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 \
|
||||||
|
bash \
|
||||||
|
tini \
|
||||||
|
ca-certificates \
|
||||||
|
tzdata
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Bastion user — UID/GID 1000, /sbin/nologin shell (ForceCommand is the only path)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
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 && \
|
||||||
|
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
|
||||||
|
RUN chmod 0755 /usr/local/bin/start-container && \
|
||||||
|
mkdir -p /etc/bastion /etc/ssh/keys /var/empty && \
|
||||||
|
chmod 700 /etc/ssh/keys && \
|
||||||
|
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
|
||||||
|
# AUTHORIZED_KEYS_HOST — file path; merged into authorized_keys if present
|
||||||
|
# AUTHORIZED_KEYS_REPO — file path; merged into authorized_keys if present
|
||||||
|
# (mount either, both, or neither — at least one
|
||||||
|
# must exist or the container refuses to start)
|
||||||
|
# SSH_PORT — sshd listen port inside the container (default 22)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
ENV FORCE_COMMAND=""
|
||||||
|
ENV AUTHORIZED_KEYS_HOST=/etc/bastion/authorized_keys.host
|
||||||
|
ENV AUTHORIZED_KEYS_REPO=/etc/bastion/authorized_keys.repo
|
||||||
|
ENV SSH_PORT=22
|
||||||
|
|
||||||
|
EXPOSE 22
|
||||||
|
|
||||||
|
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/start-container"]
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
[](https://github.com/blax-software)
|
||||||
|
|
||||||
|
# docker-bastion
|
||||||
|
|
||||||
|
A **minimal SSH bastion** that authenticates by key and runs exactly **one preconfigured command** on every login. No fallback shell, no interactive choice — the agent connecting via SSH gets dropped straight into whatever you point `FORCE_COMMAND` at.
|
||||||
|
|
||||||
|
Typical uses:
|
||||||
|
|
||||||
|
- Give an agent (or a human) an SSH-shaped door into a running container — connection lands inside `docker exec -it app bash` and behaves like SSH'ing into the app.
|
||||||
|
- Give a deploy bot an SSH-shaped door that runs `./deploy.sh` on connect, streams output, and disconnects when the script exits.
|
||||||
|
|
||||||
|
Same image, different `FORCE_COMMAND` per service.
|
||||||
|
|
||||||
|
## Image
|
||||||
|
|
||||||
|
| Tag | Base | Size (est.) |
|
||||||
|
|------------------------------|------------|-------------|
|
||||||
|
| `blaxsoftware/bastion:latest`| alpine 3.21 | ~65 MB |
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
ssh-app:
|
||||||
|
image: blaxsoftware/bastion:latest
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
|
environment:
|
||||||
|
FORCE_COMMAND: "docker exec -it learnatc-app-1 bash"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- ~/.ssh/authorized_keys:/etc/bastion/authorized_keys.host:ro
|
||||||
|
- ./docker/bastion/authorized_keys:/etc/bastion/authorized_keys.repo:ro
|
||||||
|
- bastion-keys:/etc/ssh/keys
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
ssh-deploy:
|
||||||
|
image: blaxsoftware/bastion:latest
|
||||||
|
ports:
|
||||||
|
- "2223:22"
|
||||||
|
environment:
|
||||||
|
FORCE_COMMAND: "cd /workspace && ./deploy.sh"
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- .:/workspace:ro
|
||||||
|
- ~/.ssh/authorized_keys:/etc/bastion/authorized_keys.host:ro
|
||||||
|
- bastion-keys-deploy:/etc/ssh/keys
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bastion-keys:
|
||||||
|
bastion-keys-deploy:
|
||||||
|
```
|
||||||
|
|
||||||
|
Then from the client:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -p 2222 agent@your-host # → drops into bash inside the app container
|
||||||
|
ssh -p 2223 agent@your-host # → streams ./deploy.sh, disconnects on exit
|
||||||
|
```
|
||||||
|
|
||||||
|
## Authorized Keys — two sources, merged
|
||||||
|
|
||||||
|
At boot the entrypoint concatenates whichever of these files exist into the agent's `authorized_keys`. **At least one must exist** or the container refuses to start.
|
||||||
|
|
||||||
|
| File | Typical mount |
|
||||||
|
|----------------------------------------------|-------------------------------------------------|
|
||||||
|
| `/etc/bastion/authorized_keys.host` | `~/.ssh/authorized_keys` from the docker host |
|
||||||
|
| `/etc/bastion/authorized_keys.repo` | `./docker/bastion/authorized_keys` in the repo |
|
||||||
|
|
||||||
|
Mount either, both, or neither — but neither = container exits at startup with a clear error.
|
||||||
|
|
||||||
|
## How `FORCE_COMMAND` behaves
|
||||||
|
|
||||||
|
`FORCE_COMMAND` is run via `sh -c`, so shell metacharacters work — `&&`, `||`, pipes, `cd`, redirects.
|
||||||
|
|
||||||
|
- **Interactive command** (`docker exec -it app bash`) — SSH allocates a PTY by default for `ssh user@host`, `docker exec -it` inherits it, the inner bash is interactive. Session ends when the user types `exit`.
|
||||||
|
- **Script command** (`cd /workspace && ./deploy.sh`) — output streams back over SSH, the session closes the moment the script exits. Exit code propagates to the SSH client.
|
||||||
|
|
||||||
|
The client cannot override the command. `SSH_ORIGINAL_COMMAND` is ignored.
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Default | Description |
|
||||||
|
|-------------------------|------------------------------------------|----------------------------------------------------------------------------|
|
||||||
|
| `FORCE_COMMAND` | *(required)* | The command run on every authenticated session. Shell metacharacters OK. |
|
||||||
|
| `AUTHORIZED_KEYS_HOST` | `/etc/bastion/authorized_keys.host` | Path to the host-sourced authorized_keys (mount it here). |
|
||||||
|
| `AUTHORIZED_KEYS_REPO` | `/etc/bastion/authorized_keys.repo` | Path to the repo-sourced authorized_keys (mount it here). |
|
||||||
|
| `SSH_PORT` | `22` | Port sshd listens on inside the container. |
|
||||||
|
|
||||||
|
## Build Args
|
||||||
|
|
||||||
|
| Arg | Default | Description |
|
||||||
|
|------------------|---------|-----------------------------------|
|
||||||
|
| `ALPINE_VERSION` | `3.21` | Alpine base image tag. |
|
||||||
|
| `SSH_UID` | `1000` | UID of the bastion `agent` user. |
|
||||||
|
| `SSH_GID` | `1000` | GID of the bastion `agent` group. |
|
||||||
|
|
||||||
|
## What's Inside
|
||||||
|
|
||||||
|
- **openssh-server** — hardened config: key-only auth, no forwarding, no PAM, no user env, `/sbin/nologin` shell, global `ForceCommand`.
|
||||||
|
- **docker-cli** + **docker-cli-compose** — so `FORCE_COMMAND` can target containers via a mounted docker socket. Group membership is auto-aligned at boot to the host socket's GID.
|
||||||
|
- **tini** — PID 1, signal handling.
|
||||||
|
- **bash**, **ca-certificates**, **tzdata**.
|
||||||
|
|
||||||
|
## Security Model
|
||||||
|
|
||||||
|
The security boundary is **the authorized_keys file and the ForceCommand wrapper**. Once a key authenticates, the session is hard-pinned to exactly one command. The bastion has the docker socket — equivalent to host root — so the only thing standing between a remote attacker and host root is sshd + your key hygiene.
|
||||||
|
|
||||||
|
Practical checklist:
|
||||||
|
|
||||||
|
1. **Key-only auth, no passwords** — enforced in `sshd_config`.
|
||||||
|
2. **No agent / tcp / x11 forwarding, no port tunnels** — enforced in `sshd_config`.
|
||||||
|
3. **Login shell is `/sbin/nologin`** — no fallback if `ForceCommand` somehow misfires.
|
||||||
|
4. **`PermitUserEnvironment no`, `PermitUserRC no`** — clients cannot inject env or rc files.
|
||||||
|
5. **Bind the host port to `127.0.0.1` or behind a firewall / VPN unless you actually need it public.**
|
||||||
|
6. **Keep openssh patched** — `apk upgrade` in a rebuild cycle; an unauth sshd RCE here would mean host root.
|
||||||
|
7. **Lock down siblings** — anyone who can `docker exec` into the app can also `docker exec` into the mysql/redis container via the same socket. `cap_drop: [ALL]` and `no-new-privileges` on siblings caps the blast radius.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
start-container (entrypoint)
|
||||||
|
├─ generate host keys (idempotent, persisted via /etc/ssh/keys volume)
|
||||||
|
├─ merge AUTHORIZED_KEYS_HOST + AUTHORIZED_KEYS_REPO into authorized_keys
|
||||||
|
├─ write /etc/bastion/force-command wrapper from $FORCE_COMMAND
|
||||||
|
├─ align docker socket group membership (if socket is mounted)
|
||||||
|
└─ exec sshd -D -e
|
||||||
|
|
||||||
|
ssh client
|
||||||
|
└─ key auth as `agent`
|
||||||
|
└─ ForceCommand /etc/bastion/force-command
|
||||||
|
└─ exec sh -c "$FORCE_COMMAND"
|
||||||
|
```
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
# ===========================================================================
|
||||||
|
# docker-bastion — hardened sshd config
|
||||||
|
#
|
||||||
|
# Every authenticated session is routed through /etc/bastion/force-command,
|
||||||
|
# which is generated at container start from $FORCE_COMMAND. The bastion
|
||||||
|
# user has /sbin/nologin as its shell so there is no fallback if the
|
||||||
|
# ForceCommand wrapper is missing or fails — the session simply ends.
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
Port 22
|
||||||
|
AddressFamily any
|
||||||
|
ListenAddress 0.0.0.0
|
||||||
|
ListenAddress ::
|
||||||
|
|
||||||
|
# Host keys live in /etc/ssh/keys/ so they can survive image rebuilds via
|
||||||
|
# a named volume. start-container generates them on first boot if missing.
|
||||||
|
HostKey /etc/ssh/keys/ssh_host_ed25519_key
|
||||||
|
HostKey /etc/ssh/keys/ssh_host_rsa_key
|
||||||
|
|
||||||
|
# Privilege separation directory (Alpine default is /var/empty).
|
||||||
|
StrictModes yes
|
||||||
|
|
||||||
|
# Auth — public keys only, no passwords, no interactive prompts.
|
||||||
|
# (UsePAM is omitted: Alpine's openssh-server is built without PAM support
|
||||||
|
# and rejects the directive entirely. Without PAM compiled in, the no-PAM
|
||||||
|
# behavior is already the default — nothing to disable.)
|
||||||
|
PermitRootLogin no
|
||||||
|
PasswordAuthentication no
|
||||||
|
KbdInteractiveAuthentication no
|
||||||
|
PubkeyAuthentication yes
|
||||||
|
AuthorizedKeysFile /home/%u/.ssh/authorized_keys
|
||||||
|
|
||||||
|
# Surface reduction — no forwarding, no tunnels, no user env / rc files.
|
||||||
|
AllowAgentForwarding no
|
||||||
|
AllowTcpForwarding no
|
||||||
|
X11Forwarding no
|
||||||
|
GatewayPorts no
|
||||||
|
PermitTunnel no
|
||||||
|
PermitUserEnvironment no
|
||||||
|
PermitUserRC no
|
||||||
|
|
||||||
|
# Logging.
|
||||||
|
LogLevel VERBOSE
|
||||||
|
SyslogFacility AUTH
|
||||||
|
|
||||||
|
# Belt + suspenders: applies even if per-key command="" is missing.
|
||||||
|
ForceCommand /etc/bastion/force-command
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
# ===========================================================================
|
||||||
|
# docker-bastion — multi-platform build
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker buildx bake
|
||||||
|
# docker buildx bake --set "*.platform=linux/amd64,linux/arm64"
|
||||||
|
#
|
||||||
|
# Override registry / name:
|
||||||
|
# REGISTRY=ghcr.io/myorg IMAGE_NAME=docker-bastion docker buildx bake
|
||||||
|
# ===========================================================================
|
||||||
|
|
||||||
|
variable "REGISTRY" {
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "IMAGE_NAME" {
|
||||||
|
default = "docker-bastion"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "ALPINE_VERSION" {
|
||||||
|
default = "3.21"
|
||||||
|
}
|
||||||
|
|
||||||
|
function "tag" {
|
||||||
|
params = [name]
|
||||||
|
result = REGISTRY != "" ? "${REGISTRY}/${IMAGE_NAME}:${name}" : "${IMAGE_NAME}:${name}"
|
||||||
|
}
|
||||||
|
|
||||||
|
target "default" {
|
||||||
|
context = "."
|
||||||
|
dockerfile = "Dockerfile"
|
||||||
|
args = {
|
||||||
|
ALPINE_VERSION = "${ALPINE_VERSION}"
|
||||||
|
}
|
||||||
|
platforms = ["linux/amd64", "linux/arm64"]
|
||||||
|
tags = [tag("latest"), tag("alpine${ALPINE_VERSION}")]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,149 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " docker-bastion starting"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Date: $(date)"
|
||||||
|
echo "Hostname: $(hostname)"
|
||||||
|
echo "OpenSSH: $(/usr/sbin/sshd -V 2>&1 | head -1 || echo n/a)"
|
||||||
|
echo "Docker CLI: $(docker --version 2>/dev/null || echo n/a)"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
SSH_USER="agent"
|
||||||
|
SSH_PORT="${SSH_PORT:-22}"
|
||||||
|
FORCE_COMMAND_VALUE="${FORCE_COMMAND:-}"
|
||||||
|
AUTHORIZED_KEYS_HOST="${AUTHORIZED_KEYS_HOST:-/etc/bastion/authorized_keys.host}"
|
||||||
|
AUTHORIZED_KEYS_REPO="${AUTHORIZED_KEYS_REPO:-/etc/bastion/authorized_keys.repo}"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 1) Validate config
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ -z "$FORCE_COMMAND_VALUE" ]; then
|
||||||
|
echo "FATAL: FORCE_COMMAND must be set."
|
||||||
|
echo " e.g. FORCE_COMMAND='docker exec -it app bash'"
|
||||||
|
echo " or FORCE_COMMAND='cd /workspace && ./deploy.sh'"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 2) Host keys — generate on first boot, persist via /etc/ssh/keys volume
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[1/5] Host keys..."
|
||||||
|
mkdir -p /etc/ssh/keys
|
||||||
|
chmod 700 /etc/ssh/keys
|
||||||
|
for keytype in ed25519 rsa; do
|
||||||
|
keyfile="/etc/ssh/keys/ssh_host_${keytype}_key"
|
||||||
|
if [ ! -f "$keyfile" ]; then
|
||||||
|
echo " Generating new $keytype host key"
|
||||||
|
ssh-keygen -t "$keytype" -f "$keyfile" -N "" -q
|
||||||
|
else
|
||||||
|
echo " Reusing existing $keytype host key"
|
||||||
|
fi
|
||||||
|
chmod 600 "$keyfile"
|
||||||
|
[ -f "${keyfile}.pub" ] && chmod 644 "${keyfile}.pub"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 3) Merge authorized_keys sources
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[2/5] Authorized keys..."
|
||||||
|
AUTH_FILE="/home/${SSH_USER}/.ssh/authorized_keys"
|
||||||
|
mkdir -p "$(dirname "$AUTH_FILE")"
|
||||||
|
: > "$AUTH_FILE"
|
||||||
|
|
||||||
|
added=0
|
||||||
|
for src in "$AUTHORIZED_KEYS_HOST" "$AUTHORIZED_KEYS_REPO"; do
|
||||||
|
if [ -f "$src" ]; then
|
||||||
|
echo " + Merging $src"
|
||||||
|
cat "$src" >> "$AUTH_FILE"
|
||||||
|
# Force newline between sources (final file may not end with one).
|
||||||
|
printf '\n' >> "$AUTH_FILE"
|
||||||
|
added=$((added + 1))
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$added" -eq 0 ]; then
|
||||||
|
echo "FATAL: no authorized_keys source found."
|
||||||
|
echo " Mount at least one of:"
|
||||||
|
echo " $AUTHORIZED_KEYS_HOST (typically ~/.ssh/authorized_keys from host)"
|
||||||
|
echo " $AUTHORIZED_KEYS_REPO (typically ./docker/bastion/authorized_keys)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
key_count=$(grep -cvE '^[[:space:]]*(#|$)' "$AUTH_FILE" 2>/dev/null || echo 0)
|
||||||
|
echo " Loaded $key_count authorized key(s) from $added source(s)"
|
||||||
|
|
||||||
|
chown -R "${SSH_USER}:${SSH_USER}" "$(dirname "$AUTH_FILE")"
|
||||||
|
chmod 700 "$(dirname "$AUTH_FILE")"
|
||||||
|
chmod 600 "$AUTH_FILE"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 4) ForceCommand wrapper
|
||||||
|
#
|
||||||
|
# Write the configured command to a plain file, then a small wrapper that
|
||||||
|
# exec's `sh -c "$(cat ...)"`. This way:
|
||||||
|
# - Shell metacharacters in $FORCE_COMMAND work (&&, |, redirects, cd).
|
||||||
|
# - The wrapper itself stays static (no escaping of user input into a
|
||||||
|
# heredoc), and the command file is read at session start so changes
|
||||||
|
# to $FORCE_COMMAND only need a container restart, not a rebuild.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[3/5] ForceCommand..."
|
||||||
|
mkdir -p /etc/bastion
|
||||||
|
printf '%s\n' "$FORCE_COMMAND_VALUE" > /etc/bastion/force-command.cmd
|
||||||
|
chmod 0644 /etc/bastion/force-command.cmd
|
||||||
|
|
||||||
|
cat > /etc/bastion/force-command <<'WRAPPER'
|
||||||
|
#!/bin/sh
|
||||||
|
# Auto-generated by docker-bastion start-container.
|
||||||
|
# sshd invokes this script for every authenticated session.
|
||||||
|
# SSH_ORIGINAL_COMMAND is intentionally ignored — clients cannot override.
|
||||||
|
exec sh -c "$(cat /etc/bastion/force-command.cmd)"
|
||||||
|
WRAPPER
|
||||||
|
chmod 0755 /etc/bastion/force-command
|
||||||
|
echo " $FORCE_COMMAND_VALUE"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 5) Docker socket — if mounted, align group membership so the agent user
|
||||||
|
# can talk to dockerd without --privileged.
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[4/5] Docker socket..."
|
||||||
|
if [ -S /var/run/docker.sock ]; then
|
||||||
|
sock_gid=$(stat -c '%g' /var/run/docker.sock)
|
||||||
|
echo " Socket present, host gid=$sock_gid"
|
||||||
|
|
||||||
|
grp_name=$(getent group "$sock_gid" | cut -d: -f1 || true)
|
||||||
|
if [ -z "$grp_name" ]; then
|
||||||
|
grp_name="dockerhost"
|
||||||
|
addgroup -g "$sock_gid" "$grp_name" 2>/dev/null || true
|
||||||
|
echo " Created group $grp_name (gid=$sock_gid)"
|
||||||
|
else
|
||||||
|
echo " Reusing existing group $grp_name (gid=$sock_gid)"
|
||||||
|
fi
|
||||||
|
addgroup "$SSH_USER" "$grp_name" 2>/dev/null || true
|
||||||
|
echo " Added $SSH_USER to $grp_name"
|
||||||
|
else
|
||||||
|
echo " WARN: /var/run/docker.sock not mounted — docker-based FORCE_COMMAND will fail."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 6) Adjust sshd_config port if non-default
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
if [ "$SSH_PORT" != "22" ]; then
|
||||||
|
sed -i "s/^Port 22\$/Port ${SSH_PORT}/" /etc/ssh/sshd_config
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 7) sshd config sanity check + launch
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
echo "[5/5] sshd config check..."
|
||||||
|
/usr/sbin/sshd -t -f /etc/ssh/sshd_config
|
||||||
|
|
||||||
|
echo "=========================================="
|
||||||
|
echo " Listening on port ${SSH_PORT}"
|
||||||
|
echo " User: ${SSH_USER}"
|
||||||
|
echo " ForceCommand: ${FORCE_COMMAND_VALUE}"
|
||||||
|
echo "=========================================="
|
||||||
|
|
||||||
|
# -D = foreground, -e = log to stderr (so docker logs picks it up).
|
||||||
|
exec /usr/sbin/sshd -D -e
|
||||||
Loading…
Reference in New Issue