Table des matières

EJBCA - Installation d'une infrastructure à clés publiques (PKI)

CréationNicolas THOREZ 2024/03/19 10:29
ModificationNicolas THOREZ 2024/03/26 16:26

Une infrastructure à clés publiques ou PKI (Public Key Infrastructure) est un ensemble de services permettant de gérer des clés publiques et se faisant, permettant l'authentification d'un service, système ou utilisateur.

Réf : Wikipédia

Dans cette procédure, nous allons voir la mise en place d'une PKI via le logiciel EJBCA (Enterprise JavaBeans Certificate Authority) en version Community.

Droits

Cette procédure nécessite des droits root. La plus grande prudence est donc requise.

Réalisation

Cette procédure a été réalisée et testé sur un VM en Debian 11, hébergée sur un hyperviseur Proxmox 8.0.4.

Installation

Réf : EJBCA

Installation de docker

apt install -y ca-certificates curl

install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
tee /etc/apt/sources.list.d/docker.list > /dev/null

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Installation de EJBCA

mkdir -p /opt/ejbca/containers/datadbdir
cd /opt/ejbca/containers/

nano docker-compose.yml

version: '3'
networks:
  access-bridge:
    driver: bridge
  application-bridge:
    driver: bridge
services:
  ejbca-database:
    container_name: ejbca-database
    image: "library/mariadb:latest"
    networks:
      - application-bridge
    environment:
      - MYSQL_ROOT_PASSWORD=foo123
      - MYSQL_DATABASE=ejbca
      - MYSQL_USER=ejbca
      - MYSQL_PASSWORD=ejbca
    volumes:
      - ./datadbdir:/var/lib/mysql:rw
  ejbca-node1:
    hostname: ejbca-node1
    container_name: ejbca
    image: keyfactor/ejbca-ce:latest
    depends_on:
      - ejbca-database
    networks:
      - access-bridge
      - application-bridge
    environment:
      - DATABASE_JDBC_URL=jdbc:mariadb://ejbca-database:3306/ejbca?characterEncoding=UTF-8
      - LOG_LEVEL_APP=INFO
      - LOG_LEVEL_SERVER=INFO
      - TLS_SETUP_ENABLED=simple
    ports:
      - "80:8080"
      - "443:8443"

docker compose up -d

docker compose logs -f

ejbca           | 2024-03-19 12:51:19,457+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) Waiting 5 seconds before signaling application readiness to ensure proper handling of PublicAccessAuthenticationToken.
ejbca           | 2024-03-19 12:51:24,471+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) Health check now reports application status at /ejbca/publicweb/healthcheck/ejbcahealth
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *****************************************************************************************
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *                                                                                       *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) * A fresh installation was detected and a ManagementCA was created for your initial     *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) * access to the system.                                                                 *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *                                                                                       *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *   URL:      https://ejbca-node1:443/ejbca/adminweb/                                   *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *                                                                                       *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) * If you use different port mapping or a proxy, please adapt the URL above accordingly. *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *                                                                                       *
ejbca           | 2024-03-19 12:51:24,484+0000 INFO  [/opt/keyfactor/bin/start.sh] (process:1) *****************************************************************************************

Gestion du service

Pour gérer plus facilement l'état de l'instance (démarrage, redémarrage, arrêt, status), j'ai créé le script suivant :

touch /opt/ejbca/manage-ejbca-daemon.sh
chmod +x /opt/ejbca/manage-ejbca-daemon.sh
nano /opt/ejbca/manage-ejbca-daemon.sh

#!/bin/bash

##########################################
#                                        #
#   Script de gestion du service EJBCA   #
#                                        #
##########################################

#===========#
# Variables #
#===========#

# Dossier de travail
WRKDIR=/opt/ejbca/containers

# Nom de l'instance ebjca
EJBCA=ejbca

# Nom de l'instance ebjca-database
EJBDB=ejbca-database

#===========#
# Fonctions #
#===========#

