Arborescence des pages

Comparaison des versions

Légende

  • Ces lignes ont été ajoutées. Ce mot a été ajouté.
  • Ces lignes ont été supprimées. Ce mot a été supprimé.
  • La mise en forme a été modifiée.


Sommaire

Contexte

Eté 2019, l'Université de Rouen Normandie a procédé à une montée de version de son service d'authentification CAS en 6.0.4, nous partageons sur cet espace notre expérience de cette mise en production.

Dans un premier temps, le Kit installation CAS V5.2 AMU (un grand merci à Grégory et Dominique pour ce partage) a permis d'avoir  nous a permis de mettre en place un CAS v5.2.7 qui fonctionne rapidement et fonctionnel comprenant les éléments principaux.

On a ensuite Depuis cette installation, on est ensuite passé en 6.0.3 puis 6.0.4 et on a affiné certaines configurations. 

ClearPass

Nous partageons sur cet espace les points qui nous paraissent important à souligner par rapport à cette expérience de mise en production d'un CAS 6.0.x.

Contact : Vincent Bonamy (Vincent.Bonamy at univ-rouen.fr).

Sommaire

ClearPass

On a mis en place le ClearPass pour le faire fonctionner avec esup-filemanager 3.2.0.

Dans cette Ajout du clearPass avec le esup-filemanager 3.2.0 d'esup-filemanager, nous avions avons simplement rajouté une petite modification ( pour gérer le domaine windows ) qui a depuis été intégrée (modification intégrée dans esup-filemanager):
https://github.com/uPortal-contrib/esup-filemanager/pull/58

RememberMe

Ajout du rememberme.

Nous avons activé le RememberMe dans CAS.

