Web based SSH Instant Access
Zero-Password Per-User SSH Authentication via Authentik SSO & Ephemeral SSH CA
1. Architectural Overview
This document details the design, configuration, and invocation flow for securing the ttyd web terminal service (https://ttyd.drheck.dev) on server ul.
The system integrates Authentik SSO (Identity Provider) with OpenSSH User Certificate Authority (CA) to grant authenticated web users instant, passwordless SSH shell access exclusively to their matching Linux user account.
[ Web Browser ]
│
│ 1. HTTPS Request
▼
[ Nginx Reverse Proxy ] ───2. auth_request (Cookie check)───► [ Authentik Outpost ]
│ │
│ 3. Forward Header: X-Authentik-Username │ 2b. HTTP 200 OK + User Headers
▼ ▼
[ ttyd Web Terminal Container ]
│
│ 4. Spawns Wrapper with -H X-authentik-username
▼
[ ttyd-ssh-wrapper.sh ]
│
│ 5. Reads $TTYD_USER / $HTTP_X_AUTHENTIK_USERNAME
│ 6. Generates ephemeral Ed25519 key pair in /tmp
│ 7. Signs key using ttyd_ca_user_key (Principal = $USER, Valid = +2m)
│
│ 8. ssh -i <ephemeral_key> $USER@10.0.0.3
▼
[ OpenSSH Daemon (sshd on ul) ]
│
│ 9. Validates signature against /etc/ssh/trusted_user_ca_keys.pub
│ 10. Verifies Principal == target Linux user account
▼
[ Native Shell Session for $USER ]
2. Authentication & Execution Lifecycle
Step 1: Web Request & Identity Injection
- User visits
https://ttyd.drheck.dev/. - Nginx intercepts the request and issues an subrequest to Authentik (
/outpost.goauthentik.io/auth/nginx). - If unauthenticated, Nginx redirects the user to the Authentik login portal.
- Once authenticated, Authentik returns HTTP 200 OK to Nginx along with identity headers (
X-Authentik-Username: <username>). - Nginx forwards the request to
ttydathttp://[fd77:7777:5010:0:1337:f1ac:d00d:771d]:7681with theX-authentik-usernameheader set.
Step 2: ttyd Process Spawning
ttydreceives the connection and reads the reverse proxy auth header specified by flag-H X-authentik-username.ttydsets the environment variableTTYD_USER=<username>(and forwards HTTP request headers) into the sub-process environment.ttydspawns/usr/local/bin/ttyd-ssh-wrapper.sh.
Step 3: Certificate Issuance & SSH Connection
ttyd-ssh-wrapper.shextracts the authenticated username (USER="${TTYD_USER:-${HTTP_X_AUTHENTIK_USERNAME:-...}}").- Security Gate: If
$USERis empty or missing, the wrapper outputsAccess Denied: No authenticated identity provided by Authentik.and hard-exits (exit 1). Manual prompt input is strictly prohibited to prevent account impersonation. - The wrapper creates a secure temporary directory in
/tmpusingmktemp -dand sets a bashtrapto guarantee cleanup on exit. - Generates an in-memory temporary SSH key pair:
ssh-keygen -q -t ed25519 -N "" -f "$KEY". - Signs the public key with the SSH CA private key (
/keys/ttyd_ca_user_key):ssh-keygen -s /keys/ttyd_ca_user_key -I "ttyd-$USER" -n "$USER" -V +2m "$KEY.pub"-I "ttyd-$USER": Log Identifier for OpenSSH audit trails.-n "$USER": Principal Restriction. OpenSSH will ONLY permit this certificate to authenticate as user$USER.-V +2m: Certificate validity is strictly capped at 2 minutes.
- Execs
ssh -i "$KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$USER@10.0.0.3".
Step 4: SSH Verification & Shell Teardown
- OpenSSH (
sshd) on10.0.0.3:22receives the certificate. sshdchecks/etc/ssh/sshd_config.d/10-drheck-certs.conf, referencingTrustedUserCAKeys /etc/ssh/trusted_user_ca_keys.pub.sshdverifies that:- The certificate was signed by a trusted CA key in
trusted_user_ca_keys.pub. - Current time falls within the 2-minute validity window.
- The requested target account matches the certificate principal (
$USER).
- The certificate was signed by a trusted CA key in
sshdgrants interactive shell access and drops the user into their native home directory.- When the user exits the terminal or closes the browser tab, the wrapper process terminates, triggering the
trapthat shreds and deletes the temporary key files in/tmp.
3. Configuration & Source Code Reference
A. Container Dockerfile (/data/docker/ttyd/Dockerfile)
Bakes openssh-client into the ttyd image to avoid runtime apt execution:
FROM tsl0922/ttyd:latest
RUN apt-get update && apt-get install -y --no-install-recommends openssh-client && rm -rf /var/lib/apt/lists/*
B. Docker Compose Spec (/data/docker/ttyd/docker-compose.yml)
services:
ttyd:
build: .
image: ttyd-ssh:local
container_name: ttyd
restart: unless-stopped
volumes:
- ./ttyd_ca_user_key:/keys/ttyd_ca_user_key:ro
- ./ttyd-ssh-wrapper.sh:/usr/local/bin/ttyd-ssh-wrapper.sh:ro
command: ["ttyd", "-H", "X-authentik-username", "-W", "-p", "7681", "/usr/local/bin/ttyd-ssh-wrapper.sh"]
ports:
- "[fd77:7777:5010:0:1337:f1ac:d00d:771d]:7681:7681"
C. Wrapper Script (/data/docker/ttyd/ttyd-ssh-wrapper.sh)
#!/usr/bin/env bash
set -euo pipefail
# Read username forwarded strictly from Nginx/Authentik auth headers
USER="${TTYD_USER:-${HTTP_X_AUTHENTIK_USERNAME:-${HTTP_X_AUTHENTIK_META_USERNAME:-${HTTP_X_FORWARDED_USER:-${HTTP_X_WEBAUTH_USER:-}}}}}"
if [ -z "$USER" ]; then
echo "Access Denied: No authenticated identity provided by Authentik." >&2
sleep 3
exit 1
fi
TMP_DIR=$(mktemp -d /tmp/ttyd_ssh_XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
KEY="$TMP_DIR/id_ed25519"
# Generate temporary key pair
ssh-keygen -q -t ed25519 -N "" -f "$KEY"
# Sign SSH certificate restricting principal strictly to $USER valid for 2 minutes
ssh-keygen -s /keys/ttyd_ca_user_key -I "ttyd-$USER" -n "$USER" -V +2m "$KEY.pub" >/dev/null 2>&1
# Connect via SSH as $USER to 10.0.0.3
exec ssh -i "$KEY" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$USER@10.0.0.3" "$@"
D. Nginx Proxy Configuration (/etc/nginx/sites-available/ttyd)
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name ttyd.drheck.dev;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
proxy_pass http://[fd77:7777:5010:0:1337:f1ac:d00d:771d]:7681;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
auth_request /outpost.goauthentik.io/auth/nginx;
error_page 401 = @goauthentik_proxy_signin;
auth_request_set $auth_cookie $upstream_http_set_cookie;
add_header Set-Cookie $auth_cookie;
auth_request_set $authentik_username $upstream_http_x_authentik_username;
proxy_set_header X-authentik-username $authentik_username;
proxy_set_header X-Authentik-Username $authentik_username;
proxy_set_header X-Forwarded-User $authentik_username;
proxy_set_header Remote-User $authentik_username;
}
location /outpost.goauthentik.io {
proxy_pass https://auth.drheck.dev/outpost.goauthentik.io;
proxy_set_header Host auth.drheck.dev;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
add_header Set-Cookie $auth_cookie;
auth_request_set $auth_cookie $upstream_http_set_cookie;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
}
location @goauthentik_proxy_signin {
internal;
add_header Set-Cookie $auth_cookie;
return 302 /outpost.goauthentik.io/start?rd=$scheme://$http_host$request_uri;
}
}
E. OpenSSH Configuration (/etc/ssh/sshd_config.d/10-drheck-certs.conf)
TrustedUserCAKeys /etc/ssh/trusted_user_ca_keys.pub
HostCertificate /etc/ssh/ssh_host_ed25519_key-cert.pub
- Note:
/etc/ssh/trusted_user_ca_keys.pubcontains the public keys for both general user CAs and/data/docker/ttyd/ttyd_ca_user_key.pub.
4. Backup & Disaster Recovery Procedures
Backup Script (/home/luke/git/backups/ul/scripts/ttyd_backup)
Archives the entire /data/docker/ttyd directory (including Dockerfile, docker-compose.yml, ttyd-ssh-wrapper.sh, and ttyd_ca_user_key*) into a PAR2-verified XZ archive:
#!/usr/bin/env bash
set -euo pipefail
TIMESTAMP=$(date +%Y-%m-%d_%H%M.%S)
CURRENT_DIR="/data/backups/services/current/ttyd"
ARCHIVE_DIR="/data/backups/services/archive/ttyd"
mkdir -p "${CURRENT_DIR}" "${ARCHIVE_DIR}"
for existing in "${CURRENT_DIR}"/*; do
if [ -d "${existing}" ]; then
mv "${existing}" "${ARCHIVE_DIR}/"
fi
done
BACKUP_DIR="${CURRENT_DIR}/${TIMESTAMP}"
mkdir -p "${BACKUP_DIR}"
tar -cJf "${BACKUP_DIR}/ttyd_data.tar.xz" -C /data/docker/ttyd .
par2 create -r10 "${BACKUP_DIR}/ttyd_data.tar.xz" >/dev/null
echo "ttyd backup completed: ${BACKUP_DIR}"
Restore Script (/home/luke/git/backups/ul/scripts/ttyd_restore)
Restores the service data and automatically registers the SSH CA key with OpenSSH upon recovery:
#!/usr/bin/env bash
set -euo pipefail
# [Target backup selection logic...]
par2 verify "${TARGET}/ttyd_data.tar.xz.par2"
mkdir -p /data/docker/ttyd
tar -xJf "${TARGET}/ttyd_data.tar.xz" -C /data/docker/ttyd
# Automated CA Key Restoration Hook
if [ -f /data/docker/ttyd/ttyd_ca_user_key.pub ]; then
cp -f /data/docker/ttyd/ttyd_ca_user_key.pub /etc/ssh/ttyd_ca_user_key.pub
chmod 644 /etc/ssh/ttyd_ca_user_key.pub
cat /etc/ssh/drheck_ca_user_key.pub /etc/ssh/ttyd_ca_user_key.pub > /etc/ssh/trusted_user_ca_keys.pub 2>/dev/null || true
systemctl reload sshd 2>/dev/null || true
fi
echo "ttyd restore completed from ${TARGET}"