# Demande d'aide
get-help() {
        echo "Script de gestion du service EJBCA"
        echo ""
        echo "Usage : ./manage-ejbca-daemon.sh [backup|help|restart|start|status|stop|upgrade]"
        echo ""
        echo "backup                    Sauvegarde la base de données."
        echo "help                      Affiche cette aide."
        echo "restart                   Redémarre l'instance."
        echo "start                     Démarre l'instance."
        echo "status                    Affiche l'état de l'instance."
        echo "stop                      Arrête l'instance."
        echo "upgrade                   Met à jour l'instance."
}

#=======================#
# Gestion des arguments #
#=======================#

case $1 in
        "start"|"restart"|"stop"|"status"|"upgrade"|"backup")
                ACTION="$1"
                ;;
        "help")
                get-help
                exit 0
                ;;
        *)
                echo "Erreur : Argument $1 inconnu"
                echo ""
                get-help
                exit 2
                ;;
esac

#============#
# Traitement #
#============#

EJBCA_STATUS=$(docker container inspect -f '{{.State.Running}}' $EJBCA 2>&1)
EJBDB_STATUS=$(docker container inspect -f '{{.State.Running}}' $EJBDB 2>&1)

if [ "$EJBCA_STATUS" == "true" -a "$EJBDB_STATUS" == "true" ]; then
        STATUS=0
elif [ "$EJBCA_STATUS" == "true" -a "$EJBDB_STATUS" != "true" ]; then
        STATUS=1
elif [ "$EJBCA_STATUS" != "true" -a "$EJBDB_STATUS" == "true" ]; then
        STATUS=2
else
        STATUS=3
fi

case $ACTION in
        "start")
                # Vérification de l'état et action
                case $STATUS in
                        0)
                                # Démarré
                                echo "Instance déjà démarré"
                                CODE=1
                                ;;
                        1|2)
                                # Partiel
                                echo "Redémarrage de l'instance"
                                cd $WRKDIR && docker compose restart
                                CODE=0
                                ;;
                        3)
                                # Arrêté
                                echo "Démarrage de l'instance"
                                cd $WRKDIR && docker compose up -d
                                CODE=0
                                ;;
                        *)
                                # Autre
                                echo "Erreur status : $STATUS"
                                CODE=3
                                ;;
                esac
                ;;
        "stop")
                # Vérification de l'état et action
                case $STATUS in
                        0|1|2)
                                # Démarré
                                echo "Arrêt de l'instance"
                                cd $WRKDIR && docker compose down
                                CODE=0
                                ;;
                        3)
                                # Arrêté
                                echo "Instance déjà à l'arrêt"
                                CODE=1
                                ;;
                        *)
                                # Autre
                                echo "Erreur status : $STATUS"
                                CODE=3
                                ;;
                esac
                ;;
        "restart")
                # Vérification de l'état et action
                case $STATUS in
                        0)
                                # Démarré
                                echo "Redémarrage de l'instance"
                                cd $WRKDIR && docker compose restart
                                CODE=0
                                ;;
                        1|2)
                                # Partiel
                                echo "Instance partiellement démarrée, redémarrage complet..."
                                cd $WRKDIR && docker compose down && docker compose up -d
                                CODE=1
                                ;;
                        3)
                                # Arrêt
                                echo "Instance à l'arrêt, démarrage..."
                                cd $WRKDIR && docker compose up -d
                                CODE=1
                                ;;
                        *)
                                # Autre
                                echo "Erreur status : $STATUS"
                                CODE=3
                                ;;
                esac
                ;;
        "status")
                # Vérification de l'état et action
                case $STATUS in
                        0)
                                # Démarré
                                echo "Autorité de certification et base de données actives"
                                CODE=0
                                ;;
                        1)
                                # Base de données à l'arrêt
                                echo "Autorité de certification active mais la base de données est à l'arrêt"
                                CODE=1
                                ;;
                        2)
                                # Authorité à l'arrêt
                                echo "Base de données active mais l'autorité de certification est à l'arrêt"
                                CODE=1
                                ;;
                        3)
                                # Arrêt
                                echo "Autorité de certification et base de données à l'arrêt"
                                CODE=2
                                ;;
                        *)
                                # Autre
                                echo "Erreur status : $STATUS"
                                CODE=3
                                ;;
                esac
                ;;
        "backup")
                # Vérification de l'état et action
                case $STATUS in
                        0)
                                # Démarré
                                echo "Sauvegarde en cours..."
                                Version=$(curl -k -s https://localhost/ejbca/ejbca-rest-api/v1/certificate/status | jq .revision | awk '{print $2}')
                                UserName=$(grep 'MYSQL_USER' $WRKDIR/docker-compose.yml | cut -d'=' -f2)
                                PassWord=$(grep 'MYSQL_PASSWORD' $WRKDIR/docker-compose.yml | cut -d'=' -f2)
                                cd $WRKDIR && docker compose exec $EJBDB mysqldump ejbca -u${UserName} -p${PassWord} > ejbca-${Version}-backup-$(date +%Y%m%d_%H%M_%Z).sql
                                if [[ $? -eq 0 ]]; then
                                        # Sauvegarde réussie
                                        echo "Sauvegarde réussie"
                                        CODE=0
                                else
                                        # Sauvegarde raté
                                        echo "Sauvegarde en échec"
                                        CODE=2
                                fi
                                ;;
                        *)
                                # Autres
                                echo "L'instance n'est pas correctement démarrée. Sauvegarde impossible."
                                CODE=2
                                ;;
                esac
                ;;
        "upgrade")
                # On arrête l'instance
                echo "Arrêt de l'instance"
                cd $WRKDIR && docker compose down

                # Mise à jour
                echo "Téléchargement de la dernière version"
                cd $WRKDIR && docker image pull keyfactor/ejbca-ce:latest

                # Redémarrage de l'instance
                echo "Redémarrage et mise à jour"
                cd $WRKDIR && docker compose up -d
                ;;
        *)
                # Autre
                echo "Erreur action : $ACTION"
                CODE=3
                ;;
