Skip to main content

Web based SSH Instant Access

Zero-PasswordWebshell Per-User(webshell.drheck.dev) Setup & Rollout Guide

This i a setup guide for rolling out webshell.drheck.dev, a browser-based SSH Authenticationterminal service authenticated via Authentik SSOSingle &Sign-On Ephemeral(SSO) SSHand CAshort-lived OpenSSH certificates signed by Smallstep (step-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.

[ WebBrowser BrowserClient ]
        │
        │▼ 1. (HTTPS Request/ ▼WebSocket)
[ Nginx Reverse Proxy (/etc/nginx/sites-available/webshell) ]
        │
        ├───2. auth_request (Cookie check)───► [ Authentik Embedded Outpost ] (SSO Auth & Claims)
        │
        ▼ (HTTP / WebSocket + X-Authentik-Username Header)
[ Webshell Container (ttyd + login-wrapper.sh) on eth77 fd77:...:771d:7681 ]
        │
        │├── │Step 3.SSH ForwardCertificate Header:Sign X-Authentik-Username                      │ 2b. HTTP 200 OK + User Headers
      ▼                                                              ▼──► [ ttyd Web TerminalStep-CA Container on fd77:...:ca:9000 ]
        │
        │▼ 4. Spawns Wrapper(SSH with -HEphemeral X-authentik-usernameEd25519 ▼Certificate over IPv6)
[ ttyd-ssh-wrapper.shHost ]
      │
      │ 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
      ▼
[ul 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 $USERfd77:7777:5010:0:1337:f1ac:d00d:337]:22 ]

Key

2. Authentication & Execution Lifecycle

Step 1: Web Request & Identity InjectionComponents

  1. Userstep-ca: visitsSmallstep Certificate Authority issuing 5-minute ephemeral Ed25519 user certificates, running as user https://ttyd.drheck.dev/akadmin.
Nginx intercepts the request and issues an subrequest to Authentik (/outpost.goauthentik.io/auth/nginx1101:1101). Ifwebshell: unauthenticated,Debian NginxBookworm redirectscontainer therunning userstatic tottyd the1.7.4, Authentikstep loginCLI portal.0.25.0, and login-wrapper.sh. OnceAuthentik: authenticated,Provides AuthentikOIDC returnsintegration HTTPfor 200step-ca OKand toForwardAuth single sign-on (webshell.drheck.dev). Nginx: alongTerminates withTLS and proxies HTTP/WebSocket connections while passing authenticated identity headers (X-Authentik-Username: <username>Username). NginxOpenSSH forwards(sshd): the requestConfigured to trust ttydstep-ca atsigned user certificates for passwordless OS login on host http:ul.

2. Network & ULA IPv6 Allocation

