feat(examples): OSM taxiway bastion (broker mode)
A whitelisted SSH box for an OSM airport-taxiway export pipeline. A backend may run only three commands — taxiways-list / taxiways-export <ICAO> / taxiways-cat <ICAO> — validated whole-line against an allowlist and exec'd shell-free by the vendored bastion-broker. Ubuntu 22.04 standalone (not FROM blaxsoftware/bastion): the export needs osmium, which Alpine doesn't package, and newer GDAL (Debian trixie 3.10) segfaults on the OSM->GeoJSON step — 22.04's GDAL 3.4.1 + osmium 1.14 is the known-good toolchain. Reuses the base repo's bastion-broker unchanged; a compact broker-only sshd entrypoint replaces the Alpine start-container (account unlocked via `usermod -p '*'` so glibc sshd accepts pubkey). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
256a8c4571
commit
ec1ef50937
|
|
@ -0,0 +1,49 @@
|
|||
# ===========================================================================
|
||||
# OSM taxiway bastion — self-contained OSM export box (broker mode)
|
||||
# ===========================================================================
|
||||
# A whitelisted SSH box for an OSM taxiway export pipeline. A client may run ONLY
|
||||
# the three taxiway commands (allowed-commands.list), validated whole-line and
|
||||
# exec'd shell-free by bastion-broker — the same broker the base docker-bastion
|
||||
# image uses.
|
||||
#
|
||||
# WHY UBUNTU 22.04 (not the Alpine base image): the export needs `osmium`
|
||||
# (osmium-tool), which Alpine does not package (only header-only libosmium). And
|
||||
# GDAL matters: newer ogr2ogr (Debian trixie's GDAL 3.10) SEGFAULTS on the
|
||||
# OSM→GeoJSON step, while Ubuntu 22.04's GDAL 3.4.1 + osmium 1.14 is exactly the
|
||||
# known-good toolchain that produced the existing exports. So this flavor is a
|
||||
# small standalone image that vendors the broker + a broker-only sshd entrypoint.
|
||||
#
|
||||
# taxiways-list → ls the "<ICAO>-taxiways.geojson" exports present
|
||||
# taxiways-export <ICAO> → run export-icao-geojson.sh <ICAO> (osmium/ogr2ogr/jq)
|
||||
# taxiways-cat <ICAO> → base64 the export so it tunnels over ssh cleanly
|
||||
# ===========================================================================
|
||||
FROM ubuntu:22.04
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
openssh-server \
|
||||
osmium-tool \
|
||||
gdal-bin \
|
||||
jq \
|
||||
bash \
|
||||
coreutils \
|
||||
tini \
|
||||
ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& mkdir -p /run/sshd /etc/bastion \
|
||||
&& useradd -m -u 1000 -s /bin/sh agent \
|
||||
# Unlock the account (useradd leaves the password '!'-locked). Debian's
|
||||
# glibc openssh with UsePAM no refuses a locked account even for pubkey;
|
||||
# '*' = valid-but-passwordless, so pubkey login works (password auth is
|
||||
# off anyway). Alpine's musl build doesn't need this, hence the base image.
|
||||
&& usermod -p '*' agent
|
||||
|
||||
# The broker (allowlist gate, verbatim from docker-bastion/scripts) + the three
|
||||
# wrappers it may exec + the broker-only entrypoint.
|
||||
COPY bastion-broker /usr/local/bin/bastion-broker
|
||||
COPY bin/taxiways-list bin/taxiways-export bin/taxiways-cat /usr/local/bin/
|
||||
COPY entrypoint /usr/local/bin/entrypoint
|
||||
RUN chmod 0755 /usr/local/bin/bastion-broker /usr/local/bin/taxiways-list \
|
||||
/usr/local/bin/taxiways-export /usr/local/bin/taxiways-cat /usr/local/bin/entrypoint
|
||||
|
||||
EXPOSE 22
|
||||
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint"]
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# OSM taxiway bastion (broker mode)
|
||||
|
||||
A **self-contained, whitelisted** SSH box for an OSM taxiway export pipeline. It lets a
|
||||
backend fetch/generate airport ground-layout GeoJSON without handing it a shell or root:
|
||||
the bastion only ever runs **three** commands, validated whole-line against a regex
|
||||
allowlist and exec'd **shell-free** (no `sh -c`, so nothing an ICAO argument contains can
|
||||
become a shell operator or a path).
|
||||
|
||||
```
|
||||
ssh agent@<osm-host>
|
||||
└─ bastion-broker (key auth; matches request vs allowed-commands.list)
|
||||
├─ taxiways-list → ls *-taxiways.geojson in /osm-maps
|
||||
├─ taxiways-export <ICAO> → export-icao-geojson.sh <ICAO> (osmium/ogr2ogr/jq)
|
||||
└─ taxiways-cat <ICAO> → base64 <ICAO>-taxiways.geojson (ssh-safe tunnel)
|
||||
```
|
||||
|
||||
The OSM toolchain (`osmium`, `ogr2ogr`, `jq`) is baked into the image (see `Dockerfile`),
|
||||
so the export runs **inside** this container against the mounted `osm-maps` directory
|
||||
(which holds a region PBF + `export-icao-geojson.sh`). No docker socket, no second
|
||||
container — the only host data reachable is that one directory.
|
||||
|
||||
## Files
|
||||
|
||||
| file | purpose |
|
||||
|------|---------|
|
||||
| `Dockerfile` | Debian base (Alpine has no `osmium-tool`) + `osmium-tool`/`gdal-bin`/`jq` + openssh + the wrappers |
|
||||
| `bastion-broker` | the allowlist gate, verbatim from `docker-bastion/scripts` (whole-line ERE match → shell-free exec) |
|
||||
| `entrypoint` | broker-only sshd: key-only, single non-root user, every session forced through the broker |
|
||||
| `bin/taxiways-{list,export,cat}` | the only executables the allowlist lets a client run |
|
||||
| `allowed-commands.list` | the regex allowlist (`taxiways-export [A-Z]{4}`, …) — the security boundary |
|
||||
| `docker-compose.yml` | broker-mode service, osm-maps + allowlist + keys mounts, port 6770 |
|
||||
|
||||
> This flavor is **Debian standalone** (not `FROM blaxsoftware/bastion`) because the export needs
|
||||
> `osmium`, which Alpine doesn't package. It reuses the base repo's `bastion-broker` unchanged; only
|
||||
> the sshd bootstrap is a compact broker-only `entrypoint` instead of the Alpine `start-container`.
|
||||
|
||||
## Deploy (on the host holding your osm-maps dir)
|
||||
|
||||
```bash
|
||||
# 1) Authorize the backend's agent key
|
||||
mkdir -p docker-data/bastion/users.d
|
||||
cp /path/to/agent.pub docker-data/bastion/users.d/backend.pub
|
||||
|
||||
# 2) Let the bastion's `agent` user write into osm-maps (export writes files + work/)
|
||||
AGENT_UID=$(docker run --rm blaxsoftware/bastion:latest id -u agent)
|
||||
chown -R "$AGENT_UID" /srv/osm-maps # point at your actual osm-maps dir
|
||||
|
||||
# 3) Confirm the osm-maps dir has the script + PBF
|
||||
ls /srv/osm-maps/export-icao-geojson.sh /srv/osm-maps/*.osm.pbf
|
||||
|
||||
# 4) Build + run
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
To publish the image for reuse:
|
||||
|
||||
```bash
|
||||
docker build --platform linux/amd64 -t blaxsoftware/bastion-osm-taxiways:latest .
|
||||
docker push blaxsoftware/bastion-osm-taxiways:latest
|
||||
```
|
||||
|
||||
## Test
|
||||
|
||||
```bash
|
||||
ssh -p 6770 agent@<osm-host> taxiways-list # → list (maybe empty)
|
||||
ssh -p 6770 agent@<osm-host> taxiways-export EDDF # → runs the export (minutes)
|
||||
ssh -p 6770 agent@<osm-host> taxiways-cat EDDF | base64 -d | head # → GeoJSON
|
||||
ssh -p 6770 agent@<osm-host> 'rm -rf /' # → "command not permitted", exit 126
|
||||
ssh -p 6770 agent@<osm-host> taxiways-export eddf # → refused (lowercase ≠ [A-Z]{4})
|
||||
```
|
||||
|
||||
## Backend wiring
|
||||
|
||||
The backend stores the private key whose `*.pub` you dropped in `users.d/`, and connects
|
||||
as `agent@<osm-host>:6770`, sending one of the three commands per connection. `taxiways-cat`
|
||||
returns base64 that the backend decodes into its own storage; `taxiways-export` runs the
|
||||
(minutes-long) OSM export. A pure-PHP SSH client (e.g. phpseclib) works fine — no `ssh`
|
||||
binary is required on the backend side.
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
# ===========================================================================
|
||||
# OSM taxiway broker allowlist
|
||||
# ===========================================================================
|
||||
# One extended-regex (ERE) rule per line; a request is permitted only if it
|
||||
# matches WHOLE-LINE (anchored ^…$). Blank lines and # comments are ignored,
|
||||
# and this file is re-read on every request (bind mount → edit without a
|
||||
# restart). Matched requests are word-split and exec'd WITHOUT a shell, so the
|
||||
# [A-Z]{4} class is the containment boundary, not just a filter — no ICAO
|
||||
# argument can ever carry an operator or a path.
|
||||
#
|
||||
# NO COMMAND_PREFIX is used: the three wrappers are the whole surface.
|
||||
# ===========================================================================
|
||||
|
||||
taxiways-list
|
||||
taxiways-export [A-Z]{4}
|
||||
taxiways-cat [A-Z]{4}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
#!/bin/sh
|
||||
# ===========================================================================
|
||||
# bastion-broker — command-allowlist gate for docker-bastion "broker mode".
|
||||
#
|
||||
# Invoked once per session with the client-requested command:
|
||||
# • SSH: the force-command wrapper passes "$SSH_ORIGINAL_COMMAND".
|
||||
# • HTTP: the CGI passes the X-Bastion-Command request header.
|
||||
#
|
||||
# The requested command is matched — anchored, WHOLE-LINE — against a set of
|
||||
# extended-regex (ERE) rules. On a match it is word-split (NO shell, so the
|
||||
# metacharacters ; | & ` $( ) < > are literal arguments, never operators)
|
||||
# and exec'd, optionally behind a trusted COMMAND_PREFIX. No match → refused.
|
||||
#
|
||||
# ---------------------------------------------------------------------------
|
||||
# Security model
|
||||
# ---------------------------------------------------------------------------
|
||||
# The regex rules are the ENTIRE authorization boundary for what a client may
|
||||
# run. Two properties keep that boundary tight:
|
||||
#
|
||||
# 1. Whole-line anchoring (grep -x). A rule must match the request from
|
||||
# first character to last; it can never match a substring.
|
||||
# 2. Shell-free execution. The validated request is split on whitespace and
|
||||
# exec'd directly — there is no `sh -c`. A sloppy rule therefore cannot
|
||||
# escalate to command injection; the worst case is that an allowed
|
||||
# program receives an odd-looking argument.
|
||||
#
|
||||
# Still: write rules with restrictive argument classes (e.g. [^[:space:]]+),
|
||||
# never a bare `.*`. Values that must reach the target program intact cannot
|
||||
# contain whitespace (word-splitting) — generate passwords/tokens from a
|
||||
# space-free alphabet (hex, base64url) on the caller side.
|
||||
#
|
||||
# Config is read from boot-written FILES, never the environment: sshd does
|
||||
# not propagate the daemon's env to a ForceCommand session, so anything the
|
||||
# broker needs at session time must live on disk.
|
||||
# ===========================================================================
|
||||
set -u
|
||||
export HOME=/home/agent
|
||||
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
|
||||
|
||||
ENV_RULES=/etc/bastion/allowed-commands.env # snapshot of $ALLOWED_COMMANDS (written at boot)
|
||||
LIVE_RULES=/etc/bastion/allowed-commands.list # optional live bind-mount (re-read every session)
|
||||
PREFIX_FILE=/etc/bastion/command-prefix # optional trusted prefix (written at boot)
|
||||
|
||||
# $1 wins (SSH wrapper / HTTP CGI pass it explicitly); fall back to the env
|
||||
# var sshd sets so the broker also works as a bare ForceCommand target.
|
||||
REQ="${1:-${SSH_ORIGINAL_COMMAND:-}}"
|
||||
|
||||
log() { printf '[broker] %s\n' "$1" >&2; }
|
||||
|
||||
refuse() {
|
||||
# $1 = reason for the audit log (may include the request),
|
||||
# $2 = sanitized message shown to the client.
|
||||
log "DENY: $1"
|
||||
printf 'bastion: %s\n' "$2" >&2
|
||||
exit 126
|
||||
}
|
||||
|
||||
# --- 1) Empty request -----------------------------------------------------
|
||||
[ -n "$REQ" ] || refuse "<empty request>" "no command supplied (broker mode expects a command)"
|
||||
|
||||
# --- 2) Reject anything spanning more than one line -----------------------
|
||||
# grep matches line-by-line; a multi-line request could slip a benign first
|
||||
# line past the allowlist while carrying a second, malicious line that the
|
||||
# shell-free exec would still pass along. Refuse outright.
|
||||
if [ "$(printf '%s' "$REQ" | tr -dc '\n\r' | wc -c)" -ne 0 ]; then
|
||||
refuse "<multiline request>" "command not permitted"
|
||||
fi
|
||||
|
||||
# --- 3) Match against the rule set ---------------------------------------
|
||||
# Sources are concatenated; comments (#…) and blank lines are dropped and
|
||||
# each rule is trimmed of surrounding whitespace (so indented YAML block
|
||||
# lines work). grep -Eqx => ERE, whole-line (implicit ^…$ anchoring), quiet.
|
||||
collect_rules() {
|
||||
for src in "$ENV_RULES" "$LIVE_RULES"; do
|
||||
[ -f "$src" ] || continue
|
||||
awk '{ sub(/^[[:space:]]+/, ""); sub(/[[:space:]]+$/, "") } NF && $0 !~ /^#/' "$src"
|
||||
done
|
||||
}
|
||||
|
||||
matched=0
|
||||
while IFS= read -r pat; do
|
||||
[ -n "$pat" ] || continue
|
||||
if printf '%s' "$REQ" | grep -Eqx -- "$pat"; then
|
||||
matched=1
|
||||
break
|
||||
fi
|
||||
done <<RULES
|
||||
$(collect_rules)
|
||||
RULES
|
||||
|
||||
[ "$matched" -eq 1 ] || refuse "$REQ" "command not permitted"
|
||||
|
||||
# --- 4) Execute -----------------------------------------------------------
|
||||
log "ALLOW: $REQ"
|
||||
|
||||
PREFIX=""
|
||||
[ -f "$PREFIX_FILE" ] && PREFIX="$(cat "$PREFIX_FILE")"
|
||||
|
||||
set -f # no pathname expansion when we word-split below
|
||||
# Intentional, unquoted word-splitting of the trusted prefix + validated
|
||||
# request. set -f above means * ? [ are NOT globbed; IFS does the splitting.
|
||||
# shellcheck disable=SC2086
|
||||
set -- $PREFIX $REQ
|
||||
[ "$#" -ge 1 ] || refuse "$REQ" "command not permitted"
|
||||
|
||||
exec "$@"
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
#!/bin/sh
|
||||
# Emit one ICAO's taxiway GeoJSON as base64 (an ascii-safe tunnel over ssh).
|
||||
# Broker rule: `taxiways-cat [A-Z]{4}`. The backend decodes it into
|
||||
# storage/taxiways/{icao}-taxiways.geojson.
|
||||
set -eu
|
||||
OSM_DIR=/osm-maps
|
||||
|
||||
icao="${1:-}"
|
||||
case "$icao" in
|
||||
[A-Z][A-Z][A-Z][A-Z]) ;;
|
||||
*) echo "invalid ICAO: '$icao'" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
cd "$OSM_DIR" 2>/dev/null || { echo "osm-maps dir not mounted at $OSM_DIR" >&2; exit 4; }
|
||||
f="${icao}-taxiways.geojson"
|
||||
[ -f "$f" ] || { echo "no export for $icao (run taxiways-export $icao first)" >&2; exit 3; }
|
||||
|
||||
exec base64 "$f"
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
#!/bin/sh
|
||||
# Run the OSM taxiway export for one ICAO (osmium/ogr2ogr/jq over the Europe PBF).
|
||||
# Broker rule: `taxiways-export [A-Z]{4}`. Writes <ICAO>-taxiways.geojson into the
|
||||
# osm-maps dir. This takes MINUTES on a cold cache.
|
||||
#
|
||||
# The ICAO is re-validated here (defence in depth — the allowlist already pins
|
||||
# it to [A-Z]{4}, and the broker exec's us shell-free so $1 can't be an operator).
|
||||
set -eu
|
||||
OSM_DIR=/osm-maps
|
||||
|
||||
icao="${1:-}"
|
||||
case "$icao" in
|
||||
[A-Z][A-Z][A-Z][A-Z]) ;;
|
||||
*) echo "invalid ICAO: '$icao'" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
cd "$OSM_DIR" 2>/dev/null || { echo "osm-maps dir not mounted at $OSM_DIR" >&2; exit 4; }
|
||||
[ -x ./export-icao-geojson.sh ] || { echo "export-icao-geojson.sh missing in $OSM_DIR" >&2; exit 5; }
|
||||
|
||||
exec ./export-icao-geojson.sh "$icao"
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
# List the taxiway GeoJSON exports currently present in the osm-maps dir.
|
||||
# Broker rule: `taxiways-list` (no args). Output: one "<ICAO>-taxiways.geojson"
|
||||
# per line (empty if none yet).
|
||||
#
|
||||
# OSM_DIR is hardcoded to the mount point below: sshd does NOT propagate the
|
||||
# container's environment into a ForceCommand session, so we can't read it from
|
||||
# `environment:` — mount the osm-maps dir at exactly this path.
|
||||
set -eu
|
||||
OSM_DIR=/osm-maps
|
||||
|
||||
cd "$OSM_DIR" 2>/dev/null || { echo "osm-maps dir not mounted at $OSM_DIR" >&2; exit 4; }
|
||||
ls -1 *-taxiways.geojson 2>/dev/null || true
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
# ===========================================================================
|
||||
# OSM taxiway bastion — broker mode, self-contained OSM export box
|
||||
# ===========================================================================
|
||||
# Deploy on the host that holds your osm-maps directory (region/planet PBF +
|
||||
# export-icao-geojson.sh). A backend connects over SSH as `agent` and may run
|
||||
# ONLY the three whitelisted taxiway commands (see allowed-commands.list); every
|
||||
# request is validated and exec'd shell-free by bastion-broker.
|
||||
#
|
||||
# Backend side (its own .env / config):
|
||||
# host = agent@<osm-host>
|
||||
# port = 6770 (whatever you publish below)
|
||||
# auth = a private key whose *.pub is dropped in docker-data/bastion/users.d/
|
||||
#
|
||||
# Setup:
|
||||
# 1. Drop the backend's agent public key:
|
||||
# mkdir -p docker-data/bastion/users.d
|
||||
# cp /path/to/agent.pub docker-data/bastion/users.d/backend.pub
|
||||
# 2. Make the osm-maps dir writable by the bastion's `agent` user (export writes
|
||||
# <ICAO>-taxiways.geojson + work/ intermediates there):
|
||||
# AGENT_UID=$(docker run --rm blaxsoftware/bastion:latest id -u agent)
|
||||
# chown -R "$AGENT_UID" /srv/osm-maps
|
||||
# 3. Ensure the osm-maps dir contains export-icao-geojson.sh + the region PBF.
|
||||
# 4. docker compose up -d --build
|
||||
# 5. Test from a host holding the agent key:
|
||||
# ssh -p 6770 agent@<osm-host> taxiways-list
|
||||
# ssh -p 6770 agent@<osm-host> taxiways-export EDDF # minutes
|
||||
# ssh -p 6770 agent@<osm-host> taxiways-cat EDDF | base64 -d | head
|
||||
# ssh -p 6770 agent@<osm-host> 'rm -rf /' # → refused (exit 126)
|
||||
# ===========================================================================
|
||||
|
||||
services:
|
||||
osm-taxiways-bastion:
|
||||
build: .
|
||||
image: blaxsoftware/bastion-osm-taxiways:latest
|
||||
container_name: osm-taxiways-bastion
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
# THE osm-maps dir — the only real host data reachable. Must contain the
|
||||
# region PBF + export-icao-geojson.sh, and be writable by the agent user.
|
||||
# Mount at exactly /osm-maps (the wrappers hardcode that path — sshd does
|
||||
# not propagate container env into a ForceCommand session). Point the LEFT
|
||||
# side at wherever your osm-maps dir actually lives.
|
||||
- /srv/osm-maps:/osm-maps
|
||||
|
||||
# Broker allowlist — re-read every request (live edits, no restart).
|
||||
- ./allowed-commands.list:/etc/bastion/allowed-commands.list:ro
|
||||
|
||||
# Authorized clients — one *.pub per identity (read live).
|
||||
- ./docker-data/bastion/users.d:/etc/bastion/users.d
|
||||
|
||||
# Persist the SSH host identity across rebuilds (bind mount, never a named
|
||||
# volume — `down -v` would change the host key and clients would refuse it).
|
||||
- ./docker-data/bastion/keys:/etc/ssh/keys
|
||||
ports:
|
||||
# Bind to an internal interface / front with a VPN if you don't want the
|
||||
# taxiway bastion on the public internet.
|
||||
- "6770:22"
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
#!/bin/sh
|
||||
# ===========================================================================
|
||||
# Broker-only sshd entrypoint for the OSM taxiway bastion.
|
||||
# ===========================================================================
|
||||
# Minimal, security-focused: key-only SSH for one non-root user whose every
|
||||
# session is forced through bastion-broker (client command validated whole-line
|
||||
# against the allowlist, then exec'd shell-free). No shell, no forwarding, no
|
||||
# root login. Host keys persist under the mounted /etc/ssh/keys.
|
||||
# ===========================================================================
|
||||
set -eu
|
||||
|
||||
SSH_USER=agent
|
||||
KEYS_DIR=/etc/ssh/keys
|
||||
USERS_D=/etc/bastion/users.d
|
||||
ALLOW=/etc/bastion/allowed-commands.list
|
||||
|
||||
# 1) Persistent host identity (bind-mounted dir → survives rebuilds).
|
||||
mkdir -p "$KEYS_DIR"
|
||||
for t in rsa ecdsa ed25519; do
|
||||
f="$KEYS_DIR/ssh_host_${t}_key"
|
||||
[ -f "$f" ] || ssh-keygen -t "$t" -f "$f" -N "" -q
|
||||
done
|
||||
chmod 600 "$KEYS_DIR"/ssh_host_* 2>/dev/null || true
|
||||
|
||||
# 2) authorized_keys from the mounted users.d/*.pub (one client identity per file).
|
||||
home=$(getent passwd "$SSH_USER" | cut -d: -f6)
|
||||
mkdir -p "$home/.ssh"
|
||||
: > "$home/.ssh/authorized_keys"
|
||||
if [ -d "$USERS_D" ]; then
|
||||
cat "$USERS_D"/*.pub >> "$home/.ssh/authorized_keys" 2>/dev/null || true
|
||||
fi
|
||||
chown -R "$SSH_USER:$SSH_USER" "$home/.ssh"
|
||||
chmod 700 "$home/.ssh"
|
||||
chmod 600 "$home/.ssh/authorized_keys"
|
||||
|
||||
# The broker reads this live every request; make sure it exists (empty ⇒ deny all).
|
||||
[ -f "$ALLOW" ] || { mkdir -p /etc/bastion; : > "$ALLOW"; }
|
||||
|
||||
# 3) ForceCommand wrapper → hand the client's requested command to the broker.
|
||||
cat > /usr/local/bin/force-command <<'EOF'
|
||||
#!/bin/sh
|
||||
exec /usr/local/bin/bastion-broker "${SSH_ORIGINAL_COMMAND:-}"
|
||||
EOF
|
||||
chmod 0755 /usr/local/bin/force-command
|
||||
|
||||
# 4) sshd config — key-only, single user, forced through the broker, no forwarding.
|
||||
cat > /etc/ssh/sshd_config <<EOF
|
||||
Port 22
|
||||
HostKey $KEYS_DIR/ssh_host_rsa_key
|
||||
HostKey $KEYS_DIR/ssh_host_ecdsa_key
|
||||
HostKey $KEYS_DIR/ssh_host_ed25519_key
|
||||
PermitRootLogin no
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
PubkeyAuthentication yes
|
||||
AllowUsers $SSH_USER
|
||||
ForceCommand /usr/local/bin/force-command
|
||||
AllowTcpForwarding no
|
||||
AllowAgentForwarding no
|
||||
X11Forwarding no
|
||||
PermitTunnel no
|
||||
PermitUserEnvironment no
|
||||
LogLevel VERBOSE
|
||||
EOF
|
||||
|
||||
echo "[osm-taxiways-bastion] sshd starting (broker mode, user=$SSH_USER)"
|
||||
exec /usr/sbin/sshd -D -e
|
||||
Loading…
Reference in New Issue