esac

#========#
# Sortie #
#========#

exit $CODE

Pour que l'instance démarre automatiquement au démarrage de la VM, on édite alors la crontab de root :

crontab -e

Et on ajoute la ligne suivante :

@reboot         /opt/ejbca/manage-ejbca-daemon.sh start

Création du compte administrateur

Nomenclature

Du point de vue de EJBCA, tous les utilisateurs sont des administrateurs. Les comptes sont déterminés selon ces différents types :
  • SuperAdmin : Compte d'administration ayant tous les droits.
  • Admin : Compte d'administration avec des droits limités.

De mon côté, je pars toujours du fait qu'un administrateur a toujours tous les droits. Lorsque les droits sont limités, on est alors sur un profils d'utilisateur (User) ou d'utilisateur privilégié (PowerUser).

Du coup, dans la suite de cette procédure, j'utiliserais les termes suivant :

  • Administrateur (Admin) pour le rôle SuperAdmin de EJBCA
  • Utilisateur (User) ou Utilisateur privilégié (PowerUser)pour le rôle Admin de EJBCA

Création du certificat

Import du certificat dans le navigateur

Navigateur

J'utilise principalement Firefox en tant que navigateur internet. La procédure est donc basée sur ce dernier mais reste relativement similaire sur les autres navigateurs.

Création du compte sur EJBCA

/opt/ejbca/manage-ejbca-daemon.sh restart
cd /opt/ejbca/containers/ && docker compose logs -f | grep 'Health ckeck now reports application'

Test et validation

Décisions d'authentification