Mais comme nous souhaitons le rendre disponible que sur 'mobiles', pour Pour ne laisser la case à cocher (permettant d'activer cette fonctionnalité) affichée que pour les 'mobiles', dans notre surcharge du footer.html on ajoute le bloc thymeleaf/javascript suivant : 

Bloc de code
<script type="text/javascript" th:unless="${#request.getAttribute('isMobile')}">
if(document.getElementById("rememberMe")) {
 document.getElementById("rememberMe").parentNode.style.display = "none";
}
</script>

Throttle

Modification du throttle en mettant :

Bloc de code
cas.authn.throttle.failure.range-seconds=30
cas.authn.throttle.failure.threshold=12

...

Spnego / Kerberos

Nous avons activé spnego + kerberos avec notre AD Microsoft

...

Firefox affiche alors la page de formulaire proposée, mais IE et Chrome réagissent différemment : suite au 401 ils affichent d'abord une popup d'authentifcation BASIC, en .
En cliquant sur cancel (annuler) l'utilisateur arrive bien sur le formulaire d'authentifcation CAS mais ce comportement n'est malgré tout pas souhaitable.

...

Concernant la mise en place de spnego, il faut bien suivre la documentation très bien faite ici suivante

https://apereo.github.io/cas/6.0.x/installation/SPNEGO-Authentication.html

On note que la création d'un compte AD pour le Service Principal Name consiste à créer un compte AD usuel et à appeler dans un powershell exécuté en tant qu'administrateur (: domain-account est le nom du compte , dans la documentation apereo (à l'URN on a préféré prendre un nom type cas-krb) : 

...

On note également que dans cette commande précédente ainsi que dans celle-ci : 

...

cas.example.com (cad cas) pointe corrrespond au nom de machine et non à un alias. C'Utiliser l'alias et non le nom de machine est une erreur récurrente dans la mise en place de kerberos (erreur que nous avons faite, e manière obstinée ...).
A l'URN on a donc non pas saisi cas.univ-rouen.fr mais cast.univ-rouen.fr dans ces lignes de commande (cast étant le nom de machine hébergeant notre CAS : cf "host cas.univ-rouen.fr").

...

On a patché (et proposé ces patchs par PR) de cascas-server-support-agimus-logs et cas-server-support-agimus-cookie pour qu'ils supportent CAS en 6.0.x : 

...

En 6.0.x, CAS propose d'utiliser les endpoints à la spring-boot pour interagir avec CAS , en bref, CAS propose par ce biais des API REST. 

...

A l'URN, on a activé ces endpoints pour notre application de gestion de comptes, afin qu'elle que cette application puisse demander à CAS d'expirer les tickets de comptes compromis.

...

On a implémenté la destruction des sessions CAS d'un utilisateur ainsi depuis notre application de gestion de comptes (codée en java/spring) : 

Bloc de code
public class CasService {
	
	protected Logger log = Logger.getLogger(CasService.class);
	
	RestTemplate restTemplate;
	
	String casSsoSessionsUrl;
	
	String casDestroySsoSessionsUrl;
	
	public void setRestTemplate(RestTemplate restTemplate) {
		this.restTemplate = restTemplate;
	}

	public void setCasUrl(String casUrl) {
		casSsoSessionsUrl = casUrl + "/actuator/ssoSessions?type=ALL";
		casDestroySsoSessionsUrl = casUrl + "/actuator/ssoSessions/{ticketGrantingTicket}";
	}

	public synchronized String destroySsoSessions(String login) {	
		String message = "";
		CasSsoSessions casSsoSessions = restTemplate.getForObject(casSsoSessionsUrl, CasSsoSessions.class);
		for(CasSsoSession casSsoSession : casSsoSessions.activeSsoSessions) {
			if(login.equals(casSsoSession.principal)) {
				log.info(String.format("Call Cas Destroy ticket %s for user %s", casSsoSession.principal, casSsoSession.ticketGrantingTicket));
				Map<String, String> urlVariables = new HashMap<String, String>();
				urlVariables.put("ticketGrantingTicket", casSsoSession.ticketGrantingTicket);
				CasSsoSessionDestroyResponse casSsoSessionDestroyResponse = restTemplate.postForObject(casDestroySsoSessionsUrl, null, CasSsoSessionDestroyResponse.class, urlVariables);
				log.info(String.format("CAS response : %s",  casSsoSessionDestroyResponse.toString()));
				message += casSsoSessionDestroyResponse.toString() + "\n";
			}
		}
		return message;
	}
	
}

Tickets Registry

...


	}
	
}

Tickets Registry

Cf ci-dessus (paragraphe sur le le RememberMe ), on souhaite proposer à nos utilisateurs la possibilité de conserveur leur session CAS sous mobile durant 2 semaines (14 jours, soit 1209600 secondes).
Sans rememberme de demandé, on propose une conservation de session CAS comme donné par défaut : cad c'est à dire 8 heures (28800 secondes).

...

https://apereo.github.io/cas/6.0.x/installation/Configuring-LongTerm-Authentication.html

Le TGT est peut donc alors être conservé pendant 2 semaines ... et un TGT prend une certaine place ce qui fait dire si l'utilisateur active cette fonctionnalité.
Un TGT prenant une "certaine place" à la persistence, il faut alors que le ticket registry, c'est à dire le méanisme de persistence des tickets, doit permettre une telle persistencepermette un tel usage.

Ainsi la La documentation donnée ci-dessus indique :

Bloc de code
A security policy that requires that long term authentication sessions MUST NOT be terminated prior to their natural expiration 
would mandate a ticket registry component that provides for durable storage, such as the JPA Ticket Registry.

Nous nous sommes donc dans un premier temps dirigé vers l'usage d'un ticket registry jpa, en utilsiant utilisant postgresql que nous affectionnons particulièrement.

En test nous n'avons pas eu à nous en plaindre, en production le choix de JPA , celà a bien fonctionné dans un premier temps mais rapidement ce choix s'est avéré très  problématique, cf ci-dessous.

JPA (PostgreSql)

Rien à signaler sur la configuration avec JPA et PostgresqlNous avons configuré le ticket registry JPA + Postgresql de manière usuelle.

Pour implémenter ce que nous souhaitons faire, nous avons mis les propriétés suivantes pour les temps de conservation des tickets :

Bloc de code
cas.ticket.tgt.maxTimeToLiveInSeconds=28800
cas.ticket.tgt.timeToKillInSeconds=28800
cas.ticket.tgt.rememberMe.timeToKillInSeconds=1209600.tgt.rememberMe.timeToKillInSeconds=1209600

Passé en production, pas de problème de temps de réponse, du point de vue de l'utilisateur tout fonctionne parfaitement.

Problème

Mais Passé en production, nous avons constaté que  :

  • chaque TGT prenait 'beaucoup' de place, d'une certaine manière celà nous a conforté dans un premier temps aux choix de JPA et donc de la base de données pour la persistence des tickets.
  • mais la base de données ne faisait qu'augmenter ... et donc la purge des tickets ne se faisait vraissemblablement pas correctement - cf ce graphe munin de 24 heures d'exploitation en production

En regardant les logs CAS, on trouve l'erreur récurrente suivante (ceci est un extrait : a on n'a conservé que les lignes 'intéressantes ici') : 

