AD SSO Preview

Status: Preview. LDAP/LDAPS und Windows Negotiate sind implementiert, gelten aber erst nach realen AD-, Kerberos-, LDAPS- und NTLM-Ablehnungstests über die Produktions-Topologie als Enterprise-ready. OIDC und SCIM haben ein separates Release-Gate.

Diese Seite beschreibt zentrale Anmeldung und Provisionierung. Alle externen Anmeldewege sind standardmäßig deaktiviert. Ein lokales Break-Glass-Admin-Konto bleibt für Notfälle erforderlich.

Anmeldewege

PfadEndpointDefaultZweck
Lokales Passwort (BCrypt)POST /api/auth/loginBreakGlassOnlyexplizit markierte Notfallkonten
LDAP Simple Bind über LDAPSPOST /api/auth/loginausDomänen-Login mit Benutzername und Passwort
Windows Negotiate/KerberosPOST /api/auth/windowsausIntegriertes SSO für domänengebundene Windows-Clients — im Browser und über np auth login --windows
OpenID ConnectGET /api/auth/oidcaus, release-gatedallgemeiner Enterprise-IdP-Pfad mit Authorization Code + PKCE

Alle Wege erzeugen dieselbe serverseitig widerrufbare Session. Das JWT-Cookie enthält keine Gruppenliste; die absolute Session-Lebensdauer beträgt standardmäßig acht Stunden.

Einrichtung in einer Einzeldomäne

Der kurze Weg für eine Domäne. Domänenbenutzer werden nie importiert — sie entstehen bei der ersten erfolgreichen Anmeldung, sofern sie einer erlaubten Gruppe angehören.

LDAPS ist Voraussetzung. NodePilot spricht ausschließlich LDAPS auf Port 636 mit vollständiger Zertifikatsprüfung gegen den Windows-Zertifikatsspeicher des NodePilot-Hosts; einen Bypass-Schalter gibt es nicht. Hat die Domäne noch keine CA, ist eine Enterprise Root CA der Weg mit dem geringsten Aufwand: Der DC stellt sich sein Zertifikat dann selbst aus, und die Gruppenrichtlinie verteilt das Stammzertifikat an jedes Domänenmitglied — der NodePilot-Host vertraut ihm ohne manuellen Import.

# Auf dem Domänencontroller, einmalig:
Install-WindowsFeature AD-Certificate -IncludeManagementTools
Install-AdcsCertificationAuthority -CAType EnterpriseRootCa -CACommonName 'corp-CA' -Force

Zwei Gruppen, ein Dienstkonto. Zugang und Rolle sind getrennte Begriffe: Die Mitgliedschaft in einer erlaubten Gruppe entscheidet, ob sich jemand anmelden darf, eine Rollenzuordnung entscheidet, was er ist.

New-ADGroup -Name 'NodePilot-Users'  -GroupScope Global -GroupCategory Security
New-ADGroup -Name 'NodePilot-Admins' -GroupScope Global -GroupCategory Security
Add-ADGroupMember -Identity 'NodePilot-Users'  -Members <user>
Add-ADGroupMember -Identity 'NodePilot-Admins' -Members <user>

# SIDs auslesen. Get-ADGroup nimmt GENAU EINE Identität — deshalb per Pipe, nicht als Liste:
'NodePilot-Users','NodePilot-Admins' | Get-ADGroup |
    Select-Object Name, @{n='SID';e={$_.SID.Value}}