Every container on host ul binds to a unique ULA IPv6 address on interface eth77 (fd77:7777:5010:0:1337:f1ac:d00d:.../64).

    step-ca IPv6: fd77:7777:5010:0:1337:f1ac:d00d:ca (Port 9000) webshell IPv6: fd77:7777:5010:0:1337:f1ac:d00d:771d (Port 7681) Host ul ULA IPv6 SSH Target: fd77:7777:5010:0:1337:f1ac:d00d:337 (Port 22)

    3. Step 1: Step-CA Configuration & Provisioners

    A. Docker Compose & Environment

    In /data/docker/webshell/docker-compose.yml, define step-ca (running as 1101:1101) and webshell with dual-network connectivity:

    services:
      step-ca:
        image: smallstep/step-ca:latest
        container_name: step-ca
        restart: unless-stopped
        user: "1101:1101"
        env_file: .env
        volumes:
          - ./step-ca:/home/step:rw
        ports:
          - "[fd77:7777:5010:0:1337:f1ac:d00d:ca]:9000:9000"
        networks:
          - infra_net
    
      webshell:
        build: .
        container_name: webshell
        restart: unless-stopped
        environment:
          - STEP_CA_URL=https://step-ca:9000
        volumes:
          - ./login-wrapper.sh:/usr/local/bin/login-wrapper.sh:ro
          - ./step-ca/certs/root_ca.crt:/etc/step-ca/root_ca.crt:ro
          - ./step-ca/secrets/webshell_svc_pwd:/etc/step-ca/webshell_svc_pwd:ro
        ports:
          - "[fd77:7777:5010:0:1337:f1ac:d00d:771d]:76817681:7681"
        networks:
          - infra_net
          - webshell_net
        depends_on:
          - step-ca
    
    networks:
      infra_net:
        external: true
      webshell_net:
        enable_ipv6: true
        ipam:
          config:
            - subnet: fd77:7777:5010:771d::/64
    

    B. CA Directory Permissions

    On host ul, restrict access to all CA keys and provisioner passwords to user akadmin:akadmin (1101:1101) with themode X-authentik-username700:

    header
    sudo set.chown -R 

    1101:1101 /data/docker/webshell/step-ca sudo chmod -R o-rwx /data/docker/webshell/step-ca sudo chmod 700 /data/docker/webshell/step-ca/secrets /data/docker/webshell/step-ca/secrets/*


    4. Step 2: ttydHost Processul SpawningOpenSSH Daemon Trust Setup

    To allow sshd on host ul ([fd77:7777:5010:0:1337:f1ac:d00d:337]:22) to accept certificates signed by step-ca:

    1. ttyd receivesExtract the connectionCA anduser readspublic thekey: reverse
      docker proxyexec authstep-ca headerstep specifiedcertificate byinspect flag--format -Hjson X-authentik-username/home/step/certs/user_ca_key.pub
      .
    2. ttyd sets the environment variable TTYD_USER=<username> (and forwards HTTP request headers) into the sub-process environment.
    ttyd spawns /usr/local/bin/ttyd-ssh-wrapper.sh.

    Step 3: Certificate Issuance & SSH Connection

      ttyd-ssh-wrapper.sh extracts the authenticated username (USER="${TTYD_USER:-${HTTP_X_AUTHENTIK_USERNAME:-...}}"). Security Gate: If $USER is empty or missing, the wrapper outputs Access 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 /tmp using mktemp -d and sets a bash trap to guarantee cleanup on exit. Generates an in-memory temporary SSH key pair: ssh-keygen -q -t ed25519 -N "" -f "$KEY". SignsAppend the public key with the SSH CA private key (/keys/ttyd_ca_user_keyecdsa-sha2-nistp256 ... or ssh-ed25519 ...):
      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/etc/ssh/drheck_ca_user_key.pub. -VVerify +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) on 10.0.0.3:22 receives the certificate. sshd checksthat /etc/ssh/sshd_config.d/10-drheck-certs.conf, referencingcontains:
          TrustedUserCAKeys /etc/ssh/trusted_user_ca_keys.drheck_ca_user_key.pub
          .
          sshd verifies 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). Reload OpenSSH daemon:
            sshdsudo systemctl reload ssh
            
            grants 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 trap that shreds and deletes the temporary key files in /tmp.

            3.5. ConfigurationStep &3: SourceWeb CodeTerminal ReferenceBackend (ttyd + login-wrapper.sh) Setup

            A. Container Dockerfile (/data/docker/ttyd/webshell/Dockerfile)

            BakesMandatory: Include openssh-client-6 intoand the-i :: in CMD so ttyd imagebinds to avoidIPv6 runtimeinside aptthe execution:container when connected to an IPv6 Docker bridge network.

            FROM tsl0922/ttyd:latestdebian:bookworm-slim
            
            ENV DEBIAN_FRONTEND=noninteractive
            RUN apt-get update && apt-get install -y --no-install-recommends     ca-certificates curl openssh-client bash procps     && rm -rf /var/lib/apt/lists/*
            
            
            #

            B.Install Docker Compose Spec (/data/docker/ttyd/docker-compose.yml)

            services:
              ttyd:
                build: .
                image: ttyd-ssh:local
                container_name:static ttyd restart:1.7.4 unless-stopped& volumes:step CLI 0.25.0
            RUN curl -sSL .https:/ttyd_ca_user_key:/keys/ttyd_ca_user_key:rogithub.com/tsl0922/ttyd/releases/download/1.7.4/ttyd.x86_64 -o ./ttyd-ssh-wrapper.sh:/usr/local/bin/ttyd-ssh-ttyd     && chmod +x /usr/local/bin/ttyd     && curl -sSL https://dl.step.sm/gh-release/cli/docs-step-cli/v0.25.0/step_linux_0.25.0_amd64.tar.gz | tar -xz -C /tmp     && mv /tmp/step_0.25.0/bin/step /usr/local/bin/step     && chmod +x /usr/local/bin/step     && rm -rf /tmp/step_0.25.0
            
            COPY login-wrapper.sh:rosh command:/usr/local/bin/login-wrapper.sh
            RUN chmod +x /usr/local/bin/login-wrapper.sh
            
            EXPOSE 7681
            ENTRYPOINT ["/usr/local/bin/ttyd"]
            CMD ["-6", "-i", "::", "-W", "-H", "X-authentik-username", "-W", "-p", "7681"Authentik-Username", "/usr/local/bin/ttyd-ssh-login-wrapper.sh"]
                ports:
                  - "[fd77:7777:5010:0:1337:f1ac:d00d:771d]:7681:7681"
            

            C.B. Backend Login Wrapper Script (/data/docker/ttyd/ttyd-ssh-webshell/login-wrapper.sh)

            #!/usr/bin/env bash
            set -euo pipefail
            
            # ReadResolve username forwardedacross strictlyCGI-style fromenvironment Nginx/Authentikvariables authexported headersby USER=ttyd/proxy
            TARGET_USER="${X_AUTHENTIK_USERNAME:-${TTYD_USER:-${HTTP_X_AUTHENTIK_USERNAME:-${HTTP_X_AUTHENTIK_META_USERNAME:AUTHENTIK_USERNAME:-${HTTP_X_FORWARDED_USER:-${HTTP_X_WEBAUTH_USER:-}}}}}"
            
            if [[ -z "$USER"{TARGET_USER}" ]]; then
              echo "AccessAuthentication Denied:Error: NoMissing authenticatedAuthentik identity provided by Authentik.identity." >&2
                sleep 3
              exit 1
            fi
            
            TMP_DIR=# Strict alphanumeric regex check
            if [[ ! "$(mktemp{TARGET_USER}" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then
              echo "Authentication Error: Invalid username format." >&2
              exit 1
            fi
            
            # Reject privileged and system OS accounts
            case "${TARGET_USER}" in
              root|admin|postgres|www-data|daemon|nobody|bin|sys|sync|games|man|lp|mail|news|uucp|proxy|backup|list|irc|gnats|systemd*|docker*)
                echo "Authentication Error: Access to privileged or system account is forbidden." >&2
                exit 1
                ;;
            esac
            
            KEY_DIR="/tmp/ssh_${RANDOM}_$$"
            mkdir -dm 700 "${KEY_DIR}"
            KEY_PATH="${KEY_DIR}/tmp/ttyd_ssh_XXXXXX)id_ed25519"
            
            trapcleanup() '{
              rm -rf "$TMP_DIR"'{KEY_DIR}"
            }
            trap cleanup EXIT KEY="$TMP_DIR/id_ed25519"INT TERM
            
            # Generate temporaryephemeral keyEd25519 pairkeypair
            ssh-keygen -q -t ed25519 -f "${KEY_PATH}" -N "" -f "$KEY"q
            
            # Sign SSHuser certificate restrictingvia principalJWK strictlyprovisioner to(--sign $USERflag validis mandatory for 2non-interactive minutesexecution)
            ssh-keygenif command -sv /keys/ttyd_ca_user_key -I "ttyd-$USER" -n "$USER" -V +2m "$KEY.pub"step >/dev/null 2>&1 && [[ -n "${STEP_CA_URL:-}" ]] && [[ -f "/etc/step-ca/webshell_svc_pwd" ]]; then
              step ssh certificate "${TARGET_USER}" "${KEY_PATH}.pub"     --sign     --provisioner="webshell-svc"     --provisioner-password-file="/etc/step-ca/webshell_svc_pwd"     --ca-url="${STEP_CA_URL}"     --root="/etc/step-ca/root_ca.crt"     --not-after=5m     --force >/dev/null 2>&1 || true
            fi
            
            # ConnectLogin viapasswordless to host ul SSH asdaemon $USERover tolocal 10.0.0.3ULA IPv6
            exec ssh -i "$KEY" -o StrictHostKeyChecking=no          -o UserKnownHostsFile=/dev/null          -i "$USER@10.0.0.3"{KEY_PATH}"          "${TARGET_USER}@fd77:7777:5010:0:1337:f1ac:d00d:337" "$@"
            

            D.

            6. Step 4: Authentik Single Sign-On Provisioning (ak shell)

            To configure Authentik ForwardAuth without encountering 500 Internal Server Error, Redirect URI Error, or ERR_TOO_MANY_REDIRECTS, execute the following Python provisioning script in authentik-server:

            docker exec -i authentik-server ak shell << 'EOF'
            from authentik.core.models import Application
            from authentik.providers.proxy.models import ProxyProvider, ProxyMode
            from authentik.providers.oauth2.models import RedirectURI, RedirectURIMatchingMode, RedirectURIType
            from authentik.outposts.models import Outpost
            
            SERVICE_NAME = "webshell"
            EXTERNAL_HOST = f"https://{SERVICE_NAME}.drheck.dev"
            
            # 1. Inherit default scope property mappings from a working provider template
            template_provider = ProxyProvider.objects.get(name="cockpit")
            
            # 2. Create ProxyProvider with complete OAuth, redirect URI, and cookie settings
            provider, _ = ProxyProvider.objects.get_or_create(
                name=SERVICE_NAME,
                defaults={
                    "external_host": EXTERNAL_HOST,
                    "mode": ProxyMode.FORWARD_SINGLE,
                    "authorization_flow_id": "f9ad5326-f668-4d1f-861d-43b47f0a002e",
                    "cookie_domain": "drheck.dev",
                    "grant_types": ["authorization_code", "client_credentials", "password"],
                    "redirect_uris": [
                        RedirectURI(
                            matching_mode=RedirectURIMatchingMode.STRICT,
                            url=f"{EXTERNAL_HOST}/outpost.goauthentik.io/callback?X-authentik-auth-callback=true",
                            redirect_uri_type=RedirectURIType.AUTHORIZATION
                        ),
                        RedirectURI(
                            matching_mode=RedirectURIMatchingMode.STRICT,
                            url=f"{EXTERNAL_HOST}?X-authentik-auth-callback=true",
                            redirect_uri_type=RedirectURIType.AUTHORIZATION
                        ),
                    ]
                }
            )
            
            provider.property_mappings.set(template_provider.property_mappings.all())
            provider.save()
            
            # 3. Create Application
            app, _ = Application.objects.get_or_create(
                slug=SERVICE_NAME,
                defaults={
                    "name": SERVICE_NAME.capitalize(),
                    "meta_launch_url": f"{EXTERNAL_HOST}/",
                    "open_in_new_tab": True,
                    "provider": provider,
                }
            )
            
            # 4. Mandatory: Assign provider to authentik Embedded Outpost
            outpost = Outpost.objects.get(name="authentik Embedded Outpost")
            if provider not in outpost.providers.all():
                outpost.providers.add(provider)
                outpost.save()
            
            print(f"Successfully provisioned {SERVICE_NAME} and attached to Embedded Outpost.")
            EOF
            

            7. Step 5: Nginx Reverse Proxy Configuration (/etc/nginx/sites-available/ttydwebshell)

            map $http_upgrade $proxy_target_connection_webshell {
                default upgrade;
                ""      "";
            }
            
            server {
                listen 443 ssl;
                listen [::]:443 ssl;
                http2 on;
                server_name ttyd.webshell.drheck.dev;
            
                ssl_certificate     /etc/nginx/certs/fullchain.pem;
                ssl_certificate_key /etc/nginx/certs/privkey.pem;
            
                locationclient_max_body_size /64M;
            
                {# proxy_passAuthentik http://[fd77:7777:5010:0:1337:f1ac:d00d:771d]:7681;ForwardAuth proxy_http_versionSubrequest 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;Configuration
                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;
                auth_request_set $authentik_email $upstream_http_x_authentik_email;
            
                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 $proxy_target_connection_webshell;
                    proxy_set_header Host $host;
                    proxy_set_header X-authentik-usernameReal-IP $authentik_username;remote_addr;
                    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                    proxy_set_header X-Forwarded-Proto $scheme;
                    proxy_set_header X-Authentik-Username $authentik_username;
                    proxy_set_header X-Forwarded-UserAuthentik-Email $authentik_username;
                    proxy_set_header Remote-User $authentik_username;authentik_email;
                }
            
                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.pub contains the public keys for both general user CAs and /data/docker/ttyd/ttyd_ca_user_key.pub.

              4.8. Step 6: Backup & DisasterRestore Recovery ProceduresScripts

              Backup

              Every Scriptservice (on host ul requires shellcheck-verified <service>_backup and <service>_restore scripts located in /home/luke/git/backups/ul/scripts/ttyd_backup)

              with 10% par2 redundancy.

                /home/luke/git/backups/ul/scripts/webshell_backup: Archives the entire /data/docker/ttydwebshell/ directory (including Dockerfile.env, docker-compose.yml, ttyd-ssh-wrapper.sh, and ttyd_ca_user_key*step-ca/) intoto 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"webshell/YYYY-MM-DD_HHMM.SS/webshell_config.tar.xz ARCHIVE_DIR="/data/backups/services/archive/ttyd"and mkdirgenerates -p.par2 "${CURRENT_DIR}"files "${ARCHIVE_DIR}"via 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_restorewebshell_restore):

                RestoresVerifies thearchives servicewith datapar2 verify and automaticallyextracts registersconfigurations theback SSHinto 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/ttydwebshell/.
                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}"

                5.9. SecurityInitial Assumptions & ThreatAttempts Modelvs. AnalysisActual Working Configuration

                  Technical Area Initial Assumption / Attempt Actual Working Configuration & Root Cause Prevention Standard ZeroOutpost SharedAssignment Credentials:(500 NoInternal masterServer SSHError) keyCreating or administrative credential is ever storedProxyProvider in containersAuthentik orautomatically usedactivates forForwardAuth connectionrouting. initiation.Creating Stricta Identity Isolation: Because -n $USER is set during certificate signing, user alice cannot connect to user bob's account. OpenSSH rejects any connection attempt where the target accountprovider does not matchassign it to an Outpost. Unassigned hostnames return 404 Not Found from /outpost.goauthentik.io/auth/nginx, which Nginx converts to 500 Internal Server Error. Explicitly attach providers to authentik Embedded Outpost (outpost.providers.add(provider)) before enabling Nginx traffic. Redirect URIs (Redirect URI Error) Instantiating ProxyProvider sets default authorization callback URLs. The Django ORM defaults redirect_uris to []. Authentik rejects callback URIs against an empty database list. Explicitly define RedirectURI objects for both /outpost.goauthentik.io/callback and root / in ProxyProvider.redirect_uris. Cookie Domain & Scopes (ERR_TOO_MANY_REDIRECTS) Providers operate without defining cookie_domain, grant_types, or property_mappings. Without property_mappings, the principalOAuth signedrequest omits critical scopes (ak_proxy). Without explicit cookie_domain='drheck.dev', Authentik refuses to grant ForwardAuth proxy tokens, causing an endless redirect loop. Clone property_mappings from cockpit and explicitly set cookie_domain='drheck.dev' and grant_types. CGI Env Mapping (Missing Authentik identity) ttyd -H X-Authentik-Username exports X_AUTHENTIK_USERNAME directly to child scripts. ttyd transforms HTTP headers into standard CGI environment variables (HTTP_X_AUTHENTIK_USERNAME). Always check CGI-style fallback variations: TARGET_USER="${X_AUTHENTIK_USERNAME:-${TTYD_USER:-${HTTP_X_AUTHENTIK_USERNAME:-${AUTHENTIK_USERNAME:-}}}}". IPv6 Dual-Stack Listening (502 Bad Gateway) Default ttyd invocation listens on all interfaces when connected to an IPv6 Docker network. Without -6 -i ::, ttyd binds only to IPv4 0.0.0.0. When Docker Engine forwards published IPv6 ports to the container's IPv6 address, Nginx connections are rejected (Connection refused). Always pass "-6", "-i", "::" to ttyd in theDockerfile certificate.so Short-Livedit Impact Window: Ephemeral certificates expirelistens in 120dual-stack seconds.mode. Even ifanContainer attackerScript wereUpdates (old script execution) Modifying login-wrapper.sh on host ul and running docker compose restart runs the updated script. docker compose restart only restarts containers without re-reading COPYed image files. Always bind-mount operational scripts (./login-wrapper.sh:/usr/local/bin/login-wrapper.sh:ro) in docker-compose.yml. CA Secret Privilege Restrictions User luke (1000:1000) owns the CA keys and JWK provisioner secret. Standard unprivileged user accounts should not have read access to capture a signed certificate from memory, it becomes invalid almost immediately. Immediate Key Destruction: TemporaryCA private keys existor inprovisioner /tmppasswords. forAssign millisecondsownership duringof process startup and are scrubbed via process exit traps. Privilege-Separated CA: The private CA key (/data/docker/ttyd/ttyd_ca_user_keywebshell/step-ca) isto permissions-restrictedakadmin:akadmin (06001101:1101) and ownedrun bystep-ca systemcontainer under user luke / docker-data1101:1101.