Bloc de code
2019-07-10 03:32:58,876 ERROR [org.hibernate.engine.jdbc.batch.internal.BatchingBatch] - <HHH000315: Exception executing batch [org.hibernate.StaleStateException: Batch update returned unex\
pected row count from update [0]; actual row count: 0; expected: 1], SQL: update TICKETGRANTINGTICKET set NUMBER_OF_TIMES_USED=?, CREATION_TIME=?, EXPIRATION_POLICY=?, EXPIRED=?, LAST_TIME_\
USED=?, PREVIOUS_LAST_TIME_USED=?, AUTHENTICATION=?, DESCENDANT_TICKETS=?, PROXIED_BY=?, PROXY_GRANTING_TICKETS=?, SERVICES_GRANTED_ACCESS_TO=?, ticket_Granting_Ticket_ID=? where ID=?>
2019-07-10 03:32:58,876 ERROR [org.apereo.cas.ticket.registry.DefaultTicketRegistryCleaner] - <Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1>
javax.persistence.OptimisticLockException: Batch update returned unexpected row count from update [0]; actual row count: 0; expected: 1
.......
        at org.apereo.cas.ticket.registry.JpaTicketRegistry.deleteTicketGrantingTickets(JpaTicketRegistry.java:198) ~[cas-server-support-jpa-ticket-registry-6.0.4.jar:6.0.4]
.......
        at com.sun.proxy.$Proxy152.deleteTicket(Unknown Source) ~[?:?]
        at org.apereo.cas.ticket.registry.DefaultTicketRegistryCleaner.cleanTicket(DefaultTicketRegistryCleaner.java:78) ~[cas-server-core-tickets-api-6.0.4.jar:6.0.4]
.......
        at org.apereo.cas.ticket.registry.DefaultTicketRegistryCleaner.cleanInternal(DefaultTicketRegistryCleaner.java:65) ~[cas-server-core-tickets-api-6.0.4.jar:6.0.4]
        at org.apereo.cas.ticket.registry.DefaultTicketRegistryCleaner.clean(DefaultTicketRegistryCleaner.java:45) ~[cas-server-core-tickets-api-6.0.4.jar:6.0.4]
.......

Suivi du message suivant

Bloc de code
2019-07-10 03:32:58,877 ERROR [org.apereo.cas.config.CasCoreTicketsSchedulingConfiguration] - <Transaction silently rolled back because it has been marked as rollback-only>
org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-onlyrollback-only

Les considérations qui suivent font suite à une analyse du code et des recherches sur internet (dont forums CAS).

Analyse du problème

Le DefaultTicketRegistryCleaner En analysant le code, on comprend que le DefaultTicketRegistryCleaner est en charge de nettoyer les tickets expirés.

Pour ce faire il lance une méthode clean qui est transactional et qui va donc se charger dans une seule et même et seule transaction de regarder tous les tickets pour nettoyer ceux qui sont expirés.

Le nettoyage d'un ticket ne consiste pas seulement à supprimer un ticket, il consiste aussi à supprimer les tickets potentiellement enfants ou/et à spécifier que le ticket est expiré ou/et à mettre à jour les tickets parents pour indiquer qu'un enfant n' est plus là expiré, etc.

