Dies ist keine fertige Schritt für Schritt Anleitung wie man eine „shibbolisierte“ Webanwendung (die Daten über Ihre Nutzer speichert) bereinigt, sondern die aufgeführten Punkte sollen lediglich als Gedankenanstoß dienen, wie man das Thema User Deprovisionierung angehen kann und was es zu beachten gilt.
Problem hierbei ist meist, dass man bei dem Shibboleth-Verfahren nur dann Informationen über einen Nutzer erhält, wenn sich dieser aktiv einloggt.
(Ausgenommen: man hat seinen Dienst noch über eine weitere Schnittstelle an das eigene IDM-System angeschlossen)
Also wie entscheiden wir nun ob ein Nutzer noch existiert oder nicht?
Überlegen wir mal kurz welche Möglichkeiten sich uns bieten, wenn wir nun „alte“ Nutzer identifizieren und sperren oder gar entfernen wollen:
Betrachten wir die letzte Variante mit Hilfe des Shibboleth-eigenen Board-Mittels namens Attribute-Query.
Ursprünglich diente dieses Verfahren bei SAML1 der direkten Übertragung der Nutzer-Attribute zwischen IdP und SP über einen Backchannel.
Hier stellte der SP nach erfolgreichem Login des Nutzers eine separate Anfrage an den IdP, welcher alle Attribute über den Nutzer auslieferte, die beim heutigen SAML2 über den Frontchannel übergeben werden.
Um solch einen Query stellen zu können muss der SP im Besitz eines gültigen NameIdentifiers sein, damit der IdP diesen Auflösen kann.
Wenn ein SP einen Query zu einem beliebigen Zeitpunkt stellen möchte (unabhängig vom Vorhandensein eines Login-Contextes des Nutzers), so kommt hier lediglich die „persistentId“ in Frage.
Diese muss auf Seiten des IdP dazu in einer Datenbank gespeichert werden, damit Sie rückwärts aufgelöst werden kann.
Anforderungen
Wie stellt man einen Query?
Standardmäßig bringt der SP ein „resolvertest“-Skript mit.
Dieses empfiehlt sich jedoch nur für einen initialen Test, um zu Prüfen ob alle Einstellungen passen und der Attribute Query korrekt beantwortet wird.
Für den Produktiv-Einsatz arbeitet es zu langsam!
# ./resolvertest -n +9Blu1I8v96axDXHj01Gmpg36fM= -i https://your-idp.de/idp/shibboleth -saml2 -f urn:oasis:names:tc:SAML:2.0:nameid-format:persistent # persistentId: https://your-idp.de/idp/shibboleth!https://your-sp.de/shibboleth!+9Blu1I8v96axDXHj01Gmpg36fM= # givenName: Max # surname: Mustermann # ...
Hat man Zugriff zum IdP kann man die übermittelten Attribute auf Vollständigkeit überprüfen, indem man am IdP nochmal den resolvertest ausführt.
# wget https://your-idp.de/idp/profile/admin/resolvertest?requester=https://your-sp.de&principal=uid-des-test-nutzers # persistentId: https://your-idp.de/idp/shibboleth!https://your-sp.de/shibboleth!+9Blu1I8v96axDXHj01Gmpg36fM= # givenName: Max # surname: Mustermann # ...
Will man nun vom SP aus automatisiert Anfragen stellen, so existiert hierfür der. sog. Attribute Resolver Handler:
<!-- ... --> <OutOfProcess> <Extensions> <Library path="plugins.so" fatal="true"/> </Extensions> </OutOfProcess> <InProcess> <Extensions> <Library path="plugins-lite.so" fatal="true"/> </Extensions> </InProcess> <ApplicationDefaults...> <Sessions...> <!-- ... --> <Handler type="AttributeResolver" Location="/AttributeResolver" acl="127.0.0.1 ::1" /> <!-- ... --> </Sessions> <!-- ... --> <AttributeResolver type="Query" subjectMatch="true"/> <!-- ... --> </ApplicationDefaults> <!-- ... -->
Vergessen Sie nicht, sowohl den shibd
als auch den Webserver/httpd neu zu starten!
Danach kann ein Aufruf z.B. direkt mit CURL durchgeführt werden.
!!!ACHTUNG!!! persistentId muss codiert übergeben werden, da Sonderzeichen zu Misserfolg führen!!! Stichwort URLencoding!!! # curl -k "https://your-sp.de/Shibboleth.sso/AttributeResolver?entityID=https://your-idp.de/idp/shibboleth" --data-urlencode "nameId=+9Blu1I8v96axDXHj01Gmpg36fM="
Hat man seine ersten Queries erfolgreich gestellt, kommen schnell die Fragen auf:
Wann kann ich den Nutzer löschen? und Wie verlässlich ist die Rückgabe?
Wenn wir davon ausgehen dass ein Query das gleiche Attribut-Set zurück liefert, wie ein normaler Login-Vorgang, dann sollte man den Nutzer löschen können, sobald eine leere Menge zurück kommt.
Aber ACHTUNG!!!
Folgende Fallstricke können fälschlicherweise zu einem leeren oder auch nicht leeren Ergebnis führen:
leeres Ergebnis
nicht leeres Ergebnis
Prinzipiell kann behauptet werden, dass man einem leeren Ergebnis nicht trauen kann und sollte!
Doch wie schließt man nun die diversen Fehler bei der Übertragung aus?
Sicherer wäre es, wenn man ein Attribut bekommt, was den Nutzerstatus abbildet und an Hand dessen man entscheidet, ob der Nutzer deaktiviert bzw. gelöscht werden kann.
Denn wenn man einen definierten Wert für das Attribut erhält kann man zumindest davon ausgehen, dass alles funktioniert hat und der Query erfolgreich abgearbeitet wurde und auch sonst keine Probleme bei der Abfrage des LDAP oder sonst irgendwo bei der Kommunikation zwischen den Systemen aufgetreten sind!
Um den LDAP nicht mit Karteileichen vollzumüllen, so wäre eine Möglichkeit die Struktur des LDAP wie folgt zu erweitern:
ou=users,dc=einrichtung,dc=de
(nur aktive Nutzer)ou=archive,dc=einrichtung,dc=de
ou=users,ou=archive,dc=einrichtung,dc=de
(nur archivierte Nutzer)ou=disabled,ou=users,ou=archive,dc=einrichtung,dc=de
(gelöscht, kann aber wieder angelegt werden)ou=locked,ou=users,ou=archive,dc=einrichtung,dc=de
(vorübergehend gesperrt, z.B. aus Sicherheitsgründen)ou=deleted,ou=users,ou=archive,dc=einrichtung,dc=de
(endgültig gelöschte Nutzer, falls die Identitäten noch gespeichert werden sollen, z.B. um sicherzustellen, dass User IDs nicht neu vergeben werden)
Scheidet ein Nutzer aus oder muss z.B. auf Grund eines Sicherheitsvorfalls kurzfristig deaktiviert werden, so verschiebt man diesen zunächst nach locked
.
Damit kann er sich schon mal nicht mehr am IdP authentifizieren (da nicht mehr im baseDN).
Kommt der Nutzer nach x-Tagen nicht mehr zurück an die Einrichtung so kann man ihn dauerhaft löschen und nach disabled
oder deleted
verschieben.
Generell reicht es zu, wenn die Einträge unter ou=archive
nur noch die „uid“ beinhalten und nicht mehr alle Attribute. (das spart Speicher ^^)
Jetzt definieren wir im IdP das Attribut schacUserStatus, das den Status der betreffenden Identität abbilden soll:
Vokabular für schacUserStatus:
urn:schac:userStatus:de:aai.dfn.de:idmStatus:STATUS
, wobei STATUS
die folgenden Werte annehmen kann:active
(optional) (entspricht AD 'enabled')locked
(optional) (vorübergehend gesperrt, z.B. aus Sicherheitsgründen)disabled
(optional) (gelöscht, kann aber wieder angelegt werden)deleted
(mandatory) (Account und Benutzerinformationen gelöscht)<!--...--> <!-- look in dn of archiveLDAP for inactive or blocked --> <!-- schacUserStatus --> <AttributeDefinition xsi:type="ScriptedAttribute" id="schacUserStatus"> <InputDataConnector ref="archiveLDAP" /> <DisplayName xml:lang="de">Benutzerstatus</DisplayName> <DisplayName xml:lang="en">Userstatus</DisplayName> <DisplayDescription xml:lang="de">Status eines Benutzers für einen Dienst</DisplayDescription> <DisplayDescription xml:lang="en">set of status of a person as user of services</DisplayDescription> <AttributeEncoder xsi:type="SAML1String" name="urn:mace:terena.org:schac:attribut-def:schacUserStatus" /> <AttributeEncoder xsi:type="SAML2String" name="urn:oid:1.3.6.1.4.1.25178.1.2.19" friendlyName="schacUserStatus" /> <Script> <![CDATA[ if (typeof entryDN != "undefined" && entryDN.getValues().size()> 0) { var prefix = "urn:schac:userStatus:de:aai.dfn.de:"; var disabled= "ou=disabled,ou=users,ou=archive,dc=einrichtung,dc=de"; var locked = "ou=locked,ou=users,ou=archive,dc=einrichtung,dc=de"; var deleted = "ou=deleted,ou=users,ou=archive,dc=einrichtung,dc=de"; for (i=0; i<entryDN.getValues().size(); i++) { var tmp = entryDN.getValues().get(i); if (tmp.endsWith(disabled)) { schacUserStatus.addValue(prefix + "idmStatus:disabled"); } else if (tmp.endsWith(locked)) { schacUserStatus.addValue(prefix + "idmStatus:locked"); } else if (tmp.endsWith(deleted)) { schacUserStatus.addValue(prefix + "idmStatus:deleted"); } else { schacUserStatus.addValue(prefix + "idmStatus:active"); } } } ]]> </Script> </AttributeDefinition> <DataConnector id="archiveLDAP" xsi:type="LDAPDirectory" ldapURL="%{idp.attribute.resolver.LDAP.ldapURL}" baseDN="%{idp.attribute.resolver.LDAP.archiveDN}" principal="%{idp.attribute.resolver.LDAP.bindDN}" principalCredential="%{idp.attribute.resolver.LDAP.bindDNCredential}" searchScope="SUBTREE" maxResultSize="0" useStartTLS="%{idp.attribute.resolver.LDAP.useStartTLS}"> <FilterTemplate> <![CDATA[ %{idp.attribute.resolver.LDAP.archiveSearchFilter} ]]> </FilterTemplate> </DataConnector> <!--...-->
<!--...--> idp.attribute.resolver.LDAP.archiveDN = ou=archive,dc=einrichtung,dc=de idp.attribute.resolver.LDAP.archiveSearchFilter = (uid=$requestContext.principalName) <!--...-->
<!--...--> <AttributeRule attributeID="schacUserStatus"> <PermitValueRule xsi:type="ANY" /> </AttributeRule> <!--...-->
Bekommt ein SP nun bei einem Query einen der folgenden Werte, so kann er den Nutzer verlässlich sperren oder gar löschen.
urn:schac:userStatus:de:aai.dfn.de:idmStatus:disabled
urn:schac:userStatus:de:aai.dfn.de:idmStatus:locked
urn:schac:userStatus:de:aai.dfn.de:idmStatus:deleted
Als Beispiel für eine Attribute Query, die ein solches Attribut liefert, kann dieser URL genutzt werden:
https://testsp3.aai.dfn.de/Shibboleth.sso/AttributeResolver?entityID=https://testidp.aai.dfn.de/idp/shibboleth&nameId=MCE6NXEQ3FC3PUKY4M75EYCOWN4TGKBH&format=urn:oasis:names:tc:SAML:2.0:nameid-format:persistent
(zuvor bitte bei der DFN-AAI Hotline Bescheid sagen, damit die ACL für diesen Handler entsprechend erweitert wird)
Wer darf eigentlich Queries stellen?
Da Queries nur mit Angabe der persistentId funktionieren, so kann man per RelyingParty-Config einschränken, dass nur die SPs Queries stellen dürfen für die auch die persistentId freigegeben ist.
Dies lässt sich unter Angabe einer Activation Condition simpel lösen.
ACHTUNG: Diese Einstellung ist nur zu empfehlen, wenn keine SAML1 SPs mehr bedient werden!!!
<!--...--> <bean id="shibboleth.DefaultRelyingParty" parent="RelyingParty"> <property name="profileConfigurations"> <list> <bean parent="SAML2.SSO" p:postAuthenticationFlows="#{{'attribute-release'}}" p:nameIDFormatPrecedence="#{{'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'}}" /> <ref bean="SAML2.Logout" /> <ref bean="SAML2.ArtifactResolution" /> </list> </property> </bean> <util:list id="shibboleth.RelyingPartyOverrides"> <bean parent="RelyingParty" p:activationCondition-ref="SP-consumes-persistentId"> <property name="profileConfigurations"> <list> <bean parent="SAML2.SSO" p:postAuthenticationFlows="#{{'attribute-release'}}" p:nameIDFormatPrecedence="#{{'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent', 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'}}" /> <ref bean="SAML2.Logout" /> <ref bean="SAML2.AttributeQuery" /> <ref bean="SAML2.ArtifactResolution" /> </list> </property> </bean> </util:list> <!--...-->
<!--...--> <bean id="SP-consumes-persistentId" parent="shibboleth.Conditions.RelyingPartyId"> <constructor-arg name="candidates"> <list> <value>https://your-sp.de/shibboleth</value> <value>https://another-sp.de/shibboleth</value> </list> </constructor-arg> </bean> <!--...-->
Wozu sollen Queries genutzt werden?
Möchte man via Query Nutzerdaten synchron halten, so sind keine weiteren Einstellungen nötig.
Es empfiehlt sich jedoch im Vorfeld mit dem Datenschutzbeauftragten dieses Vorgehen im Vorfeld zu besprechen, da hier ohne Einverständnis des Nutzers personenbezogene Daten ausgetauscht werden!!!
Will man Queries ausschließlich zur User-Deprovisionierung benutzen, so kann man sämtliche anderen Attribute für diesen Kanal deaktivieren.
Dies bringt unter anderem folgende Vorteile mit sich:
Dazu laden wir uns zunächst folgende JAR-File idp-predicate-impl-1.0.0.jar herunter und legen diese unter „./edit-webapp/WEB-INF/lib/“ ab.
Anschließend den IdP neubauen.
# ./bin/build.sh
Danke noch mal an Steffen Hofmann (FU Berlin), der diese Datei zur Verfügung gestellt hat.
Mit Hilfe des enthaltenen Predicate ist es uns möglich eine Entscheidung zu treffen ob es sich um einen Attribute-Query handelt oder nicht.
Darauf aufbauend erstellen wir uns beliebige Activation Conditions, um die Erstellung und Freigabe von Attributen zu steuern.
<!--...--> <!-- condition is true if request is NOT an attribute-query --> <bean id="no-query" parent="shibboleth.Conditions.NOT"> <constructor-arg> <list> <ref bean="RequestedAttributeQueryProfileIdPredicate" /> </list> </constructor-arg> </bean> <!-- condition is true if request is NOT an attribute-query and if sp is one of the following --> <bean id="SP-consumes-isMemberOf" parent="shibboleth.Conditions.AND"> <constructor-arg> <list> <ref bean="no-query" /> <bean parent="shibboleth.Conditions.RelyingPartyId"> <constructor-arg name="candidates"> <list> <value>https://your-sp.de/shibboleth</value> <value>https://another-sp.de/shibboleth</value> </list> </constructor-arg> </bean> </list> </constructor-arg> </bean> <!-- condition is true if request IS an attribute-query and if sp consumes the persistentId --> <bean id="SP-consumes-schacUserStatus" parent="shibboleth.Conditions.AND"> <constructor-arg> <list> <ref bean="RequestedAttributeQueryProfileIdPredicate" /> <ref bean="SP-consumes-persistentId" /> </list> </constructor-arg> </bean> <!--...-->
Nun aktivieren wir die Bedingungen im Resolver.
Dadurch werden normale Attribute nur noch bei einem normalen Login gebaut und an den Filter weitergereicht werden, aber nicht bei einem Query.
Das schacUserStatus Attribut hingegen wird nur noch bei einem Query erstellt.
<!--...--> # Die Angabe einer ActivationCondition an einem normalen Attribut führt leider nicht dazu, dass die LDAP-Abfragen nur gestellt werden, wenn der SP in der Condition steht # Sondern: # - Das Attribut wird erst gebaut (LDAP-Abfrage) und erst danach greift die Condition und regelt die Weitergabe des Attributs an die Filter # - Die Condition MUSS daher an die LDAP-Dependency (DataConnector) gehangen werden, um die Ausführung der LDAP-Anfragen zu steuern/vermeiden <AttributeDefinition xsi:type="Simple" id="uid"> <InputDataConnector ref="myLDAP" attributeNames="uid"/> <!--...--> </AttributeDefinition> <DataConnector id="myLDAP" xsi:type="LDAPDirectory" activationConditionRef="no-query" <!--...--> </DataConnector> # Bei Attributen, die zusätzliche Conditions erhalten sollen können diese direkt am Attribut referenziert werden <AttributeDefinition xsi:type="Simple" id="isMemberOf" activationConditionRef="SP-consumes-isMemberOf"> <InputDataConnector ref="groupLDAP" attributeNames="cn"/> <!--...--> </AttributeDefinition> <DataConnector id="groupLDAP" xsi:type="LDAPDirectory" activationConditionRef="SP-consumes-isMemberOf" <!--...--> </DataConnector> <AttributeDefinition xsi:type="ScriptedAttribute" id="schacUserStatus" activationConditionRef="SP-consumes-schacUserStatus"> <InputDataConnector ref="archiveLDAP" /> <!--...--> </AttributeDefinition> <DataConnector id="archiveLDAP" xsi:type="LDAPDirectory" activationConditionRef="SP-consumes-schacUserStatus" <!--...--> </DataConnector> <!--...-->
Zusammenfassend hat man somit folgende Punkte realisiert:
Wann und wie oft sollten Queries gestellt werden?
Zunächst vorweg:
Der SP ist verantwortlich dafür Sorge zu tragen, dass der IdP während seines Betriebes nicht gestört wird, in dem er z.B. mit zu vielen Anfragen „bombardiert“ wird!!!
Daher empfiehlt es sich Nutzer nur dann zu prüfen wenn folgende Bedingungen erfüllt sind:
Desweiteren sollte man kleine Pausen zwischen einzelnen Queries lassen, damit man als SP nicht Gefahr läuft z.B. durch Mechanismen wie Fail2Ban ausgesperrt zu werden.
Als Betreiber eines IdP ist es durchaus denkbar sich ähnlich wie im Thema Abwehr Brute Force Gedanken zu machen um sich gegen zu viele Query-Anfragen zu schützen.
Am Besten man nutzt also eine Cachefile, in der man hinterlegt, wann der Nutzer das letzte Mal erfolgreich abgefragt wurde.
Es ist also nicht notwendig und nicht emfehlenswert jeden Nutzer jeden Tag zu prüfen!
Vorteile:
Alternativ besteht vielleicht die Möglichkeit einen separaten IdP aufzusetzen, der ausschließlich für die Abarbeitung von Queries zuständig ist.
Dabei sollte sichergestellt sein, dass alle IdP-Server via Datenbank-Replikation den gleichen Datenbestand aufweisen!!!
Denkbare Schwellwerte für Fail2Ban wären: maximal 50 Queries innerhalb von 10 Sekunden (nicht getestet und abhängig von der Menge der zu prüfenden Nutzer)