# Setup The portal runs on Linux (bare metal, VM, or container). It needs outbound HTTPS to the IdP, LDAPS to a domain controller, and HTTPS to AD CS Web Enrollment. ## 1. Configuration Copy [`.env.example`](../.env.example) to `.env`. Every variable is commented there. Minimum: - `SESSION_SECRET` — long random string - `PUBLIC_BASE_URL` — the URL browsers use (the reverse-proxy origin) - `OIDC_*` — issuer, client id/secret, redirect `https:///auth/callback` - `LDAP_*` — `ldaps://` URL, bind DN/password, base DN - `CERTSRV_*` — `https://ca.example.com/certsrv`, NTLM user, template common name - `PROFILE_SIGNING_P12_PATH` / `PROFILE_SIGNING_P12_PASSWORD` Set `SESSION_HTTPS_ONLY=true` behind HTTPS. For a local `http://127.0.0.1:8000` lab only, set it false and use matching `OIDC_REDIRECT_URI`. ## 2. OpenID Connect client Register a confidential web app at your IdP: - Redirect URI: `{PUBLIC_BASE_URL}/auth/callback` - Scopes: `openid email profile` (override with `OIDC_SCOPES`) - PKCE: S256 (the portal always sends a code challenge) `OIDC_ISSUER` must be the discovery origin (no trailing slash). The portal loads `{issuer}/.well-known/openid-configuration`. `OIDC_USERNAME_CLAIM` (default `email`) is what LDAP searches for. If your IdP puts the AD UPN in `preferred_username` or `upn`, set the claim accordingly and keep `LDAP_USER_FILTER` in sync. ## 3. LDAP Use **LDAPS** (`ldaps://dc:636`) in production. The bind account only needs to read users (and, if `LDAP_REQUIRED_GROUP` is set, group membership). Default filter: ``` (&(objectClass=user)(|(userPrincipalName={username})(mail={username}))) ``` `LDAP_REQUIRED_GROUP` may be a full DN or a CN. Nested members are included (`LDAP_MATCHING_RULE_IN_CHAIN`). If the DC uses a private CA, set `LDAP_CA_CERTS_FILE` to a PEM bundle. `LDAP_VERIFY_TLS=false` is lab-only. ## 4. AD CS Web Enrollment See [adcs.md](adcs.md). From the Linux host you should be able to reach `CERTSRV_URL` over HTTPS. The portal uses NTLM (`CERTSRV_DOMAIN` + `CERTSRV_USERNAME`). If `CERTSRV_USERNAME` is already `DOMAIN\user` or `user@upn`, the domain field is ignored. Install the issuing CA TLS cert into `CERTSRV_CA_CERTS_FILE` (or the system trust store). Do not disable `CERTSRV_VERIFY_TLS` except in a lab. ## 5. Profile signing material Place a PKCS#12 on the host (for Docker, bind-mount it) and point `PROFILE_SIGNING_P12_PATH` at it. See [profile-signing.md](profile-signing.md). Optional: `PROFILE_CA_CERT_PATH` PEM/DER of the issuing CA to embed so devices that do not already trust it still accept the user cert. ## 6. Reverse proxy / TLS Terminate TLS at Caddy, nginx, or another proxy. Forward `Host`, `X-Forwarded-For`, and `X-Forwarded-Proto`. The app is started with `proxy_headers=True`. Example Caddy: ``` certs.example.com { reverse_proxy 127.0.0.1:8000 } ``` ## 7. Run **Local** ```bash python3.12 -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install -e ".[dev]" python -m cert_portal ``` Listens on `HOST`/`PORT` (default `0.0.0.0:8000`). **Docker** ```bash docker build -t cert-portal . docker run --rm -p 8000:8000 --env-file .env \ -v /etc/cert-portal:/etc/cert-portal:ro \ cert-portal ``` The image runs as UID 1000. The signing P12 path inside the container must match `PROFILE_SIGNING_P12_PATH`. CI on this Gitea publishes `git.atlashorizon.net/nick/cert-portal` (see [gitea.md](gitea.md)): ```bash docker login git.atlashorizon.net docker pull git.atlashorizon.net/nick/cert-portal:latest docker run --rm -p 8000:8000 --env-file .env \ -v /etc/cert-portal:/etc/cert-portal:ro \ git.atlashorizon.net/nick/cert-portal:latest ``` ## 8. OpenAPI `/docs`, `/redoc`, and `/openapi.json` are **off** when `ENVIRONMENT=production` unless you set `DOCS_ENABLED=true`. They are on in `development`. Do not expose Swagger to the internet without additional access control. ## Health `GET /health` returns `{"status":"ok","version":"..."}` and does not require a session. Point your load balancer at it.