Entre nos chainages de proxycas, de clearpass, ... on comprend du log ci-dessus que pour un ticket, le nettoyage ne passe pas : on tente de mettre à jour un ticket qui est en fait supprimé. Si
Cependant si l'opération de suppression était est effectivement dans la même transaction, ça devrait doit passer malgré tout ... c'est sans doute pour ça qu'on n'a , on en déduit 2 choses distinctes : 

  • que celà fait suite sans doute à un bug antérieur, notre base n'est plus consistante ici peut-être
  • le fait que DefaultTicketRegistryCleaner n'utilise qu'une seule

...

  • et même transaction doit permettre qu'un update sur un ticket sur lequel on a appelé un delete avant ne pose justement pas de pb (pure spéculation ici, l'usage d'une seule transaction pour nettoyer n tickets laisse perplexe).

Vu .Bref, vu qu'il y a une erreur sur un nettoyage de d'un ticket, c'est toute la transaction qui est avortée.
Et donc aucun ticket ne peut être nettoyé.
D'où le nombre de tickets qui ne fait qu'augmenter en base.

Remarques sur l'implémentation de la persistence JPA

En tentant de débloquer la situation, on a remarqué les choses suivantes : 

...

Dans une logique sql, on récupèrerait directement par un select uniquement les tickets que l'on doit supprimer.

Enfin la taille de la base de données, et la taille prise donc par la persistence d'un ticket dans postgresql est justifiée par cet usage des Large Object ; du coup celà prend une place relativement conséquente ; l'ordre de grandeur est environ 3GB pour 30000 TGT .....

Conclusion : le ticketRegistry JPA à éviter

Celà nous amène à conclure que l'usage d'un ticket registry CAS en JPA/Postgresql est clairement à éviter. 

C'est ce qui est +/- indiqué dans la documentation ici : https://apereo.github.io/cas/6.0.x/ticketing/JPA-Ticket-Registry.html :

...

C'est a priori le ticket registry recommandé, et c'est celui que le script de l'AMU donnait par défautdéfaut.

Aussi lors de notre passage en production sur CAS 6.0.4, ayant quelques doutes avec le ticket registry en jpa/postgresql, nous avions préservé les configurations pour rebasculer sur du Redis en cas de problème, ce qui a donc été fait 2 jours après la mise en production de notre CAS 6.0.4.

Le passage sur Redis  est Redis a donc été immédiat. Et rapidement on voit
En production, on constate que le stockage (complètement en RAM ... et en avec un fichier de dump pour préserver les tickets lors des redémarrages) est en fait peu gourmand.

Simple d'usage, on peut interroger avec redis-cli le serveur redis et observer ce qu'il se passe le fonctionnement : les tickets (clef/valeur) sont poussés par CAS avec une date d'expiration, cette date d'expiration étant ensuite gérée en interne par redis lui-même.

Cette durée / date d'expiration correspond au pramètre cas.ticket.tgt.maxTimeToLiveInSeconds - paramètre que nous n'avions en fait pas compris lors de la mise en place de CAS avec JPA / Postgresql puisque (une base SQL comme postgresql ne propose pas ce mécanisme d'expiration de la donnée).

Ainsi nous mettons finalement comme péramétrage : 

...

  • Le ticket registry via Redis donne toute satisfaction.
  • Le ticket registry via jpa/postgresql a peut-être été un choix possible dans les anciennes versions de CAS ; aujourd'hui celà est très risqué de l'utilisé ... il est vraiment à déconseiller dans le sens où il ne correspond pas à la logique de développement actuel de CAS.

Misc

Via des versions à jour de spring-boot et spring-security, CAS en 6.0.x amène l'usage des dernières technologies web, notamment en matière de sécurité (csp, xss, csrf, xsrf ...) et de protocole (chunked transfer encoding).Notre migration depuis un CAS 4 a ainsi posé problèmes à 2 applications.

Iframe

Ainsi l'authentification d'une de nos applications ne fonctionnait plus après le passage sur notre nouveau CAS car celle-ci intègre le CAS dans une iframe. En attendant de corriger celà côté de l'application, on a ajusté (temporairement donc) la chose pour que ça passe à nouveau : suppression de l'entête http X-Frame-Options en positionnant simplement la règle suivante sur notre Apache qui fait office de proxy avec notre Tocmat CAS :

...