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.
This commit is contained in:
parent
a9e02398eb
commit
74b3983ff4
14
Dockerfile
14
Dockerfile
|
|
@ -29,6 +29,7 @@ RUN apk add --no-cache \
|
|||
openssh-keygen \
|
||||
docker-cli \
|
||||
docker-cli-compose \
|
||||
busybox-extras \
|
||||
bash \
|
||||
tini \
|
||||
ca-certificates \
|
||||
|
|
@ -66,17 +67,24 @@ RUN chmod 0755 /usr/local/bin/start-container && \
|
|||
# docker exec -it app bash
|
||||
# docker compose -f /workspace/compose.yml exec app bash
|
||||
# cd /workspace && ./deploy.sh
|
||||
# HTTP_TOKEN — optional. When set, enables an HTTP listener on
|
||||
# $HTTP_PORT serving /cgi-bin/run. Clients must
|
||||
# send `Authorization: Bearer <HTTP_TOKEN>`.
|
||||
# Leave unset for SSH-only mode.
|
||||
# HTTP_PORT — HTTP listen port (default 8080).
|
||||
# SSH_PORT — sshd listen port inside the container (default 22).
|
||||
# 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 HTTP_TOKEN=""
|
||||
ENV HTTP_PORT=8080
|
||||
ENV SSH_PORT=22
|
||||
ENV AUTHORIZED_KEYS_HOST=/etc/bastion/authorized_keys.host
|
||||
ENV AUTHORIZED_KEYS_REPO=/etc/bastion/authorized_keys.repo
|
||||
ENV SSH_PORT=22
|
||||
|
||||
EXPOSE 22
|
||||
EXPOSE 22 8080
|
||||
|
||||
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/start-container"]
|
||||
|
|
|
|||
274
README.md
274
README.md
|
|
@ -2,94 +2,195 @@
|
|||
|
||||
# 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.
|
||||
[](https://alpinelinux.org)
|
||||
[](https://www.openssh.com)
|
||||
[](https://docs.docker.com/engine/reference/commandline/cli/)
|
||||
[](#whats-inside)
|
||||
[](#license)
|
||||
|
||||
Typical uses:
|
||||
A **minimal SSH + HTTP bastion** for routing one preconfigured command per authenticated session. Authenticate by SSH key or HTTP bearer token, the container runs whatever you point `FORCE_COMMAND` at — `docker exec` into a sibling container, `./deploy.sh`, `nginx -s reload`, anything — and streams the output back.
|
||||
|
||||
- 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.
|
||||
**Why it exists:** giving an agent or CI bot `docker exec` access usually means handing them the docker socket and trusting their entire toolchain not to misbehave. A bastion with a hard-coded `FORCE_COMMAND` is the inverse: the credential authorizes *one specific thing*, the surface is sshd + busybox httpd, and the same image works for a dozen different roles by varying `FORCE_COMMAND`.
|
||||
|
||||
Same image, different `FORCE_COMMAND` per service.
|
||||
## Available Tags
|
||||
|
||||
## Image
|
||||
| Tag | Base | Notes |
|
||||
|----------------------------------|--------------|--------------------------------------|
|
||||
| `blaxsoftware/bastion:latest` | alpine 3.21 | Default tag, follows alpine releases |
|
||||
| `blaxsoftware/bastion:alpine3.21`| alpine 3.21 | Pinned alpine version |
|
||||
|
||||
| Tag | Base | Size (est.) |
|
||||
|------------------------------|------------|-------------|
|
||||
| `blaxsoftware/bastion:latest`| alpine 3.21 | ~65 MB |
|
||||
## Quick Start — drop into a WordPress container
|
||||
|
||||
## Quick Start
|
||||
The most common use: give a deploy agent SSH-shaped access *into* a running WordPress container. Every session lands inside the `wordpress-app` container's bash; clients can run WP-CLI commands, edit config, debug — same UX as `ssh user@host` against a VPS, but scoped to one container.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
ssh-app:
|
||||
# The bastion. SSH on 2222, HTTP behind traefik on https://deploy-wp.example.com.
|
||||
bastion:
|
||||
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
|
||||
- ./docker-data/bastion-app/keys:/etc/ssh/keys
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# REQUIRED — the single command that runs on every authenticated session.
|
||||
# Shell metacharacters work: &&, ||, pipes, cd, redirects.
|
||||
FORCE_COMMAND: "docker exec -it wordpress-app bash"
|
||||
|
||||
# OPTIONAL — enables the HTTP endpoint at /cgi-bin/run.
|
||||
# Without HTTP_TOKEN set, the bastion is SSH-only.
|
||||
# Generate with: openssl rand -hex 32
|
||||
HTTP_TOKEN: "${BASTION_HTTP_TOKEN}"
|
||||
|
||||
ssh-deploy:
|
||||
image: blaxsoftware/bastion:latest
|
||||
ports:
|
||||
- "2223:22"
|
||||
environment:
|
||||
FORCE_COMMAND: "cd /workspace && ./deploy.sh"
|
||||
volumes:
|
||||
# REQUIRED when FORCE_COMMAND talks to docker (docker exec, docker compose, etc).
|
||||
# Mounts the host's daemon socket so docker-cli inside the bastion reaches it.
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- .:/workspace:ro
|
||||
|
||||
# OPTIONAL — host-sourced authorized_keys. Your laptop's keys, or anything
|
||||
# outside the repo. Read-only mount.
|
||||
- ~/.ssh/authorized_keys:/etc/bastion/authorized_keys.host:ro
|
||||
- ./docker-data/bastion-deploy/keys:/etc/ssh/keys
|
||||
restart: unless-stopped
|
||||
|
||||
# OPTIONAL — repo-sourced authorized_keys. CI / deploy-bot keys committed
|
||||
# alongside the project. At least one of these two must exist or the
|
||||
# container refuses to start.
|
||||
- ./docker/bastion/authorized_keys:/etc/bastion/authorized_keys.repo:ro
|
||||
|
||||
# REQUIRED — host keys persist across rebuilds. Bind mount, NEVER a named
|
||||
# volume; `docker compose down -v` would wipe a named volume and clients
|
||||
# would see "REMOTE HOST IDENTIFICATION HAS CHANGED" after every redeploy.
|
||||
- ./docker-data/bastion/keys:/etc/ssh/keys
|
||||
|
||||
ports:
|
||||
# OPTIONAL — expose SSH on the host directly. Skip this entirely if you
|
||||
# only want the HTTP path through traefik.
|
||||
- "2222:22"
|
||||
|
||||
labels:
|
||||
# OPTIONAL — traefik HTTP route. Visit https://deploy-wp.example.com/cgi-bin/run
|
||||
# with `Authorization: Bearer $BASTION_HTTP_TOKEN` to invoke FORCE_COMMAND.
|
||||
# Remove these labels if you don't want the HTTP path published.
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: "web"
|
||||
traefik.http.routers.bastion.rule: "Host(`deploy-wp.example.com`)"
|
||||
traefik.http.routers.bastion.entrypoints: "websecure"
|
||||
traefik.http.routers.bastion.tls: "true"
|
||||
traefik.http.services.bastion.loadbalancer.server.port: "8080"
|
||||
|
||||
networks: [web]
|
||||
|
||||
# Your actual WordPress container — bastion's FORCE_COMMAND targets it by name.
|
||||
wordpress-app:
|
||||
image: wordpress:latest
|
||||
container_name: wordpress-app
|
||||
environment:
|
||||
WORDPRESS_DB_HOST: db
|
||||
WORDPRESS_DB_NAME: wp
|
||||
WORDPRESS_DB_USER: wp
|
||||
WORDPRESS_DB_PASSWORD: "${DB_PASSWORD}"
|
||||
volumes:
|
||||
- ./docker-data/wordpress:/var/www/html
|
||||
networks: [web]
|
||||
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
```
|
||||
|
||||
> Host keys live in `./docker-data/bastion-*/keys/` as bind mounts — never
|
||||
> named volumes. `docker compose down -v` then can't wipe them, and the
|
||||
> client doesn't see "REMOTE HOST IDENTIFICATION HAS CHANGED" after a
|
||||
> rebuild. Gitignore `docker-data/` in the surrounding repo.
|
||||
|
||||
Then from the client:
|
||||
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
|
||||
# Interactive shell inside the wp container — feels exactly like ssh-into-vps
|
||||
ssh -p 2222 agent@your-host
|
||||
|
||||
# Or trigger from a URL — token-protected, output streams back
|
||||
curl -H "Authorization: Bearer $BASTION_HTTP_TOKEN" \
|
||||
https://deploy-wp.example.com/cgi-bin/run
|
||||
```
|
||||
|
||||
## Authorized Keys — two sources, merged
|
||||
## Quick Start — reload nginx on a webhook
|
||||
|
||||
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.
|
||||
A scoped bastion that does exactly one thing: test the new nginx config and reload if it passes. The HTTP path lets a CI job (GitHub Action, Forgejo runner, anything that can `curl`) trigger a reload after pushing new configs to disk — no SSH keys to provision in CI.
|
||||
|
||||
| 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 |
|
||||
```yaml
|
||||
services:
|
||||
bastion:
|
||||
image: blaxsoftware/bastion:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# `nginx -t` exits non-zero on a syntax error; `&&` short-circuits so a
|
||||
# broken config never gets applied. The exit code propagates back to
|
||||
# the HTTP client (which sees the connection close mid-stream on failure).
|
||||
FORCE_COMMAND: "docker exec nginx-app nginx -t && docker exec nginx-app nginx -s reload"
|
||||
HTTP_TOKEN: "${BASTION_HTTP_TOKEN}"
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
# SSH path stays available for human debugging — same key, same scope.
|
||||
- ~/.ssh/authorized_keys:/etc/bastion/authorized_keys.host:ro
|
||||
- ./docker-data/bastion/keys:/etc/ssh/keys
|
||||
labels:
|
||||
traefik.enable: "true"
|
||||
traefik.docker.network: "web"
|
||||
traefik.http.routers.bastion-nginx.rule: "Host(`reload-nginx.example.com`)"
|
||||
traefik.http.routers.bastion-nginx.entrypoints: "websecure"
|
||||
traefik.http.routers.bastion-nginx.tls: "true"
|
||||
traefik.http.services.bastion-nginx.loadbalancer.server.port: "8080"
|
||||
networks: [web]
|
||||
|
||||
Mount either, both, or neither — but neither = container exits at startup with a clear error.
|
||||
nginx-app:
|
||||
image: nginx:alpine
|
||||
container_name: nginx-app
|
||||
volumes:
|
||||
- ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./conf.d:/etc/nginx/conf.d:ro
|
||||
networks: [web]
|
||||
|
||||
## How `FORCE_COMMAND` behaves
|
||||
networks:
|
||||
web:
|
||||
external: true
|
||||
```
|
||||
|
||||
`FORCE_COMMAND` is run via `sh -c`, so shell metacharacters work — `&&`, `||`, pipes, `cd`, redirects.
|
||||
CI snippet:
|
||||
|
||||
- **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.
|
||||
```bash
|
||||
# After updating nginx.conf on disk:
|
||||
curl --fail-with-body -H "Authorization: Bearer $BASTION_HTTP_TOKEN" \
|
||||
https://reload-nginx.example.com/cgi-bin/run
|
||||
```
|
||||
|
||||
The client cannot override the command. `SSH_ORIGINAL_COMMAND` is ignored.
|
||||
`curl --fail-with-body` makes the CI step fail (non-zero exit) if the bastion returns 4xx/5xx, with the body printed — so a `nginx -t` syntax error in the new config shows up in the CI log without extra wiring.
|
||||
|
||||
## Environment Variables
|
||||
## Two channels, two shapes
|
||||
|
||||
| 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. |
|
||||
| Channel | Best for | TTY? | Streaming? |
|
||||
|---------|--------------------------------|----------|--------------------|
|
||||
| SSH | Interactive (`docker exec -it`) **or** scripts | yes | yes |
|
||||
| HTTP | Scripts only — no TTY | no | yes (chunked / close-delimited) |
|
||||
|
||||
## Build Args
|
||||
Interactive commands (`docker exec -it app bash`) over HTTP fail because there's no TTY — use SSH for those. Both channels stream output line-by-line; both close as soon as `FORCE_COMMAND` exits and the exit code propagates (SSH: to the client; HTTP: nonzero closes the response mid-stream).
|
||||
|
||||
The client cannot override the command. `SSH_ORIGINAL_COMMAND` and HTTP request bodies are intentionally ignored.
|
||||
|
||||
## 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 with a clear error.
|
||||
|
||||
| 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 one, both, or neither — though neither = startup failure.
|
||||
|
||||
## Environment variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|-------------------------|----------------------------------------|----------------------------------------------------------------------------|
|
||||
| `FORCE_COMMAND` | *(required)* | The command run on every authenticated session. Shell metacharacters OK. |
|
||||
| `HTTP_TOKEN` | *(unset → HTTP disabled)* | Enables the HTTP listener. Clients send `Authorization: Bearer <this>`. |
|
||||
| `HTTP_PORT` | `8080` | Port for the HTTP listener (only when `HTTP_TOKEN` is set). |
|
||||
| `SSH_PORT` | `22` | Port for sshd inside the container. |
|
||||
| `AUTHORIZED_KEYS_HOST` | `/etc/bastion/authorized_keys.host` | Path of the host-sourced authorized_keys to merge. |
|
||||
| `AUTHORIZED_KEYS_REPO` | `/etc/bastion/authorized_keys.repo` | Path of the repo-sourced authorized_keys to merge. |
|
||||
|
||||
## Build args
|
||||
|
||||
| Arg | Default | Description |
|
||||
|------------------|---------|-----------------------------------|
|
||||
|
|
@ -97,39 +198,66 @@ The client cannot override the command. `SSH_ORIGINAL_COMMAND` is ignored.
|
|||
| `SSH_UID` | `1000` | UID of the bastion `agent` user. |
|
||||
| `SSH_GID` | `1000` | GID of the bastion `agent` group. |
|
||||
|
||||
## What's Inside
|
||||
## 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**.
|
||||
- **openssh-server** — hardened config: key-only auth, no forwarding, no PAM, no user env, `/sbin/nologin` login shell, global `ForceCommand` directive.
|
||||
- **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.
|
||||
- **bash, ca-certificates, tzdata.**
|
||||
|
||||
## Security Model
|
||||
Total image: ~105 MB. Most of that is docker-cli (~50 MB) and docker-cli-compose (~25 MB).
|
||||
|
||||
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.
|
||||
## Security model
|
||||
|
||||
The security boundary is **the authorized_keys file (SSH) and the `HTTP_TOKEN` (HTTP), plus the `ForceCommand` wrapper**. Once a key or bearer token authenticates, the session runs exactly one command — there is no fallback shell. The bastion holds the docker socket, which is host-root-equivalent, so the only thing standing between a remote attacker and host root is the auth layer + your key/token 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.
|
||||
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.
|
||||
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.
|
||||
8. **Keep alpine + openssh patched.** An unauth RCE in sshd or httpd here means host root. `apk upgrade` in a rebuild cycle.
|
||||
9. **Lock down siblings.** Anyone who can `docker exec` into the app via this bastion can also `docker exec` into `mysql`/`redis`/etc through the same socket. `cap_drop: [ALL]` and `no-new-privileges: true` on every sibling caps the blast radius.
|
||||
10. **One bastion per role.** Don't reuse a single `FORCE_COMMAND` for both interactive shells and deploy automation — separate ports and separate token/key sets make audit trails meaningful.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
start-container (entrypoint)
|
||||
├─ generate host keys (idempotent, persisted via /etc/ssh/keys volume)
|
||||
├─ generate host keys (idempotent, persisted via /etc/ssh/keys bind mount)
|
||||
├─ 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)
|
||||
├─ align docker socket group membership to host GID (if socket is mounted)
|
||||
├─ start httpd → /var/www/cgi-bin/run (if $HTTP_TOKEN is set)
|
||||
└─ exec sshd -D -e
|
||||
|
||||
ssh client
|
||||
└─ key auth as `agent`
|
||||
└─ ForceCommand /etc/bastion/force-command
|
||||
└─ exec sh -c "$FORCE_COMMAND"
|
||||
|
||||
http client
|
||||
└─ Authorization: Bearer <HTTP_TOKEN>
|
||||
└─ /var/www/cgi-bin/run validates token
|
||||
└─ exec /etc/bastion/force-command
|
||||
└─ exec sh -c "$FORCE_COMMAND"
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT.
|
||||
|
||||
## Star History
|
||||
|
||||
<a href="https://www.star-history.com/?repos=blax-software%2Fdocker-bastion&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=blax-software/docker-bastion&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=blax-software/docker-bastion&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=blax-software/docker-bastion&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ echo "=========================================="
|
|||
|
||||
SSH_USER="agent"
|
||||
SSH_PORT="${SSH_PORT:-22}"
|
||||
HTTP_PORT="${HTTP_PORT:-8080}"
|
||||
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}"
|
||||
|
|
@ -134,13 +135,56 @@ if [ "$SSH_PORT" != "22" ]; then
|
|||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7) sshd config sanity check + launch
|
||||
# 7) Optional HTTP listener (opt-in via $HTTP_TOKEN)
|
||||
#
|
||||
# When $HTTP_TOKEN is set, busybox httpd binds $HTTP_PORT and serves a
|
||||
# single CGI endpoint at /cgi-bin/run. Clients authenticate by sending
|
||||
# `Authorization: Bearer <HTTP_TOKEN>` and the script exec's the same
|
||||
# /etc/bastion/force-command wrapper SSH uses — so the output streams
|
||||
# back (httpd uses Connection: close on CGI without Content-Length,
|
||||
# meaning the client sees bytes as they arrive).
|
||||
#
|
||||
# Leave $HTTP_TOKEN unset to keep the bastion SSH-only.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "[5/5] sshd config check..."
|
||||
echo "[5/6] HTTP listener..."
|
||||
if [ -n "${HTTP_TOKEN:-}" ]; then
|
||||
echo " HTTP_TOKEN set — enabling httpd on port ${HTTP_PORT}"
|
||||
mkdir -p /var/www/cgi-bin
|
||||
cat > /var/www/cgi-bin/run <<'CGI'
|
||||
#!/bin/sh
|
||||
# Auto-generated by docker-bastion start-container.
|
||||
# busybox httpd places the Authorization header in $HTTP_AUTHORIZATION.
|
||||
expected="${HTTP_TOKEN:-}"
|
||||
got="${HTTP_AUTHORIZATION#Bearer }"
|
||||
if [ -z "$expected" ] || [ "$got" != "$expected" ]; then
|
||||
printf 'Status: 401 Unauthorized\r\nContent-Type: text/plain\r\nWWW-Authenticate: Bearer\r\n\r\nUnauthorized\n'
|
||||
exit 0
|
||||
fi
|
||||
printf 'Content-Type: text/plain\r\nCache-Control: no-cache\r\nX-Accel-Buffering: no\r\n\r\n'
|
||||
# Merge stderr into stdout so failures are visible to the client.
|
||||
exec /etc/bastion/force-command 2>&1
|
||||
CGI
|
||||
chmod 0755 /var/www/cgi-bin/run
|
||||
# `httpd` here is the busybox-extras applet (symlink → /bin/busybox-extras).
|
||||
# `busybox httpd` would fail — the core busybox binary in alpine doesn't
|
||||
# include the httpd applet anymore; only busybox-extras does.
|
||||
# -f = foreground (we background with &); -p PORT; -h DOCROOT; -u USER.
|
||||
httpd -f -p "${HTTP_PORT}" -h /var/www -u "${SSH_USER}" &
|
||||
HTTP_PID=$!
|
||||
echo " httpd PID ${HTTP_PID}, endpoint: POST /cgi-bin/run with Authorization: Bearer <token>"
|
||||
else
|
||||
echo " HTTP_TOKEN unset — HTTP listener disabled (SSH-only mode)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8) sshd config sanity check + launch
|
||||
# ---------------------------------------------------------------------------
|
||||
echo "[6/6] sshd config check..."
|
||||
/usr/sbin/sshd -t -f /etc/ssh/sshd_config
|
||||
|
||||
echo "=========================================="
|
||||
echo " Listening on port ${SSH_PORT}"
|
||||
echo " SSH: port ${SSH_PORT}"
|
||||
[ -n "${HTTP_TOKEN:-}" ] && echo " HTTP: port ${HTTP_PORT} (token-protected)"
|
||||
echo " User: ${SSH_USER}"
|
||||
echo " ForceCommand: ${FORCE_COMMAND_VALUE}"
|
||||
echo "=========================================="
|
||||
|
|
|
|||
Loading…
Reference in New Issue