====== EJBCA - Création de certificats TLS ====== --- //[[nekan@shyrkasystem.com|Nicolas THOREZ]] 2024/03/20 17:03// Grâce à nos autorités de certification, nous avons la possibilité de créer des certificats TLS pour nos serveurs webs. Ces certificats et le fait que les autorités sont reconnues par les postes clients permettent de se passer des certificats auto-signés et par extensions, suppriment les messages d'erreurs lors de l'accès à des sites auto-signés : {{ :linux:ejbca:ejbca-042.png |}} On va donc voir comment créer un modèle de certificat TLS et les détails de la création de certificats pour des serveurs webs. Réf. : [[https://doc.primekey.com/ejbca/tutorials-and-guides/tutorial-issue-tls-server-certificates-with-ejbca|EJBCA]] ===== Création du modèle de certificat ===== * Sur l'interface d'administration, dans la section ''CA Functions'', on va dans ''Certificate Profiles'' et on clone le profil par défaut ''SERVER'' : {{ :linux:ejbca:ejbca-043.png |}} * On nomme le modèle et on le créé en cliquant sur ''Create from template'' : {{ :linux:ejbca:ejbca-044.png |}} * On édite le modèle fraichement créé via son bouton ''Edit'' : {{ :linux:ejbca:ejbca-045.png |}} * On commence par définir : * **Available Key Algorithms** : ''ECDSA'' * **Available ECDSA curves** : ''P-256 / prime256v1 / secp256r1'' * **Signature Algorithm** : ''Inherit from Issuing CA'' * **Validity or end date of the certificate** : ''1y'' * **Expriration Restriction** : On coche la cae ''Use...'' puis dans la section suivante, on coche les cases : * //Monday// * //Friday// * //Saturday// * //Sunday// * **Profile Description** : Optionnel, permet de mieux identifier le modèle. {{ :linux:ejbca:ejbca-046.png |}} * Dans la section ''X.509v3 extensions'', on décoche : * **Basic Constraints** {{ :linux:ejbca:ejbca-047.png |}} * Dans la section ''X.509v3 extensions - Usages'', on coche : * **Key Usage** * **Digital Signature** * **Key encipherment** * Dans //Extended Key Usage//, on choisira : * **Server Authentication** {{ :linux:ejbca:ejbca-048.png |}} * Dans la section ''X.509v3 extensions - Names'', : * on coche **Subject Alternative Name** * on décoche **Issuer Alternative Name** {{ :linux:ejbca:ejbca-049.png |}} * Dans la section ''X.509v3 extensions - Validation data'', on coche : * **CRL Distribution Points** * **Use CA defined CRL Distribution Point** * **Authority Information Access :** * **Use CA defined OCSP locator** * **Use CA defined CA issuer** {{ :linux:ejbca:ejbca-050.png |}} * Dans la section ''Other Data'' : * on décoche **LDAP DN order** * dans la partie //Available CAs//, on sélectionne notre autorité intermédiaire. * on sauvegarde le tout en cliquant sur ''Save'' {{ :linux:ejbca:ejbca-051.png |}} ===== Création du modèle de serveur ===== * On va désormais dans la menu ''RA Functions'', ''End Entity Profiles'' et dans la section ''Add End Entity Profile'', on nomme notre modèle et on l'ajoute ajoute en cliquant sur ''Add Profile'' : {{ :linux:ejbca:ejbca-052.png |}} * On sélectionne notre nouveau profil dans la liste et on l'édite : {{ :linux:ejbca:ejbca-053.png |}} * On commence par décocher **End Entity E-mail**, on peut aussi ajouter si on le souhaite une description : {{ :linux:ejbca:ejbca-054.png |}} * Dans la section ''Subject DN Attributes'', on sélectionnes dans le menu déroulant et on ajoute à la suite de ''CN, Common name'' déjà présent avec le bouton ''Add'' : * **O, Organization** : on indiquera le nom de notre organisation et on cochera ''Required'' uniquement * **C, Country (ISO 3166)** : on indiquera le code du pays de l'organisation et on cochera ''Required'' uniquement * Pour **CN, Common name**, on cochera ''Required'' et ''Modifiable'' {{ :linux:ejbca:ejbca-055.png |}} * Dans la section ''Other Subject Attributes'', pour l'attribut ''Subject Alternative Name'', on ajoutera ''DNS Name'' et on cochera ''Use entity CN field'' : {{ :linux:ejbca:ejbca-056.png |}} * Dans la section ''Main Certificate Data'', on choisira : * pour **Default Certificate Profile** : le profil TLS précédemment créé * pour **Default CAs** : l'autorité intermédiaire précédemment créée * pour **Default Token** : ''User Generated'' * pour **Available Tokens** : ''User Generated'' et ''PEM file'' * Puis on sauvegarde le tout avec le bouton ''Save'' en bas de la page : {{ :linux:ejbca:ejbca-057.png |}} Voilà, notre autorité de certification est configurée et opérationnelle pour la délivrance de certificat TLS pour nos serveurs webs internes. Il ne reste plus qu'à créer ces fameux certificats. Pour cela, nous avons deux possibilités : * Via l'interface web * Via des appels API ===== Création d'un certificat TLS ===== ==== Via l'interface ==== * On commence en ligne de commande par créer une configuration pour openssl : nano /tmp/openssl.conf [ req ] default_md = sha256 prompt = no distinguished_name = dn [ dn ] CN = O = C = * On crée un clé privée : openssl ecparam -genkey -name prime256v1 -out .key * On crée une requête de certification : openssl req -new -key .key -config /tmp/openssl.conf * On copie cette requête et on va sur l'interface de gestion, soit via l'interface d'administration en cliquant sur le menu ''RA Web'' (tout en bas des menus), soit directement via l'adresse ''https:%%//%%/ejbca/ra''. * Une fois sur l'interface, on clique sur ''Make New Request'' : {{ :linux:ejbca:ejbca-003.png |}} * On sélectionne : * Notre profil de certificat TLS * Notre profil de certification pour du TLS * On choisit ''Provided by user'' * On colle notre requête créée en ligne commande dans ''Upload CSR'' * On clique sur ''Upload CSR'' {{ :linux:ejbca:ejbca-058.png |}} * On trouve alors le résumé de notre requête. On ajoutera dans le champs ''Username'' de la section ''Provide User Credentials'', le ''FQDN'' principale pour ce certificat puis on pourra le télécharger en cliquant sur un des boutons de téléchargement situé en bas de page, chacun correspondant à un type précis de certificat (selon les besoin de votre serveur web donc) : {{ :linux:ejbca:ejbca-059.png |}} ==== Par appel API ==== * Pour faciliter la création/interrogation/révocation par appel API, j'ai créé un script ''manage-certificate.sh'' : #!/bin/bash ######################################### # # # Script de gestion des certificats # # # ######################################### #===========# # Variables # #===========# # Dossier de travail WorkingDirectory=/usr/local/batch/certmgr # Authentification pour le script ScriptAuthentication="$WorkingDirectory/script.p12:123456" # Adresse de l'autorité de certification AuthorityAddress="localhost" # Profil de certificat CertificateProfileName="TLS-SERVER" # Profil de serveur EndEntityProfileName="TlsServer" # Nom de l'autorité de certification CertificationAuthorityName="SubCa" # Fichier de configuration pour openssl OpensslConfig="/tmp/cert.conf" # Modèle de requête API ApiTemplate='{"certificate_request":$ApiCsr, "certificate_profile_name":$ApiCp, "end_entity_profile_name":$ApiEep, "certificate_authority_name":$ApiCa, "username":$ApiUser, "password":$ApiPwd}' # Dossier racine pour les certificats CertificateRoorDirectory="$WorkingDirectory/issued" # Utilisateur pour l'enregistremet EnrollmentUser="exploit" # Mot de passe pour l'enregistrement EnrollmentPassword="123456" # Fichier de réponse de l'API ApiResponse="/tmp/api.json" # Certificat racine RootCertificate="$WorkingDirectory/issued/RootCa.pem" # Certificat intermédiaire SubCertificate="$WorkingDirectory/issued/SubCa.pem" # Fichier de log ManageLog="/var/log/manage-certificate.log" # Raison de révocation par défaut RevokeReason="CESSATION_OF_OPERATION" # Organisation Organization="ShyrkaSystem" # Pays Country="FR" #===========# # Fonctions # #===========# # Demande d'aide display-help() { echo "Script de gestion des certificats" echo "---------------------------------" echo "" echo "Usage : ./manage-certificate.sh [add|new|create] " echo " [renew] " echo " [revoke|delete] " echo " [status] " echo " [help]" echo "" echo "Arguments :" echo "" echo "add Crée un certificat pour le domaine ." echo "create Identique à add." echo "delete Révoque et supprime le certificat pour le domaine ." echo "help Affiche l'aide." echo "new Identique à add." echo "renew Renouvelle un certificat existant (création si inexistant)." echo "revoke Identique à delete." echo "status Affiche le status du certificat pour le domaine ." echo "" echo " Obligatoire pour toute action et sous la forme d'un FQDN (ex : www.google.fr)." echo " Facultatif. Indique la raison de la révocation du certificat." echo " Par défaut, la raison sera CESSATION_OF_OPERATION." echo " Les raisons disponibles sont :" echo " - KEY_COMPROMISE" echo " - CA_COMPROMISE" echo " - AFFILIATION_CHANGED" echo " - SUPERSEDED" echo " - CESSATION_OF_OPERATION" echo " - CERTIFICATE_HOLD" echo " - PRIVILEGES_WITHDRAW" echo " - AA_COMPROMISE" echo "" } # Identifiant d'un certificat get-id() { openssl x509 -noout -serial -in $CertificateRoorDirectory/$1/fullchain.pem | cut -d'=' -f2 | sed 's/://g' } # Status d'un certificat get-status() { curl -X GET -k -s \ --cert-type P12 \ --cert "$ScriptAuthentication" \ --header "accept: application/json" \ "https://${AuthorityAddress}/ejbca/ejbca-rest-api/v1/certificate/CN%3D${1}%2CO%3D${2//[^[:alnum:]]/}%2CC%3DFR/${3}/revocationstatus" \ | jq . } # Révocation d'un certificat revoke-certificate() { curl -X PUT -k -s \ --cert-type P12 \ --cert "$ScriptAuthentication" \ --header "accept: application/json" \ "https://${AuthorityAddress}/ejbca/ejbca-rest-api/v1/certificate/CN%3D${1}%2CO%3D${2//[^[:alnum:]]/}%2CC%3DFR/${3}/revoke?reason=${4}" \ | jq . } # Création du certificat create-certificate() { # Création du JSON pour la requête API Json=$(jq -n \ --arg ApiCsr "$1" \ --arg ApiCp "$2" \ --arg ApiEep "$3" \ --arg ApiCa "$4" \ --arg ApiUser "$5" \ --arg ApiPwd "$6" \ "$7") # Envoi de la requête curl -X POST -s -k \ --cert-type P12 \ --cert "$ScriptAuthentication" \ --header 'Content-Type: application/json' \ --data "$Json" \ "https://${AuthorityAddress}/ejbca/ejbca-rest-api/v1/certificate/pkcs10enroll" \ | jq . } #===========# # Arguments # #===========# # Initialisation action="null" fqdn="null" case $1 in "help") display-help exit 0 ;; "add"|"new"|"create"|"revoke"|"delete"|"status"|"renew") action="$1" fqdn="$2" reason="$3" ;; *) echo "Erreur : Argument $1 inconnu" echo "" display-help exit 2 ;; esac # Vérification if [ "$action" == "null" ]; then # Aucune action, affichage de l'aide display-help exit 1 else # Vérification du fqdn if [ "$fqdn" == "null" ]; then echo "$(date '+%F %X %Z') - WARN - un nom de domaine est obligatoire." | tee -a $ManageLog echo "" display-help exit 1 elif [[ $fqdn =~ ^([a-z0-9]+([a-z0-9]|\-)*\.)+[a-z]{2,} ]]; then true else echo "$(date '+%F %X %Z') - CRIT - le domaine $fqdn n'est pas valide." | tee -a $ManageLog exit 2 fi fi # Log echo "$(date '+%F %X %Z') - INFO - Démarrage (Action: $action | Domaine: $fqdn)" | tee -a $ManageLog #============# # Traitement # #============# if [ "$action" == "add" -o "$action" == "create" -o "$action" == "new" -o "$action" == "renew" ]; then # Vérification de l'état actuel if [ "$action" == "renew" ]; then # Renouvellement demandé if [ ! -d $CertificateRoorDirectory/$fqdn ]; then # Pas de certificat existant, création du dossier et de la clé privée echo "$(date '+%F %X %Z') - WARN - Certificat inexistant. Création du certificat au lieu du renouvellement." | tee -a $ManageLog mkdir -p $CertificateRoorDirectory/$fqdn echo "$(date '+%F %X %Z') - INFO - Création du dossier $CertificateRoorDirectory/$fqdn" | tee -a $ManageLog openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key echo "$(date '+%F %X %Z') - INFO - Création de la clé privée $CertificateRoorDirectory/$fqdn/private.key" | tee -a $ManageLog elif [ ! -f $CertificateRoorDirectory/$fqdn/private.key ]; then # Pas de clé privée à utiliser echo "$(date '+%F %X %Z') - WARN - Clé privée absente. Création d'une nouvelle clé." | tee -a $ManageLog openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key else # Vérification de la clé privée CertificateChecksum=$(openssl x509 -noout -pubkey -in $CertificateRoorDirectory/$fqdn/fullchain.pem 2>&1 | sha256sum | awk '{print $1}') PrivateKeyChecksum=$(openssl pkey -pubout -in $CertificateRoorDirectory/$fqdn/private.key 2>&1 | sha256sum | awk '{print $1}') if [ "$CertificateChecksum" == "$PrivateKeyChecksum" ]; then # Clé valide echo "$(date '+%F %X %Z') - INFO - Clé privée valide." | tee -a $ManageLog else # Clé invalide echo "$(date '+%F %X %Z') - WARN - Clé privée invalide. Création d'une nouvelle clé." | tee -a $ManageLog openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key fi fi else # Création demandée if [ -f $CertificateRoorDirectory/$fqdn/fullchain.pem ]; then # Certificat déjà existant echo "$(date '+%F %X %Z') - WARN - Un certificat pour $fqdn existe déjà." | tee -a $ManageLog echo "$(date '+%F %X %Z') - INFO - Vous pouvez le renouveller à la place sinon il faudra le révoquer au préalable." | tee -a $ManageLog echo "$(date '+%F %X %Z') - INFO - Si vous êtes sûr que ce certificat est déjà révoqué, il est nécessaire de supprimer le dossier $CertificateRoorDirectory/$fqdn." | tee -a $ManageLog exit 1 elif [ ! -d $CertificateRoorDirectory/$fqdn ]; then # Création du répertoire pour le certificat mkdir -p $CertificateRoorDirectory/$fqdn echo "$(date '+%F %X %Z') - INFO - Création du dossier $CertificateRoorDirectory/$fqdn" | tee -a $ManageLog # Création de la clé privée openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key echo "$(date '+%F %X %Z') - INFO - Création de la clé privée $CertificateRoorDirectory/$fqdn/private.key" | tee -a $ManageLog else # Création de la clé privée openssl ecparam -genkey -name prime256v1 -out $CertificateRoorDirectory/$fqdn/private.key echo "$(date '+%F %X %Z') - INFO - Création de la clé privée $CertificateRoorDirectory/$fqdn/private.key" | tee -a $ManageLog fi fi # Création de la configuration pour openssl if [ -e $OpensslConfig ]; then rm -f $OpensslConfig fi echo "[ req ]" > $OpensslConfig echo "default_md = sha256" >> $OpensslConfig echo "prompt = no" >> $OpensslConfig echo "distinguished_name = dn" >> $OpensslConfig echo "" >> $OpensslConfig echo "[ dn ]" >> $OpensslConfig echo "CN = $fqdn" >> $OpensslConfig echo "O = $Organization" >> $OpensslConfig echo "C = $Country" >> $OpensslConfig # Création de la requête CsrContent=$(openssl req -new -key $CertificateRoorDirectory/$fqdn/private.key -config $OpensslConfig) # Envoi de la requête CreateRequest=$(create-certificate "$CsrContent" "$CertificateProfileName" "$EndEntityProfileName" "$CertificationAuthorityName" "$EnrollmentUser" "$EnrollmentPassword" "$ApiTemplate") # Extraction du certificat echo $CreateRequest | jq -r .certificate > $CertificateRoorDirectory/$fqdn/cert.b64 # Conversion du DER en binaire openssl base64 -d -A -in $CertificateRoorDirectory/$fqdn/cert.b64 -out $CertificateRoorDirectory/$fqdn/cert.bin # Conversion du binaire en PEM openssl x509 -inform der -in $CertificateRoorDirectory/$fqdn/cert.bin -out $CertificateRoorDirectory/$fqdn/fullchain.pem # Ajout des certificats racines et intermédiaires cat $SubCertificate >> $CertificateRoorDirectory/$fqdn/fullchain.pem cat $RootCertificate >> $CertificateRoorDirectory/$fqdn/fullchain.pem # Log echo "$(date '+%F %X %Z') - INFO - Création du certificat $CertificateRoorDirectory/$fqdn/fullchain.pem" | tee -a $ManageLog exit 0 elif [ "$action" == "status" ]; then # Recherche du n° de série du dernier certificat CertificateSerialNumber=$(get-id $fqdn) # Demande du status CertificateStatusRequest=$(get-status $CertificationAuthorityName $Organization $CertificateSerialNumber) # Extraction des données CertificateExtractedStatus=$(echo $CertificateStatusRequest | jq .revoked) if [ "$CertificateExtractedStatus" == "false" ]; then CertificateStatus="actif" else CertificateStatus="révoqué" fi CertificateDetails=$(echo $CertificateStatusRequest | jq .revocation_reason) # Log echo "$(date '+%F %X %Z') - INFO - Certificat $fqdn (S/N: $CertificateSerialNumber) en état : $CertificateStatus (Détails: $CertificateDetails)" | tee -a $ManageLog exit 0 elif [ "$action" == "delete" -o "$action" == "revoke" ]; then # Validation de la raison de la révocation if [ -z "$reason" ]; then echo "$(date '+%F %X %Z') - INFO - Aucune raison de révocation indiqué, utilisation de la raison par défaut ($RevokeReason)." | tee -a $ManageLog else case ${reason^^} in "KEY_COMPROMISE"|"CA_COMPROMISE"|"AFFILIATION_CHANGED"|"SUPERSEDED"|"CESSATION_OF_OPERATION"|"CERTIFICATE_HOLD"|"PRIVILEGES_WITHDRAW"|"AA_COMPROMISE") RevokeReason=${reason^^} echo "$(date '+%F %X %Z') - INFO - Raison de la révocation : $RevokeReason" | tee -a $ManageLog ;; *) echo "$(date '+%F %X %Z') - WARN - Raison de révocation indiquée ($reason) indisponible, utilisation de la raison par défaut ($RevokeReason)." | tee -a $ManageLog ;; esac fi # Recherche du n° de série du dernier certificat CertificateSerialNumber=$(get-id $fqdn) # Demande du status CertificateStatusRequest=$(get-status $CertificationAuthorityName $Organization $CertificateSerialNumber) # Extraction des données CertificateExtractedStatus=$(echo $CertificateStatusRequest | jq .revoked) # Vérification if [ "$CertificateExtractedStatus" == "false" ]; then # Révocation RevokeRequest=$(revoke-certificate $CertificationAuthorityName $Organization $CertificateSerialNumber $RevokeReason) # Extraction des données RevokeStatus=$(echo $RevokeRequest | jq .revoked) RevokeMessage=$(echo $RevokeRequest | jq .message) # Log if [ "$RevokeStatus" == "true" ]; then /usr/bin/rm -rf $CertificateRoorDirectory/$fqdn echo "$(date '+%F %X %Z') - INFO - Certificat $fqdn révoqué et supprimé (API Message: $RevokeMessage)" | tee -a $ManageLog exit 0 else echo "$(date '+%F %X %Z') - CRIT - Echec de la révocation pour $fqdn (API Message: $RevokeMessage)" | tee -a $ManageLog echo "$(date '+%F %X %Z') - INFO - Fichiers conservés dans $CertificateRoorDirectory/$fqdn" exit 2 fi else # Log echo "$(date '+%F %X %Z') - WARN - Certificat $fqdn déjà révoqué)" | tee -a $ManageLog exit 1 fi else # Action inconnue echo "$(date '+%F %X %Z') - UNKN - L'action $action n'est pas prévue" | tee -a $ManageLog exit 3 fi * Il suffit de modifier les variables : * ''WorkingDirectory'' : Le dossier dans lesquels sont stockés les certificats créés, le certificat du script, etc... * ''AuthorityAddress'' : L'adresse de votre autorité de certification. * ''CertificateProfileName'' : Le nom de votre profil de certificat. * ''EndEntityProfileName'' : Le nom de votre profil de serveur. * ''CertificationAuthorityName'' : Le nom de votre autorité de certification. * ''EnrollmentUser'' : Un nom de compte à associer aux certificats créés. Si il n'existe pas, il sera créé automatiquement. * ''EnrollmentPassword'' : Le mot de passe du compte précédent. * ''Organization'' : Le nom de votre organisation. * ''Country'' : Votre pays. * On place le certificat du script dans ''$WorkingDirectory'' et les certificats racine et intermédiaire au format ''PEM'' dans ''$WorkingDirectory/issued/''. * Il ne reste plus qu'à lancer le script. ~~DISCUSSION~~