server/ldap_auth.py: simple bind to ldaps://prime.local:636 as sAMAccountName@prime.local, nested-group membership via the LDAP_MATCHING_RULE_IN_CHAIN extensible match, and a selftest() that validates the DC certificate without binding so it can never contribute to a lockout. Verified against the live domain, not just reasoned about: selftest() to prime.local -> ok, "certificate validates" selftest() to 192.168.3.37 -> refused, untrusted (no IP SAN) empty / whitespace password -> empty_input, with Connection nulled out so any call to bind() would have raised missing CA file -> unconfigured, is_config_problem=True Tls.validate -> ssl.CERT_REQUIRED, explicit ca_certs_file Three things here are load-bearing and commented as such at the call site: - The empty-password guard runs BEFORE bind(). An LDAP simple bind with an empty password is an anonymous bind and it SUCCEEDS, so without the guard a blank password authenticates as whatever username was submitted. - No `version=` pin on Tls. An earlier draft of this file pinned PROTOCOL_TLSv1_2, which would have silently downgraded every connection from the TLS 1.3 these DCs actually negotiate. - Retries cover connect failures only. A rejected credential returns immediately, because every failed bind counts against the domain lockout policy and this endpoint must not become a way to lock people out of Windows. The trust anchor is server/certs/prime-ca-chain.pem - PRIME CONTROLS ROOT CA plus ISSUING CA 1, public certificates with no private key, checked in because they are public and long-lived (2051 / 2036). The system trust store is deliberately not used: it currently trusts five other self-signed CAs on this estate. LDAP_CA_FILE overrides the path for a mounted bundle. docker-compose.yml: the `outbound` network is no longer optional. Its comment said to detach it if you were not using the Micron asset picker; doing that now breaks every sign-in, since `internal` has no default gateway and therefore no route to prime.local:636. Not yet verified, and called out rather than assumed: the nested-group case needs a real group with a nested member, and the in-container `openssl s_client -CAfile` check needs the stack. Both are T10.1 done-when boxes still open. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
89 lines
5.1 KiB
Plaintext
89 lines
5.1 KiB
Plaintext
# Copy to .env (dev) or set these in the systemd unit (prod).
|
|
|
|
# PostgreSQL connection (production). Format:
|
|
# postgresql+psycopg://USER:PASSWORD@HOST:5432/DBNAME
|
|
DATABASE_URL=postgresql+psycopg://wpsuite:CHANGE_ME@localhost:5432/wpsuite
|
|
|
|
# If DATABASE_URL is omitted entirely, the API falls back to a local SQLite
|
|
# file (sqlite:///./wpsuite.db) — handy for trying it out without Postgres.
|
|
|
|
# Only needed for CROSS-ORIGIN local development (comma-separated). In
|
|
# production the site is same-origin via NGINX, so leave this unset.
|
|
# CORS_ORIGINS=http://localhost:5500
|
|
|
|
# ── Authentication ────────────────────────────────────────────────────────────
|
|
# Secret used to sign session cookies (JWTs). REQUIRED in production: if unset,
|
|
# the API falls back to a random per-process key, so logins reset on every
|
|
# restart and break across multiple gunicorn workers. Generate a strong one:
|
|
# python -c "import secrets; print(secrets.token_urlsafe(48))"
|
|
AUTH_SECRET_KEY=CHANGE_ME_run_the_command_above
|
|
|
|
# How long a login lasts before re-authentication (hours). Default 12.
|
|
# AUTH_SESSION_HOURS=12
|
|
|
|
# ── Domain authentication, D13 (REQUIRED — there is no fallback) ───────────────
|
|
# The suite stores no passwords. Sign-in is an LDAPS simple bind against the
|
|
# domain, so if this is misconfigured NOBODY CAN SIGN IN, admins included. There
|
|
# is deliberately no local break-glass account (decided Aug 21 2026 — see
|
|
# docs/waves/decisions-2026-08-21.md). Check `docker compose logs api` on startup:
|
|
# the API logs one line saying whether LDAP is configured and reachable.
|
|
#
|
|
# Connect to the DOMAIN NAME, never a DC hostname and never an IP. Every DC's
|
|
# certificate carries `prime.local` in its SAN, so the domain name both passes
|
|
# hostname validation and round-robins across all six DCs. An IP fails with
|
|
# `hostname mismatch` (there is no IP SAN) and the only way to force it through is
|
|
# to disable validation, which must not happen — domain passwords cross this link.
|
|
# LDAP_DOMAIN=prime.local
|
|
# LDAP_HOST=prime.local
|
|
# LDAP_PORT=636
|
|
|
|
# Trust anchor: PRIME CONTROLS ROOT CA + PRIME CONTROLS ISSUING CA 1 as a PEM
|
|
# bundle. These are PUBLIC certificates — no private key, nothing issued to this
|
|
# app, nothing to request from IT. The repo ships a verified copy and the default
|
|
# points at it, so you only set this to override with a mounted file.
|
|
# LDAP_CA_FILE=/app/server/certs/prime-ca-chain.pem
|
|
|
|
# An AD group required to sign in. Empty means every domain account may sign in.
|
|
# This is the INITIAL value and the fallback; the live value is set in the Admin
|
|
# console, which refuses to save a group that does not resolve or that the saving
|
|
# admin is not a member of. Nested groups count.
|
|
# LDAP_REQUIRED_GROUP=WP-Suite-Users
|
|
|
|
# Bind/connect timeout, and how many extra CONNECT attempts to make. Retries never
|
|
# apply to a rejected password — each failed bind counts against the domain lockout
|
|
# policy, so guessing would lock real accounts out of Windows.
|
|
# LDAP_TIMEOUT_SECONDS=8
|
|
# LDAP_CONNECT_RETRIES=2
|
|
|
|
# ── Email notifications (optional) ─────────────────────────────────────────────
|
|
# WP-assignment emails are OFF by default and are turned on from the Admin
|
|
# console (Notifications & email card), where the SMTP host/port/from-address
|
|
# live. The one secret that must NOT be stored in the database — the SMTP
|
|
# password — is read from this environment variable instead. Leave it unset
|
|
# until you have the SMTP details; the toggle stays effectively off (queued
|
|
# notifications are marked "skipped", nothing is sent) until both the toggle is
|
|
# on and SMTP is configured.
|
|
# SMTP_PASSWORD=your-smtp-app-password
|
|
|
|
# ── Micron asset catalog (optional) ───────────────────────────────────────────
|
|
# Backs the searchable asset picker in the work package creator. READ-ONLY: the
|
|
# app only ever runs the single SELECT in server/assets_db.py, so give it a
|
|
# db_datareader login and nothing more.
|
|
#
|
|
# Leave this unset and the suite works normally — the picker reports that no
|
|
# catalog is configured and people type asset tags in by hand.
|
|
#
|
|
# URL-encode special characters in the password (@ = %40, # = %23, / = %2F …).
|
|
# MICRON_DB_URL=mssql+pymssql://readonly_user:PASSWORD@sqlhost.example.com:1433/MicronDB
|
|
#
|
|
# To use pyodbc instead of pymssql you must also add pyodbc to requirements.txt
|
|
# and install the Microsoft ODBC driver in the image:
|
|
# MICRON_DB_URL=mssql+pyodbc://readonly_user:PASSWORD@sqlhost.example.com/MicronDB?driver=ODBC+Driver+18+for+SQL+Server
|
|
#
|
|
# Two things to check when the picker says the catalog is unreachable:
|
|
# 1. The table/column names in ASSET_QUERY (server/assets_db.py) match the real
|
|
# Micron schema — that one constant is the whole schema contract.
|
|
# 2. The api container is on the `outbound` network in docker-compose.yml. The
|
|
# `internal` network has no default gateway, which blocks the VPN as well as
|
|
# the internet.
|