# Nur-Lese-Dienstkonto für die autoritativen Abfragen:
New-ADUser -Name 'svc-nodepilot-dir' -AccountPassword (Read-Host -AsSecureString) `
    -Enabled $true -PasswordNeverExpires $true
Get-ADUser 'svc-nodepilot-dir' | Select-Object -ExpandProperty DistinguishedName

Anschließend unter Admin-Einstellungen → Authentication: LDAP aktivieren, einen Endpunkt angeben (dc1.corp.example.com:636), dazu BaseDn, UpnSuffix, Service-Bind-DN samt Passwort, die SID von NodePilot-Users als erlaubte Gruppe und die SID von NodePilot-Admins auf die Rolle Admin gemappt. Der eingebaute Test prüft TLS-Vertrauen, Service-Bind, Search Base und Gruppenauflösung gegen den ungespeicherten Entwurf, vor der Übernahme.

Genau einen Endpunkt konfigurieren, solange nicht jeder aufgeführte DC dauerhaft erreichbar ist. Der Verzeichniszugriff arbeitet mit All-DC-Konsens, nicht mit Failover — ein zweiter, zeitweise offline stehender DC blockiert damit jede AD-Anmeldung, statt für den ersten einzuspringen.

Angemeldet wird mit dem bloßen Benutzernamen (alice), nicht mit DOMAIN\alice; den UPN-Suffix hängt NodePilot selbst an. Eine fehlende Rollenzuordnung ist kein Fehler — der Benutzer entsteht als Viewer.

Sichere Basiskonfiguration

{
  "Authentication": {
    "LocalLoginMode": "BreakGlassOnly",
    "SessionAbsoluteLifetimeHours": 8,
    "MaxAuthorizationStalenessMinutes": 15,
    "Ldap": {
      "Enabled": false,
      "Endpoints": [
        "ldaps://dc01.firma.de:636",
        "ldaps://dc02.firma.de:636"
      ],
      "Port": 636,
      "UseSsl": true,
      "BaseDn": "DC=firma,DC=de",
      "UpnSuffix": "firma.de",
      "BindTimeoutSeconds": 5,
      "ServiceBindDn": "CN=svc-nodepilot-ldap,OU=Services,DC=firma,DC=de",
      "ServicePassword": "<secret>",
      "AllowedGroupSids": [
        "S-1-5-21-...-512",
        "S-1-5-21-...-1108"
      ],
      "DirectorySyncIntervalMinutes": 5,
      "DirectorySyncMaxConcurrency": 16,
      "GlobalRoleMappings": [
        { "GroupSid": "S-1-5-21-...-512", "Role": "Admin" },
        { "GroupSid": "S-1-5-21-...-1108", "Role": "Operator" }
      ],
      "JitUserDefaultRootRole": null
    },
    "Windows": {
      "Enabled": false,
      "AllowNtlmFallback": false,
      "NtlmDisabledByPolicy": false
    },
    "Oidc": {
      "Enabled": false,
      "Authority": "https://idp.example.com/tenant/v2.0",
      "ClientId": "<client-id>",
      "ClientSecret": "<secret>",
      "DisplayName": "Firmenkonto",
      "NameClaimType": "preferred_username",
      "GroupsClaimType": "groups",
      "Scopes": ["openid", "profile", "email"],
      "AllowedGroupIds": ["<admin-group-object-id>", "<operator-group-object-id>"],
      "GlobalRoleMappings": [
        { "GroupId": "<admin-group-object-id>", "Role": "Admin" },
        { "GroupId": "<operator-group-object-id>", "Role": "Operator" }
      ]
    },
    "Scim": {
      "Enabled": false,
      "Authority": "https://idp.example.com/tenant/v2.0",
      "BearerToken": "<random-secret-with-at-least-32-characters>",
      "PreviousBearerToken": null
    }
  }
}
  • Bei aktiviertem LDAP oder Windows-SSO sind mindestens ein LDAPS-Endpunkt, BaseDn, Service-Bind-Credentials und eine AllowedGroupSids-SID erforderlich; LDAP-Passwortlogin benötigt zusätzlich UpnSuffix.
  • LDAPS mit vollständiger Zertifikatsprüfung ist verpflichtend: Das DC-Zertifikat muss gegen den Windows-Zertifikatsspeicher des API-Hosts validieren, einen In-App-Bypass gibt es nicht. Plaintext-LDAP und StartTLS auf Port 389 sind kein unterstützter Enterprise-Pfad. LDAP-Referrals werden nie verfolgt — jede Abfrage wird vom bewusst konfigurierten Endpunkt beantwortet.
  • AllowedGroupSids ist die Zugangs-Policy; GlobalRoleMappings bestimmt davon unabhängig die globale Rolle. Ohne Rollen-Match gilt Viewer.
  • DirectorySyncIntervalMinutes liegt zwischen 1 und 5 Minuten. DirectorySyncMaxConcurrency begrenzt einen Sync-Pass auf 1–32 parallele Service-Bind-Lookups, Default 16.
  • Windows-SSO ist Kerberos-only. AllowNtlmFallback=true wird abgelehnt; NtlmDisabledByPolicy=true bestätigt die zusätzlich umgesetzte Host-/Domain-Policy.
  • Secrets gehören in Umgebungsvariablen oder den Secret-Provider, nicht in eine eingecheckte Datei.

Die komplette Authentication-Sektion ist boot-fest. Gespeicherte Änderungen werden erst nach einem Service-Neustart aktiv. Der LDAP-Verbindungstest in den Admin-Einstellungen prüft den noch nicht gespeicherten Entwurf gegen TLS-Vertrauen, Service-Bind und Search Base.

Im Cluster ist Authentication Config-as-Code. PUT /api/admin/settings/Authentication liefert 409 CLUSTER_CONFIG_AS_CODE_REQUIRED; identische Konfiguration und Secrets müssen auf jeden Node ausgerollt und durch einen Cluster-Neustart aktiviert werden.

HA mit OIDC

OIDC-Correlation, Nonce und serverseitige Tickets werden mit ASP.NET Core Data Protection geschützt. Jeder HA-Node benötigt deshalb denselben persistenten Keyring und dasselbe Zertifikat mit Private Key:

{
  "DataProtection": {
    "KeyRingPath": "\\\\fileserver\\nodepilot\\data-protection-keys",
    "CertificateThumbprint": "<shared-certificate-thumbprint>",
    "SharedKeyRing": true
  }
}

KeyRingPath muss für alle Nodes auf denselben persistenten Storage zeigen. Das Zertifikat muss auf jedem Node in LocalMachine\My liegen; es kann vom Kestrel-TLS-Zertifikat getrennt sein. HA+OIDC startet ohne diese drei expliziten Angaben nicht.

Identität, Gruppen und Offboarding

  • Externe Identitäten werden über das unveränderliche Paar (Authority, Subject) gefunden.
  • Benutzer werden über das Attribut userPrincipalName unterhalb der BaseDn gesucht. Ein erfolgreicher Bind genügt nicht: Ist das UPN-Attribut am Konto leer, akzeptiert Active Directory den Bind trotzdem über die implizite Form samAccountName@DNS-Domäne, es existiert aber kein auffindbares Objekt. Der Login wird dann mit dem eigenen Audit-Grund ldap_user_object_not_found abgelehnt — als Konto-Problem, nicht als Directory-Ausfall: Der Circuit Breaker bleibt unberührt, ein einzelnes fehlkonfiguriertes Konto kann also nicht die LDAP-Anmeldung für alle blockieren. Behebung: Set-ADUser <user> -UserPrincipalName '<user>@<upn-suffix>'.
  • LDAP und Windows verwenden dieselbe AD-Authority und den Benutzer-objectSid als Subject. Beide Wege landen daher bei derselben Person auf demselben NodePilot-Benutzer.
  • Windows verwendet aus dem Kerberos-Principal nur die primäre SID. Bei jedem Windows-Login lädt ein Service-Bind über LDAPS den aktuellen, autoritativen Benutzer- und Gruppen-Snapshot; möglicherweise alte PAC-Gruppen werden nicht vertraut. Ist der Directory-Lookup nicht möglich, schlägt der Login geschlossen fehl.
  • OIDC verwendet den validierten Issuer als Authority und sub als Subject.
  • Gleichnamige bestehende Benutzer werden nicht automatisch zusammengeführt. Kollisionen werden abgewiesen und auditiert.
  • Gruppen liegen als serverseitige Membership-Snapshots vor und werden nicht in JWT oder Cookie geschrieben. Folder-Gruppenrechte nutzen diese Snapshots.
  • Folder-Gruppenrechte sind mit PrincipalAuthority + PrincipalKey namespaced. AD-SIDs und gleichlautende OIDC-/SCIM-Gruppen-IDs können dadurch keine Rechte zwischen Providern erben.
  • Der AD-Sync läuft standardmäßig alle fünf Minuten. Ein Gruppenentzug, eine Deaktivierung oder ein Tombstone widerruft Sessions und blockiert auch geplante Jobs und Trigger spätestens nach der maximalen Autorisierungs-Staleness von 15 Minuten.
  • Eine lokal durch einen Admin gesetzte Deaktivierung ist sticky: weder ein gesunder AD-Snapshot noch SCIM active=true reaktiviert das Konto automatisch. Dafür ist die explizite Admin-Reaktivierung erforderlich.
  • Gelöschte externe Identitäten bleiben als Tombstone erhalten und können nur explizit durch einen Admin reaktiviert werden.
  • Ein All-Not-Found-Sync für sämtliche bekannten AD-Identitäten wird als fehlerhafte BaseDn/Search-Berechtigung verworfen und erzeugt keine Massen-Tombstones. Access bleibt nach dem Freshness-Limit trotzdem fail-closed.
  • Bei aktivem LDAP, Windows, OIDC oder SCIM startet eine bestehende Datenbank nur mit einem aktiven lokalen Break-Glass-Admin. Eine leere Installation bleibt für den einmaligen lokalen Bootstrap startfähig.

Kerberos und HAProxy

Für Windows-SSO muss der HTTP-SPN mit setspn -S auf der Dienstidentität registriert sein. Läuft NodePilot unter einem gMSA oder Domänenkonto, deckt das HOST/-SPN des Computerkontos den Dienst nicht ab — das Ticket ist dann für den Prozess unlesbar, Kerberos scheitert und SPNEGO fällt still auf NTLM zurück.

Zusätzlich müssen Browser die URL als Intranet-Ziel behandeln: AuthServerAllowlist für Edge und Chrome, dazu die Zuordnung zur Zone „Lokales Intranet" per GPO. Ein korrekt konfigurierter Client fragt nie nach Zugangsdaten — erscheint ein Anmeldedialog, ist die Policy nicht wirksam.

Wichtig für die Abnahme: ein gespeichertes Kennwort in der Windows-Anmeldeinformationsverwaltung oder ein Enterprise-SSO-Produkt, das Dialoge automatisch ausfüllt, lässt die Anmeldung nahtlos wirken, obwohl die Browser-Policy fehlt. Serverseitig sind beide Fälle nicht unterscheidbar. Der Nachweis ist deshalb nur auf einem Client ohne solche Werkzeuge und nach vollständigem Browser-Neustart aussagekräftig.

Vor HAProxy gelten zusätzlich:

  • HTTP/1.1 und persistente Verbindungen auf beiden Hops;
  • http-reuse never, damit Backend-Verbindungen nie zwischen Clients geteilt werden;
  • Source-Stickiness während des Negotiate-Handshakes;
  • validiertes Backend-TLS mit CA, SNI und Hostname-Prüfung;
  • vom Proxy gelöschte und neu gesetzte Forwarded Headers sowie eine enge ForwardedHeaders:KnownProxies-Liste.

Die vollständige Vorlage liegt unter deploy/templates/haproxy.cfg.template.

OIDC und SCIM 2.0 — separates Release-Gate

OIDC verwendet Authorization Code + PKCE, HTTPS-Metadata sowie Issuer-, Audience-, State- und Nonce-Prüfung. Der Zugriff benötigt mindestens eine konfigurierte Authentication:Oidc:AllowedGroupIds-Gruppe. Das Discovery-Endpoint meldet oidcEndpoint und den konfigurierten Anzeigenamen.

Gruppen aus einem OIDC-Token gelten nur, wenn sein iat vorhanden, höchstens eine Minute in der Zukunft und höchstens MaxAuthorizationStalenessMinutes alt ist — maximal 15 Minuten. Bei einem expliziten Group-Overage-Signal darf NodePilot stattdessen ausschließlich authority-scoped SCIM-Memberships verwenden, deren LastSeenAt ebenfalls innerhalb dieses Fensters liegt. Ein Login selbst verlängert die SCIM-Freshness nicht.

SCIM stellt ServiceProviderConfig, ResourceTypes, Schemas, /api/scim/v2/Users und /api/scim/v2/Groups bereit. Bearer-Tokens müssen 32–4096 Zeichen lang sein. Für eine unterbrechungsfreie Rotation bleibt der alte Wert kurzzeitig unter PreviousBearerToken, bis der IdP den neuen BearerToken verwendet; danach wird der alte Slot gelöscht. Authentication:Scim:Authority muss zur OIDC-Authority passen oder fällt auf diese zurück. Beim Anlegen eines Users ist externalId verpflichtend und muss exakt und case-sensitive dem OIDC-sub entsprechen; es ist danach unveränderlich.

Ein SCIM-User-Update bestätigt keine Gruppen und erneuert deshalb keine Autorisierungs-Freshness. Für Group-Overage muss der IdP mindestens alle 15 Minuten einen vollständigen Group-Snapshot beziehungsweise einen semantisch gleichwertigen Membership-Heartbeat liefern, der auch unveränderte Memberships aktualisiert. Dieses Verhalten ist Teil des SCIM-Release-Gates.

OIDC und SCIM bleiben bis zu realen Provider-, Parallel-JIT-, Group-Overage- und Offboarding-Tests release-gated. SAML ist nicht Teil dieses Zielbilds.

API und Betrieb

EndpointZweck
GET /api/auth/methodsaktive Loginwege inklusive OIDC-URL und Anzeigename
POST /api/auth/loginlokales Passwort oder LDAP
POST /api/auth/windowsNegotiate/Kerberos
GET /api/auth/oidc / GET /api/auth/oidc/callbackOIDC-Browserflow
POST /api/admin/settings/test/ldapLDAPS-/Bind-/Search-Test des Entwurfs
GET /healthz/readyDB-Readiness; enthält bewusst keinen Directory-Check
GET /healthz/directoryseparater LDAPS-/Service-Bind-Healthcheck über alle DCs; ein ausgefallener Secondary ergibt Degraded
/api/scim/v2/*SCIM-Discovery, Users und Groups

Anmeldename im Formular. alice, DOMAIN\alice und alice@firma.de werden alle auf denselben UPN normalisiert; ein Vorwärts-Schrägstrich (DOMAIN/alice) wird nicht erkannt und bleibt Teil des Namens. Jeder Fehlschlag hinterlässt einen Audit-Eintrag mit reason: ldap_invalid_credentials (Passwort/UPN falsch), ldap_user_object_not_found (UPN-Attribut fehlt), no_allowed_directory_group (Gruppen-Gate), pre_jit_account_throttle (fünf Versuche je 15 Minuten) oder infrastructure_failure (Directory nicht erreichbar, zusätzlich HTTP 503).

Beim IdP wird https://<nodepilot>/signin-oidc als Redirect URI registriert. /api/auth/oidc/callback ist nur die interne Landing-URL, nachdem der OIDC-Handler Code, State, Nonce, Issuer und Signatur validiert hat.

Vor einer Hochstufung von AD SSO Preview müssen reale Tests über die Ziel-Topologie belegen: LDAP und Windows mappen dieselbe SID auf denselben Benutzer, der Windows-Pfad ignoriert PAC-Gruppen zugunsten des LDAPS-Snapshots, LDAPS lehnt ungültige Zertifikate ab, Kerberos funktioniert durch HAProxy, NTLM wird abgelehnt und Offboarding greift innerhalb von 15 Minuten. Für OIDC/SCIM gehören HA-Failover mit gemeinsamem Data-Protection-Keyring sowie vollständige Membership-Heartbeats innerhalb von 15 Minuten zusätzlich zum Release-Gate.

Alle Kapitel