Si à la reconnexion, vous n'avez pas de demande de certificat, c'est très probablement parce que Firefox a enregistré la décision de ne pas en envoyer. Pour réinitialiser cette décision, il suffit de supprimer l'entrée correspondante à l'adresse du serveur dans le gestionnaire de certificats de Firefox (ouvert lors de l'enregistrement du certificat), onglet Décisions d'authentification. Une fois la réinitialisation faîte, on peut retenter la connexion.

Echec d'authentification

Si vous ne vous êtes pas connecté grâce au certificat, alors vous aurez une erreur et vous ne pourrez pas supprimer l'accès publique.

Configuration pour les accès API

Si on souhaite interagir avec les autorités de certifications via API (pour de l'automatisation par exemple), il convient de le configurer le service et l'utilisateur au niveau de l'interface d'administration :

Service

Utilisateur

Automatisation

A ce niveau, le service API est disponible sur notre infrastructure. Les administrateurs peuvent l'utiliser avec leur certificat. Cependant, dans le cadre d'une automatisation, il est dangereux d'utiliser un compte avec trop de droits et notamment celui d'un admin. Il faut donc créer un utilisateur avec des droits se limitant au strict nécessaire. On va donc créer un profil pour créer, interroger et révoquer des certificats TLS uniquement.
Catégorie Droits Autorisation
Role Based Access Rules /administrator/ Allow
Regular Access Rules /ca_functionality/create_certificate/ Allow
/ca_functionality/use_username/ Allow
/ra_functionality/edit_end_entity/ Allow
/ra_functionality/revoke_end_entity/ Allow
CA Access Rules /ca/<votre autorité intermédiaire>/ Allow
End Entity Profile Access Rules /endentityprofilesrules/<votre profil de certificat TLS>/ Allow
/endentityprofilesrules/<votre profil de certificat TLS>/approve_end_entity/ Deny
/endentityprofilesrules/<votre profil de certificat TLS>/delete_end_entity/ Deny
/endentityprofilesrules/<votre profil de certificat TLS>/view_end_entity/ Deny
/endentityprofilesrules/<votre profil de certificat TLS>/view_end_entity_history/ Deny

Bot

Voilà, on dispose désormais d'un utilisateur authentifié avec un certificat et un mot de passe et disposant de droits limités.

Supervision

Pour la supervision dans Nagios, nous allons contrôler 4 éléments :

Ci-dessous, je dépose les scripts que j'utilise pour les différents contrôles

L'état du service docker

On se base sur les plugins de nagios avec la commande :

/usr/lib/nagios/plugins/check_procs -w 1:1 -c 0: -C dockerd

L'état des différents containers

J'utilise le script de Tim Laurence (Source : GitHub) pour contrôler l'état des containers. Une fois le script installé, il suffit de lancer la commande suivante :

/usr/lib/nagios/plugins/check_docker.py --no-ok --status running --containers ejbca ejbca-database

L'état de santé de la PKI

La PKI réalise une vérification automatique de son état de santé. Son rapport est affiché à l'URI /ejbca/publicweb/healthcheck/ejbcahealth. Le script suivant interroge cette adresse et renvoie l'information au format NRPE :

#!/bin/bash

# Adresse du serveur
ServerAddress="localhost"

# URI de l'état de santé
Uri="/ejbca/publicweb/healthcheck/ejbcahealth"

# Requête
Request=$(curl -k -s https://${ServerAddress}${Uri})

# Traitement du résultat de la requête
if [ "$Request" == "ALLOK" ]; then
        # Tout va bien
        Status="GOOD"
        ExitCode=0
else
        # PKI en erreur
        Status="CRIT"
        ExitCode=2
fi

# Renvoi des informations
echo "$Status - Health status : $Request"
exit $ExitCode

La présence de mises à jour

On vérifie la présence de mise à jour en comparant le hash de la version actuelle de notre PKI avec le hash de la dernière version.

#!/bin/bash

# Adresse du serveur
ServerAddress="localhost"

# Uri du status
ServerUri="/ejbca/ejbca-rest-api/v1/certificate/status"

# Nom du container
Container="keyfactor/ejbca-ce"

# Récupération de la version actuelle
Actual=$(curl -k -s https://${ServerAddress}${ServerUri} | jq .revision | awk '{print $2}')

# Récupération du hash de la version actuelle
ActualHash=$(docker manifest inspect ${Container}:${Actual} | jq .config.digest)

# Récupération du hash de la dernière version
LatestHash=$(docker manifest inspect ${Container}:latest | jq .config.digest)

# Test et retour
if [ "$ActualHash" == "$LatestHash" ]; then
        # Les hash sont les mêmes, on est à jour
        echo "GOOD - up-to-date"
        exit 0
else
        # Une mise à jour est disponible
        echo "WARN - update available"
        exit 1
fi

Conclusion

Opérationnel

A ce stade, vous avez une infrastructure opérationnelle. Félicitations.

On peut désormais créer des autorités de certifications : EJBCA - Création de certificats racine et intermédiaire