diff --git a/.env.example b/.env.example index 75c817a..5176ea1 100644 --- a/.env.example +++ b/.env.example @@ -15,14 +15,12 @@ WEB_ENABLED=true WEB_HOSTNAME=files.example.com WEB_HTTPS_PORT=443 WEB_JWT_SECRET=ReplaceWithAtLeast32RandomBytes -WEB_TLS_MODE=step -STEP_CA_URL=https://ca.example.com:9000 -STEP_CA_FINGERPRINT=ReplaceWithStepRootFingerprint -STEP_CA_PROVISIONER=fileserver -STEP_CA_PROVISIONER_PASSWORD=ReplaceWithProvisionerPassword -# STEP_CA_TOKEN=single-use-bootstrap-token -# STEP_CA_PROVISIONER_PASSWORD_FILE=/run/secrets/step-ca-provisioner-password -# STEP_CA_REBOOTSTRAP=false +WEB_TLS_MODE=acme +ACME_CA_SERVER=https://ca.internal/acme/acme/directory +ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem +ACME_HTTP_LISTEN=:80 +ACME_HTTP_PORT=80 +# ACME_RENEW_CHECK_SECONDS=900 # WEB_TLS_CERT_FILE=/state/tls/web.crt # WEB_TLS_KEY_FILE=/state/tls/web.key # WEB_JWT_TTL_SECONDS=28800 diff --git a/README.md b/README.md index d602847..4e5df94 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Run a complete disposable environment with Docker or Podman: The launcher builds the current application and starts an isolated, run-scoped network containing: - a real Samba AD DC for `DEV.TEST`, seeded with users, nested groups, and three `FS_*` groups; -- a real Smallstep CA that issues the web UI certificate for `localhost`; +- a real Smallstep CA whose ACME provisioner issues the web UI certificate for `files.localhost`; - an authenticated rsync daemon used by the normal backup implementation; - the actual file-server image, joined to the dummy domain; - a continuous SMB client that reads, writes, and lists files as several domain users. @@ -126,7 +126,7 @@ The launcher builds the current application and starts an isolated, run-scoped n The preview starts with group, Private, FSLogix, and historical audit data. The client keeps current-day audit activity moving, while a real backup runs immediately and repeats in the background. Open the URL and use the credentials printed by the launcher. Defaults are: ```text -URL: https://localhost:8443 +URL: https://files.localhost:8443 Username: DEV\previewadmin Password: PreviewAdmin123! ``` @@ -140,8 +140,9 @@ Useful overrides: | Variable | Default | Purpose | | --- | --- | --- | | `DEV_HTTPS_PORT` | `8443` | HTTPS port bound to host loopback | +| `DEV_WEB_HOSTNAME` | `files.localhost` | HTTPS name validated by the disposable ACME CA | | `DEV_SKIP_BUILD` | `0` | Set to `1` to reuse already-built local images | -| `DEV_STEP_CA_IMAGE` | `docker.io/smallstep/step-ca:latest` | CA image; pin a tag or digest for reproducible CI | +| `DEV_CA_IMAGE` | `docker.io/smallstep/step-ca:latest` | CA image; pin a tag or digest for reproducible CI | | `DEV_SERVER_IMAGE` | `ad-ds-simple-file-server-dev:latest` | Local file-server image name | | `DEV_AD_IMAGE` | `ad-ds-simple-file-server-ad-dev:latest` | Local AD/client image name | | `DEV_BACKUP_IMAGE` | `ad-ds-simple-file-server-backup-dev:latest` | Local rsync target image name | @@ -209,12 +210,11 @@ The runner returns non-zero on the first failed assertion, prints bounded logs f - optional `BACKUP_PROGRESS_INTERVAL_SECONDS` (default `10`) - `WEB_HOSTNAME` (the DNS name in the HTTPS certificate) - optional `WEB_HTTPS_PORT` (host port, default `443`) - - `STEP_CA_URL` - - `STEP_CA_FINGERPRINT` - - `STEP_CA_PROVISIONER` - - `STEP_CA_PROVISIONER_PASSWORD` + - `ACME_CA_SERVER` (ACME directory URL) + - ACME CA root bundle (a host PEM file imported into the persistent state volume) + - optional `ACME_HTTP_PORT` (HTTP-01 host port, default `80`) - Setup generates a random `WEB_JWT_SECRET`. A one-time `STEP_CA_TOKEN` or a provisioner password file can be configured manually instead of keeping a provisioner password in `.env`. + Setup generates a random `WEB_JWT_SECRET`, stores only the generic ACME settings, and imports the public CA root as `/state/tls/acme-ca-certificates.pem`. Optional: - `SAMBA_HOSTNAME` (defaults to `adsambafsrv`) @@ -301,25 +301,50 @@ The web API has no mutation endpoint other than session login/logout. Files, gro - Login attempts are rate-limited per client address. - Responses set HSTS, a restrictive Content Security Policy, clickjacking protection, MIME sniffing protection, and no-store caching. -### TLS with a local Smallstep CA +### TLS with an internal ACME CA -Default enrollment uses `WEB_TLS_MODE=step`: +The default uses standard ACME HTTP-01 enrollment. The names deliberately describe the protocol and inputs rather than a particular CA product: ```env WEB_ENABLED=true WEB_HOSTNAME=files.example.com WEB_HTTPS_PORT=443 WEB_JWT_SECRET= -WEB_TLS_MODE=step -STEP_CA_URL=https://ca.example.com:9000 -STEP_CA_FINGERPRINT= -STEP_CA_PROVISIONER=fileserver -STEP_CA_PROVISIONER_PASSWORD= +WEB_TLS_MODE=acme +ACME_CA_SERVER=https://ca.internal/acme/acme/directory +ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem +ACME_HTTP_LISTEN=:80 +ACME_HTTP_PORT=80 ``` -At first startup the container bootstraps the CA root, requests a certificate for `WEB_HOSTNAME`, and stores its certificate, key, and Step client state on the persistent `state_data` volume. The `step ca renew --daemon` process renews the certificate; the HTTPS listener notices the certificate file change and reloads it. +`ACME_CA_CERTIFICATES` is a path inside the file-server container. The setup script imports the selected host PEM bundle at the path above; manual deployments must copy or mount the bundle there. The CA must be able to resolve `WEB_HOSTNAME` and reach `http://WEB_HOSTNAME/.well-known/acme-challenge/...` on port 80. `ACME_HTTP_LISTEN` controls the in-container standalone challenge listener and `ACME_HTTP_PORT` controls the Compose host-port mapping. -For secret-file based enrollment, set `STEP_CA_PROVISIONER_PASSWORD_FILE` to a mounted file. A single-use `STEP_CA_TOKEN` is also supported. To use externally managed files instead: +The equivalent Traefik settings translate as follows: + +| Traefik setting | File-server setting | +| --- | --- | +| `acme.httpchallenge=true` | `WEB_TLS_MODE=acme` | +| `acme.httpchallenge.entrypoint=http` | `ACME_HTTP_LISTEN=:80` and `ACME_HTTP_PORT=80` | +| `acme.caserver=https://ca.internal/acme/acme/directory` | `ACME_CA_SERVER=https://ca.internal/acme/acme/directory` | +| `acme.cacertificates=/traefik/stepca/roots.pem` | `ACME_CA_CERTIFICATES=` | + +The client uses [`step ca certificate --acme --root --standalone`](https://smallstep.com/docs/step-cli/reference/ca/certificate/) and periodically checks whether the certificate needs renewal. Renewal is a fresh ACME order, so this mode is not coupled to a proprietary renewal endpoint. The HTTPS listener reloads the replaced certificate files. + +A generic direct-CA/JWK or token enrollment mode remains available when ACME HTTP-01 cannot be routed: + +```env +WEB_TLS_MODE=ca +TLS_CA_URL=https://ca.internal:9000 +TLS_CA_FINGERPRINT= +TLS_CA_PROVISIONER=fileserver +TLS_CA_PROVISIONER_PASSWORD= +# TLS_CA_PROVISIONER_PASSWORD_FILE=/run/secrets/ca-provisioner-password +# TLS_CA_TOKEN= +# TLS_CA_REBOOTSTRAP=false +# TLS_CA_STATE_DIR=/state/tls-client +``` + +Externally managed certificate files can be used instead: ```env WEB_TLS_MODE=files @@ -327,12 +352,12 @@ WEB_TLS_CERT_FILE=/state/tls/web.crt WEB_TLS_KEY_FILE=/state/tls/web.key ``` -Smallstep trust configuration is reused from the state volume on normal restarts, so a temporary CA outage does not stop Samba or an already-certificate-equipped web service. Set `STEP_CA_REBOOTSTRAP=true` only when intentionally replacing the configured CA trust. - Useful optional settings: | Variable | Default | Purpose | | --- | ---: | --- | +| `WEB_TLS_AUTORENEW` | `true` | Renew managed certificates automatically | +| `ACME_RENEW_CHECK_SECONDS` | `900` | Interval for ACME renewal checks; minimum 60 seconds | | `WEB_JWT_TTL_SECONDS` | `28800` | Session lifetime, 5 minutes to 7 days | | `WEB_LOGIN_ATTEMPTS_PER_5_MIN` | `10` | Per-address login attempt limit | | `WEB_USAGE_SCAN_INTERVAL_SECONDS` | `900` | Recursive storage scan interval | diff --git a/app/init.sh b/app/init.sh index 390d3fb..f67d282 100755 --- a/app/init.sh +++ b/app/init.sh @@ -303,7 +303,8 @@ web_should_start() { env_is_true "$WEB_ENABLED" return fi - if [[ -n "${STEP_CA_URL:-}" ]] || \ + if [[ -n "${ACME_CA_SERVER:-}" ]] || \ + [[ -n "${TLS_CA_URL:-}" ]] || \ [[ -s "${WEB_TLS_CERT_FILE:-/state/tls/web.crt}" ]]; then return 0 fi @@ -328,17 +329,57 @@ ensure_web_jwt_secret() { log 'WEB_JWT_SECRET was not provided; using a persistent generated secret.' } +selected_tls_mode() { + case "${WEB_TLS_MODE:-}" in + '') + if [[ -n "${ACME_CA_SERVER:-}" ]]; then + printf 'acme' + else + printf 'ca' + fi + ;; + acme|ca|files) printf '%s' "$WEB_TLS_MODE" ;; + *) return 1 ;; + esac +} + +request_acme_certificate() { + local cert_tmp="${WEB_TLS_CERT_FILE}.acme.tmp" + local key_tmp="${WEB_TLS_KEY_FILE}.acme.tmp" + + rm -f "$cert_tmp" "$key_tmp" + log "Requesting ACME HTTPS certificate for ${WEB_HOSTNAME}" + if ! step ca certificate "$WEB_HOSTNAME" "$cert_tmp" "$key_tmp" \ + --acme "$ACME_CA_SERVER" \ + --root "$ACME_CA_CERTIFICATES" \ + --standalone \ + --http-listen "${ACME_HTTP_LISTEN:-:80}" \ + --force; then + rm -f "$cert_tmp" "$key_tmp" + return 1 + fi + + chmod 0600 "$key_tmp" + chmod 0644 "$cert_tmp" + mv -f "$key_tmp" "$WEB_TLS_KEY_FILE" + mv -f "$cert_tmp" "$WEB_TLS_CERT_FILE" +} + configure_web_tls() { - local tls_mode="${WEB_TLS_MODE:-step}" - local tls_dir="" + local tls_mode="" local provisioner_password_file="" + if ! tls_mode="$(selected_tls_mode)"; then + printf '[init] ERROR: WEB_TLS_MODE must be acme, ca, or files\n' >&2 + return 1 + fi + export ACTIVE_WEB_TLS_MODE="$tls_mode" export WEB_HOSTNAME="${WEB_HOSTNAME:-${AD_DNS_NAME}}" export WEB_BIND_PORT="${WEB_BIND_PORT:-8443}" export WEB_TLS_CERT_FILE="${WEB_TLS_CERT_FILE:-/state/tls/web.crt}" export WEB_TLS_KEY_FILE="${WEB_TLS_KEY_FILE:-/state/tls/web.key}" - tls_dir="$(dirname "$WEB_TLS_CERT_FILE")" - mkdir -p "$tls_dir" "$(dirname "$WEB_TLS_KEY_FILE")" + export STEPPATH="${TLS_CA_STATE_DIR:-/state/tls-client}" + mkdir -p "$(dirname "$WEB_TLS_CERT_FILE")" "$(dirname "$WEB_TLS_KEY_FILE")" "$STEPPATH" if [[ "$tls_mode" == "files" ]]; then if [[ ! -s "$WEB_TLS_CERT_FILE" || ! -s "$WEB_TLS_KEY_FILE" ]]; then @@ -347,41 +388,49 @@ configure_web_tls() { fi return fi - if [[ "$tls_mode" != "step" ]]; then - printf '[init] ERROR: WEB_TLS_MODE must be step or files\n' >&2 - return 1 + + if [[ "$tls_mode" == "acme" ]]; then + require_env ACME_CA_SERVER + require_env ACME_CA_CERTIFICATES + if [[ ! -s "$ACME_CA_CERTIFICATES" ]]; then + printf '[init] ERROR: ACME_CA_CERTIFICATES is not a readable PEM bundle: %s\n' "$ACME_CA_CERTIFICATES" >&2 + return 1 + fi + if [[ ! -s "$WEB_TLS_CERT_FILE" || ! -s "$WEB_TLS_KEY_FILE" ]]; then + request_acme_certificate + fi + return fi - export STEPPATH="${STEP_PATH:-/state/step}" if [[ ! -s "$STEPPATH/config/defaults.json" ]] || \ - env_is_true "${STEP_CA_REBOOTSTRAP:-false}"; then - require_env STEP_CA_URL - require_env STEP_CA_FINGERPRINT - log "Bootstrapping Smallstep trust for ${STEP_CA_URL}" - step ca bootstrap --force --ca-url "$STEP_CA_URL" --fingerprint "$STEP_CA_FINGERPRINT" + env_is_true "${TLS_CA_REBOOTSTRAP:-false}"; then + require_env TLS_CA_URL + require_env TLS_CA_FINGERPRINT + log "Bootstrapping CA trust for ${TLS_CA_URL}" + step ca bootstrap --force --ca-url "$TLS_CA_URL" --fingerprint "$TLS_CA_FINGERPRINT" fi if [[ ! -s "$WEB_TLS_CERT_FILE" || ! -s "$WEB_TLS_KEY_FILE" ]]; then log "Requesting HTTPS certificate for ${WEB_HOSTNAME}" - if [[ -n "${STEP_CA_TOKEN:-}" ]]; then - step ca certificate "$WEB_HOSTNAME" "$WEB_TLS_CERT_FILE" "$WEB_TLS_KEY_FILE" --token "$STEP_CA_TOKEN" --force + if [[ -n "${TLS_CA_TOKEN:-}" ]]; then + step ca certificate "$WEB_HOSTNAME" "$WEB_TLS_CERT_FILE" "$WEB_TLS_KEY_FILE" --token "$TLS_CA_TOKEN" --force else - require_env STEP_CA_PROVISIONER - if [[ -n "${STEP_CA_PROVISIONER_PASSWORD_FILE:-}" ]]; then - provisioner_password_file="$STEP_CA_PROVISIONER_PASSWORD_FILE" - elif [[ -n "${STEP_CA_PROVISIONER_PASSWORD:-}" ]]; then - provisioner_password_file="/run/step-provisioner-password" + require_env TLS_CA_PROVISIONER + if [[ -n "${TLS_CA_PROVISIONER_PASSWORD_FILE:-}" ]]; then + provisioner_password_file="$TLS_CA_PROVISIONER_PASSWORD_FILE" + elif [[ -n "${TLS_CA_PROVISIONER_PASSWORD:-}" ]]; then + provisioner_password_file="/run/tls-ca-provisioner-password" umask 077 - printf '%s\n' "$STEP_CA_PROVISIONER_PASSWORD" > "$provisioner_password_file" + printf '%s\n' "$TLS_CA_PROVISIONER_PASSWORD" > "$provisioner_password_file" else - printf '[init] ERROR: STEP_CA_TOKEN, STEP_CA_PROVISIONER_PASSWORD_FILE, or STEP_CA_PROVISIONER_PASSWORD is required for initial TLS setup\n' >&2 + printf '[init] ERROR: TLS_CA_TOKEN, TLS_CA_PROVISIONER_PASSWORD_FILE, or TLS_CA_PROVISIONER_PASSWORD is required for initial TLS setup\n' >&2 return 1 fi step ca certificate "$WEB_HOSTNAME" "$WEB_TLS_CERT_FILE" "$WEB_TLS_KEY_FILE" \ - --provisioner "$STEP_CA_PROVISIONER" \ + --provisioner "$TLS_CA_PROVISIONER" \ --provisioner-password-file "$provisioner_password_file" \ --force - if [[ "$provisioner_password_file" == "/run/step-provisioner-password" ]]; then + if [[ "$provisioner_password_file" == "/run/tls-ca-provisioner-password" ]]; then rm -f "$provisioner_password_file" fi fi @@ -390,6 +439,24 @@ configure_web_tls() { chmod 0644 "$WEB_TLS_CERT_FILE" } +start_acme_renewal_daemon() { + local interval="${ACME_RENEW_CHECK_SECONDS:-900}" + if [[ ! "$interval" =~ ^[0-9]+$ ]] || (( interval < 60 )); then + log "Invalid ACME_RENEW_CHECK_SECONDS=${interval}; using 900" + interval=900 + fi + + log "Starting ACME certificate renewal check every ${interval} seconds" + ( + while true; do + sleep "$interval" + if step certificate needs-renewal "$WEB_TLS_CERT_FILE" >/dev/null 2>&1; then + request_acme_certificate || log 'ACME certificate renewal failed; retrying at the next interval.' + fi + done + ) & +} + start_observability_services() { log 'Starting Samba audit collector' python3 /app/audit_collector.py & @@ -397,10 +464,15 @@ start_observability_services() { if web_should_start; then ensure_web_jwt_secret configure_web_tls - unset STEP_CA_PROVISIONER_PASSWORD STEP_CA_TOKEN - if [[ "${WEB_TLS_MODE:-step}" == "step" ]] && env_is_true "${WEB_TLS_AUTORENEW:-true}"; then - log 'Starting Smallstep certificate renewal daemon' - step ca renew --daemon --force "$WEB_TLS_CERT_FILE" "$WEB_TLS_KEY_FILE" & + unset TLS_CA_PROVISIONER_PASSWORD TLS_CA_TOKEN + if env_is_true "${WEB_TLS_AUTORENEW:-true}"; then + case "$ACTIVE_WEB_TLS_MODE" in + acme) start_acme_renewal_daemon ;; + ca) + log 'Starting CA certificate renewal daemon' + step ca renew --daemon --force "$WEB_TLS_CERT_FILE" "$WEB_TLS_KEY_FILE" & + ;; + esac fi log "Starting read-only web UI for https://${WEB_HOSTNAME}" python3 /app/web_ui.py & diff --git a/dev/e2e.py b/dev/e2e.py index 6734955..a4c0e29 100755 --- a/dev/e2e.py +++ b/dev/e2e.py @@ -30,8 +30,23 @@ DOMAIN_SID = os.environ["PREVIEW_DOMAIN_SID"] ADMIN_USER = os.environ["PREVIEW_ADMIN_USER"] ADMIN_PASSWORD = os.environ["PREVIEW_ADMIN_PASSWORD"] USER_PASSWORD = os.environ["PREVIEW_USER_PASSWORD"] -BASE_URL = f"https://localhost:{HTTPS_PORT}" +HTTPS_HOST = os.environ["PREVIEW_HTTPS_HOST"] +BASE_URL = f"https://{HTTPS_HOST}:{HTTPS_PORT}" +_ORIGINAL_GETADDRINFO = socket.getaddrinfo + + +def preview_getaddrinfo(host, port, *args, **kwargs): + if host == HTTPS_HOST: + host = "127.0.0.1" + return _ORIGINAL_GETADDRINFO(host, port, *args, **kwargs) + + +socket.getaddrinfo = preview_getaddrinfo TLS_CONTEXT = ssl.create_default_context(cafile=CA_ROOT) +HTTP_OPENER = urllib.request.build_opener( + urllib.request.ProxyHandler({}), + urllib.request.HTTPSHandler(context=TLS_CONTEXT), +) @dataclass @@ -85,9 +100,7 @@ def http( f"{BASE_URL}{path}", data=body, headers=headers, method=method ) try: - with urllib.request.urlopen( - request, context=TLS_CONTEXT, timeout=30 - ) as response: + with HTTP_OPENER.open(request, timeout=30) as response: return Response(response.status, response.headers, response.read()) except urllib.error.HTTPError as exc: return Response(exc.code, exc.headers, exc.read()) @@ -152,10 +165,10 @@ def main() -> int: check("max-age=" in index.headers.get("Strict-Transport-Security", ""), "HSTS missing") check("default-src 'self'" in index.headers.get("Content-Security-Policy", ""), "CSP missing") with socket.create_connection(("localhost", HTTPS_PORT), timeout=10) as raw: - with TLS_CONTEXT.wrap_socket(raw, server_hostname="localhost") as secured: + with TLS_CONTEXT.wrap_socket(raw, server_hostname=HTTPS_HOST) as secured: certificate = secured.getpeercert() sans = {value for kind, value in certificate.get("subjectAltName", ()) if kind == "DNS"} - check("localhost" in sans, "issued certificate does not cover localhost") + check(HTTPS_HOST in sans, f"issued certificate does not cover {HTTPS_HOST}") check(certificate.get("issuer") != certificate.get("subject"), "web certificate is self-signed instead of CA-issued") announce("JWT boundary, real Kerberos credentials, and Domain Admin authorization") diff --git a/docker-compose.yml b/docker-compose.yml index 0b7c901..7ca25d9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,6 +13,7 @@ services: ports: - "445:445" - "139:139" + - "${ACME_HTTP_PORT:-80}:80" - "${WEB_HTTPS_PORT:-443}:${WEB_BIND_PORT:-8443}" volumes: - private_data:/data/private diff --git a/scripts/dev b/scripts/dev index 7d95b22..9f9d74b 100755 --- a/scripts/dev +++ b/scripts/dev @@ -34,6 +34,7 @@ dev_realm=${DEV_REALM:-DEV.TEST} dev_workgroup=${DEV_WORKGROUP:-DEV} dev_dns_domain=${DEV_DNS_DOMAIN:-dev.test} dev_base_dn=${DEV_BASE_DN:-DC=dev,DC=test} +dev_web_hostname=${DEV_WEB_HOSTNAME:-files.localhost} dev_dc_password=${DEV_DC_PASSWORD:-PreviewDc123!} dev_user_password=${DEV_USER_PASSWORD:-PreviewUser123!} dev_admin_user=${DEV_ADMIN_USER:-previewadmin} @@ -49,7 +50,7 @@ dev_backup_interval=${DEV_BACKUP_INTERVAL_SECONDS:-120} server_image=${DEV_SERVER_IMAGE:-ad-ds-simple-file-server-dev:latest} ad_image=${DEV_AD_IMAGE:-ad-ds-simple-file-server-ad-dev:latest} backup_image=${DEV_BACKUP_IMAGE:-ad-ds-simple-file-server-backup-dev:latest} -step_ca_image=${DEV_STEP_CA_IMAGE:-docker.io/smallstep/step-ca:latest} +ca_image=${DEV_CA_IMAGE:-docker.io/smallstep/step-ca:latest} run_id="ad-file-server-dev-$$-$RANDOM" network_name="$run_id-net" @@ -216,18 +217,20 @@ for volume in "${volumes[@]}"; do "$engine" volume create "$volume" >/dev/null done -printf 'starting local CA: %s\n' "$step_ca_image" +printf 'starting local CA: %s\n' "$ca_image" "$engine" run -d \ --name "$ca_container" \ --hostname "ca.${dev_dns_domain}" \ --network "$network_name" \ --ip "$ca_ip" \ + --add-host "${dev_web_hostname}:${files_ip}" \ -e "DOCKER_STEPCA_INIT_NAME=AD file server preview CA" \ -e "DOCKER_STEPCA_INIT_DNS_NAMES=ca.${dev_dns_domain},localhost,127.0.0.1" \ -e "DOCKER_STEPCA_INIT_PROVISIONER_NAME=${dev_ca_provisioner}" \ -e "DOCKER_STEPCA_INIT_PASSWORD=${dev_ca_password}" \ + -e "DOCKER_STEPCA_INIT_ACME=true" \ -v "$ca_volume:/home/step" \ - "$step_ca_image" >/dev/null + "$ca_image" >/dev/null if ! wait_for_exec "$ca_container" 120 step ca health \ --ca-url=https://localhost:9000 \ @@ -235,11 +238,15 @@ if ! wait_for_exec "$ca_container" 120 step ca health \ show_logs die 'local CA did not become healthy' fi -ca_fingerprint=$("$engine" exec "$ca_container" step certificate fingerprint \ - /home/step/certs/root_ca.crt) preview_tmp_root=${TMPDIR:-/tmp} ca_root_file="${preview_tmp_root%/}/${run_id}-root-ca.crt" "$engine" exec "$ca_container" cat /home/step/certs/root_ca.crt > "$ca_root_file" +"$engine" run --rm -i \ + -v "$state_volume:/state" \ + --entrypoint /bin/bash \ + "$server_image" -lc \ + 'install -d -m 0755 /state/tls && dd of=/state/tls/acme-ca-certificates.pem status=none && chmod 0644 /state/tls/acme-ca-certificates.pem' \ + < "$ca_root_file" printf 'starting disposable rsync backup target\n' "$engine" run -d \ @@ -312,14 +319,13 @@ printf 'starting actual file server and HTTPS web UI\n' -e "NETBIOS_NAME=FILES" \ -e "AD_DNS_NAME=files.${dev_dns_domain}" \ -e "WEB_ENABLED=true" \ - -e "WEB_HOSTNAME=localhost" \ + -e "WEB_HOSTNAME=${dev_web_hostname}" \ -e "WEB_BIND_PORT=8443" \ -e "WEB_JWT_SECRET=preview-only-jwt-secret-with-at-least-32-bytes" \ - -e "WEB_TLS_MODE=step" \ - -e "STEP_CA_URL=https://ca.${dev_dns_domain}:9000" \ - -e "STEP_CA_FINGERPRINT=${ca_fingerprint}" \ - -e "STEP_CA_PROVISIONER=${dev_ca_provisioner}" \ - -e "STEP_CA_PROVISIONER_PASSWORD=${dev_ca_password}" \ + -e "WEB_TLS_MODE=acme" \ + -e "ACME_CA_SERVER=https://ca.${dev_dns_domain}:9000/acme/acme/directory" \ + -e "ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem" \ + -e "ACME_HTTP_LISTEN=:80" \ -e "WEB_USAGE_SCAN_INTERVAL_SECONDS=60" \ -e "WEB_DIRECTORY_CACHE_SECONDS=30" \ -e "AUDIT_POLL_SECONDS=0.25" \ @@ -387,6 +393,7 @@ if [[ $run_e2e == 1 ]]; then PREVIEW_BACKUP_CONTAINER="$backup_container" \ PREVIEW_CA_ROOT="$ca_root_file" \ PREVIEW_HTTPS_PORT="$dev_https_port" \ + PREVIEW_HTTPS_HOST="$dev_web_hostname" \ PREVIEW_REALM="$dev_realm" \ PREVIEW_WORKGROUP="$dev_workgroup" \ PREVIEW_DNS_DOMAIN="$dev_dns_domain" \ @@ -402,7 +409,7 @@ if [[ $run_e2e == 1 ]]; then fi printf '\nPreview ready\n' -printf ' URL: https://localhost:%s\n' "$dev_https_port" +printf ' URL: https://%s:%s\n' "$dev_web_hostname" "$dev_https_port" printf ' Username: %s\\%s\n' "$dev_workgroup" "$dev_admin_user" printf ' Password: %s\n' "$dev_admin_password" printf ' CA root: %s\n' "$ca_root_file" diff --git a/setup b/setup index 8c010bc..f4b1641 100755 --- a/setup +++ b/setup @@ -138,10 +138,11 @@ write_env_file() { local web_hostname="" local web_https_port="443" local web_jwt_secret="" - local step_ca_url="" - local step_ca_fingerprint="" - local step_ca_provisioner="" - local step_ca_provisioner_password="" + local acme_ca_server="" + local acme_ca_certificates_source="" + local acme_ca_certificates_source_dir="" + local acme_http_port="80" + local acme_http_port_input="" local web_hostname_input="" local service_account_sam="" local fslogix_group_prompt="" @@ -217,10 +218,16 @@ write_env_file() { web_hostname="${web_hostname_input:-$web_hostname}" read -r -p "WEB_HTTPS_PORT [443]: " web_https_port web_https_port="${web_https_port:-443}" - prompt_value step_ca_url "STEP_CA_URL (e.g. https://ca.example.com:9000)" - prompt_value step_ca_fingerprint "STEP_CA_FINGERPRINT" - prompt_value step_ca_provisioner "STEP_CA_PROVISIONER (JWK provisioner name)" - prompt_value step_ca_provisioner_password "STEP_CA_PROVISIONER_PASSWORD" true + prompt_value acme_ca_server "ACME_CA_SERVER (e.g. https://ca.internal/acme/acme/directory)" + prompt_value acme_ca_certificates_source "ACME CA root bundle (PEM file on this host)" + if [[ ! -s "$acme_ca_certificates_source" ]]; then + printf "ACME CA root bundle is not a readable file: %s\n" "$acme_ca_certificates_source" >&2 + return 1 + fi + acme_ca_certificates_source_dir="$(cd "$(dirname "$acme_ca_certificates_source")" && pwd)" + acme_ca_certificates_source="${acme_ca_certificates_source_dir}/$(basename "$acme_ca_certificates_source")" + read -r -p "ACME_HTTP_PORT (HTTP-01 host port) [80]: " acme_http_port_input + acme_http_port="${acme_http_port_input:-80}" web_jwt_secret="$(python3 - <<'PY' import secrets @@ -279,11 +286,11 @@ WEB_ENABLED=true WEB_HOSTNAME=${web_hostname} WEB_HTTPS_PORT=${web_https_port} WEB_JWT_SECRET=${web_jwt_secret} -WEB_TLS_MODE=step -STEP_CA_URL=${step_ca_url} -STEP_CA_FINGERPRINT=${step_ca_fingerprint} -STEP_CA_PROVISIONER=${step_ca_provisioner} -STEP_CA_PROVISIONER_PASSWORD=${step_ca_provisioner_password} +WEB_TLS_MODE=acme +ACME_CA_SERVER=${acme_ca_server} +ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem +ACME_HTTP_LISTEN=:80 +ACME_HTTP_PORT=${acme_http_port} EOF cp "$BOOTSTRAP_ENV_FILE" "$ENV_FILE" @@ -293,6 +300,12 @@ EOF printf "Building image...\n" docker compose build samba + printf "Importing ACME CA root bundle...\n" + docker compose run --rm --no-deps -T \ + --entrypoint /bin/bash samba -lc \ + 'install -d -m 0755 "$(dirname "$ACME_CA_CERTIFICATES")" && dd of="$ACME_CA_CERTIFICATES" status=none && chmod 0644 "$ACME_CA_CERTIFICATES"' \ + < "$acme_ca_certificates_source" + printf "Provisioning service account %s...\n" "$SERVICE_ACCOUNT_NAME" docker compose run --rm --entrypoint /bin/bash samba -lc ' set -euo pipefail @@ -348,11 +361,11 @@ WEB_ENABLED=true WEB_HOSTNAME=${web_hostname} WEB_HTTPS_PORT=${web_https_port} WEB_JWT_SECRET=${web_jwt_secret} -WEB_TLS_MODE=step -STEP_CA_URL=${step_ca_url} -STEP_CA_FINGERPRINT=${step_ca_fingerprint} -STEP_CA_PROVISIONER=${step_ca_provisioner} -STEP_CA_PROVISIONER_PASSWORD=${step_ca_provisioner_password} +WEB_TLS_MODE=acme +ACME_CA_SERVER=${acme_ca_server} +ACME_CA_CERTIFICATES=/state/tls/acme-ca-certificates.pem +ACME_HTTP_LISTEN=:80 +ACME_HTTP_PORT=${acme_http_port} # Optional overrides: # LDAP_URI=ldaps://${domain} # LDAP_BASE_DN=DC=example,DC=com @@ -375,7 +388,7 @@ STEP_CA_PROVISIONER_PASSWORD=${step_ca_provisioner_password} # WEB_DIRECTORY_CACHE_SECONDS=300 # AUDIT_COMPRESS_AFTER_HOURS=24 # AUDIT_QUERY_MAX_DAYS=31 -# STEP_CA_PROVISIONER_PASSWORD_FILE=/run/secrets/step-ca-provisioner-password +# ACME_RENEW_CHECK_SECONDS=900 # WEB_TLS_CERT_FILE=/state/tls/web.crt # WEB_TLS_KEY_FILE=/state/tls/web.key EOF