Page mise à jour le 30 avril 2024
Dans cet article, j’explique comment j’ai mis en place et utilise un serveur de clé distant sécurisé.
Problématique et cahier des charges
La problématique est la suivante :
- Lorsque l’on dispose de serveurs physiques (à son domicile ou au travail) ou hébergés (cloud, serveur dédié, …) ;
- Qui hébergent des services sécurisés, par exemple des volumes disque chiffrés (Luks ou autre) ;
- Dont la clé est par exemple nécessaire lors d’un redémarrage ;
- Que l’on ne souhaite pas stocker localement la clé nécessaire à ces services (en cas de vol, d’intrusion physique) ;
- Il est pénible de devoir entrer manuellement la clé (via une connexion SSH ou autre).
Il est alors souhaitable d’avoir un service qui :
- Fournisse la ou les clés demandées ;
- De manière distante (service non local) ;
- De manière sécurisée ;
- Avec administration distante et logs ;
- Avec possibilité de révocation de l’autorisation de délivrance des clés (en cas de vol d’un serveur, d’intrusion).
Solution
Pour mes besoins, j’ai mis en place une solution maison.
Un serveur web tiers (HTTPS, PHP) héberge les clés
- L’accès est sécurisé :
- Filtrage des connections par .htaccess (via l’IP du demandeur) ;
- Délivrance de la clé par cohérence d’un couple fournit par le demander : {asker ; key_asked ; password} ;
- L’administration est aisée :
- Modification du script PHP à distance pour ajouter/modifier/supprimer une clé ;
- Un mail est envoyé à chaque connexion.
Les services utilisateurs demandent la clé par requête HTTPS
- Par exemple, avec wget ;
- Le serveur de clé reçoit et traite la requête, si le couple {asker ; key_asked ; password} est correct, la clé est délivrée et directement utilisable.
Mise en oeuvre du serveur de clé HTTPS
Protection du script pat htaccess
Afficher ce script au format texte
<Limit GET POST> Order Deny,Allow Deny from all Allow from .your-domain.com Allow from 111.111.111.111 Allow from 2222:2222:2222:2222::1 </Limit> Options -Indexes
Script PHP
Afficher ce script au format texte
<?php ####################### # Info # ####################### #SCRIPT_NAME= #SCRIPT_AUTHOR="Valérian REITHINGER (@:valerian@reithinger.fr ; web:www.valerian.reithinger.fr)" #SCRIPT_VERSION="1.2 (05/jan/2018)" #SCRIPT_QUICK_DESCRIPTION="Give a key with correct {asker ; key_asked ; password}" ########################### # CONFIG # ########################### $DEBUG=false; $MAIL_TO = "your@mail.fr"; $MAIL_FROM = "keyfeeder@mydomaine.com"; ########################### # REQUEST INFO # ########################### $SERVER_HOST = $_SERVER['HTTP_HOST']; $REQUEST_DATE = date("d/m/Y H:i"); $ASKER_IP = $_SERVER['REMOTE_ADDR']; $ASKER_HOST = gethostbyaddr($ASKER_IP); $ASKER_IPINFO_LINK = 'http://ipinfo.io/'.$ASKER_IP; $ASKER_AGENT = $_SERVER['HTTP_USER_AGENT']; ########################### # ARGS # ########################### $asker=$_GET['asker']; $askedkey=$_GET['askedkey']; $password=$_GET['password']; $GivenArgs=(isset($_GET['asker']) + isset($_GET['askedkey']) + isset($_GET['password']) ); ################################## # SUB_FUNCTIONS # ################################## function send_mail() { global $MAIL_TO, $MAIL_FROM ; global $SERVER_HOST, $REQUEST_DATE, $ASKER_IP, $ASKER_HOST, $ASKER_IPINFO_LINK, $ASKER_AGENT ; global $asker, $askedkey, $GivenArgs ; global $STATUS ; $MAIL_subject = "[www.keyfeeder.val-r.fr] Someone asked me for a key"; $MAIL_headers ='FROM: '.$MAIL_FROM."\r\n"; $MAIL_headers .='Reply-To: '.$MAIL_FROM."\r\n"; $MAIL_headers .='Content-Type: text/plain; charset="iso-8859-1"'."\r\n"; $MAIL_headers .='Content-Transfer-Encoding: 8bit'; $MAIL_message = 'Hi, this is givemethekey.php on '.$SERVER_HOST.', someone asked me for a key:'."\r\n"."\r\n"; $MAIL_message .= '* on '.$REQUEST_DATE."\r\n"; $MAIL_message .= '* client '.$ASKER_IP.' = '.$ASKER_HOST.' ('.$ASKER_IPINFO_LINK.')'."\r\n"; $MAIL_message .= '* asked a key with '.$GivenArgs.' arg(s) :'."\r\n"; $MAIL_message .= ' * asker: '.$asker."\r\n"; $MAIL_message .= ' * key: '.$askedkey."\r\n"; $MAIL_message .= ' * password: '.'********'."\r\n"; $MAIL_message .= ' * agent: '.$ASKER_AGENT."\r\n"; $MAIL_message .= "\r\n"; $MAIL_message .= 'The Request was: '.$STATUS."\r\n"; mail($MAIL_TO, $MAIL_subject, $MAIL_message, $MAIL_headers); } ################################## # MAIN # ################################## $STATUS="empty status" ; if($GivenArgs != 3) { $STATUS = 'ERROR: All the mandatory args are not given!'; echo $STATUS; send_mail(); exit(); }<span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span> ########################### # FEEDER # ########################### switch($asker) { #****************************** # test.domain.com #****************************** case 'test.domain.com': if( ($ASKER_IP!='111.111.111.111') && (substr_compare($ASKER_IP, "1111:1111:1111:1111", 0, 19, true)!=0) ) { $STATUS = 'ERROR: asker IP="'.$ASKER_IP.'" is NOT authorized for this asker !'; echo $STATUS; send_mail(); exit(); } switch($askedkey) { case 'theKeyForSomething': if($password=='thePassword'){ echo 'This Is The Password ;-)'; $STATUS = "OK"; send_mail(); exit(); } else{ $STATUS = 'ERROR: wrong password!'; echo $STATUS; send_mail(); exit(); } break; default : $STATUS = 'ERROR: askedkey="'.$askedkey.'" is unknown !'; echo $STATUS; send_mail(); exit(); break; } break; #****************************** # unknow asker #****************************** default : $STATUS = 'ERROR: asker="'.$asker.'" is unknown !'; echo $STATUS; send_mail(); exit(); break; } ?>
Exemple d’appel du serveur de clé
Voici un script bash que j’utilise sur mes serveurs pour monter automatiquement au démarrage (via crontab @reboot) un volume chiffré :
Afficher ce script au format texte
#!/bin/bash ####################### # Config # ####################### DEBUG=false HOST_TO_PING="google.com" #### LOG #### LOGPATH="/var/log/my_logs/disks" #### KEY FEEDER via HTTPS #### KF_HTTPS_HOST="www.thekeyserver.domain.com" KF_HTTPS_SUBDIR="subdir/disks-keys" KF_HTTPS_SCRIPT="givemethekey.php" KF_HTTPS_ASKER="myserver.domain.com" KF_HTTPS_ASKEDKEY="myEncryptedDisk" KF_HTTPS_PASSWORD="abcdefghijklmno" #### MountLuks args #### MNT_DISK="/dev/mdX" MNT_MAPPER="mdX_crypt" MNT_MOUNTP="/mnt/mdX_open" ####################### # Info # ####################### SCRIPT_NAME=$(echo $0 | sed "s/^.*\///g") #sed in order to only keep script's name, without dir path where script is SCRIPT_AUTHOR="Valérian REITHINGER (@:valerian@reithinger.fr ; web:www.valerian.reithinger.fr)" SCRIPT_VERSION="1.1 (04/jan/2018)" SCRIPT_QUICK_DESCRIPTION="Mount the Luks encrypted filesystem: $MNT_MAPPER via keyfeeder: $KF_HTTPS_HOST" SCRIPT_ARG_MIN_NB=0 # optional arg(s) not counted SCRIPT_ARG_MAX_NB=0 # optional arg(s) not counted ####################### # Versions # ####################### # 1.0 (25/apr/2017) created # 1.1 (04/jan/2018) light the code ####################### # Dependencies # ####################### # * 'MountLuks' script, tested # * 'wget' command, tested ####################### # To Do # ####################### # ####################### # Tests # ####################### # On Debian with V1.1 : OK ############################################################################################ # MAIN sub-Functions # ############################################################################################ ###################################### # check dependencies # ###################################### CheckDependencies() { if hash MountLuks 2>/dev/null; then : else EchoErrorMsg "no 'MountLuks' script was found on this system!" exit -1 fi if hash wget 2>/dev/null; then : else EchoErrorMsg "no 'wget' command was found on this system!" exit -1 fi } ############################### # DisplayHelpMsg # ############################### # Display help msg DisplayHelpMsg() { echo "$SCRIPT_NAME: $SCRIPT_QUICK_DESCRIPTION" echo " Usage: $SCRIPT_NAME" echo " Optional args:" echo " -v or --verbose : verbose mode" echo " -q or --quiet : quiet mode (do not display anything, except warnings or errors)" echo " Other args:" echo " -h or --help : display this help message" echo " --version : display script version" echo " Example: $ $SCRIPT_NAME" echo " Then, the process run automaticaly" echo " Location: $0" echo " Version: $SCRIPT_VERSION" echo " Author: $SCRIPT_AUTHOR" } ################################### # Print script version # ################################### DisplayDescriptionMsg() { echo $SCRIPT_QUICK_DESCRIPTION } ############################### # DisplayVersionMsg # ############################### DisplayVersionMsg() { echo $SCRIPT_VERSION } ############################### # CheckArgs # ############################### CheckArgs() { #Check args (nb needed, help, version, ...) ARG_NB=$1 #nb of args given to this script TAB_ARGS=("${@}") #array of args TAB_ARGS=("${TAB_ARGS[@]:1}") #(need to remove 1st element = nb of args) if $DEBUG; then EchoDebugMsg "$ARG_NB arg(s) given : ${TAB_ARGS[@]}"; fi #------------------------------ # Init variables - #------------------------------ VERBOSE=false QUIET_MODE=false NOToptARGS=0 #------------------------------ # Loop on args - #------------------------------ for arg in "${TAB_ARGS[@]}" do if $DEBUG; then EchoDebugMsg "arg: $arg" ; fi case "$arg" in #------------------------------ # OPT args - #------------------------------ #Help asked ? "--help"|"-h") DisplayHelpMsg exit 0 ;; #Version asked ? "--version") DisplayVersionMsg exit 0 ;; #Description asked ? "--description") DisplayDescriptionMsg exit 0 ;; #verbose ? "--verbose"|"-v") VERBOSE=true ;; #quiet mode "--quiet"|"-q") QUIET_MODE=true ;; #------------------------------ # not OPT args - #------------------------------ *) ((NOToptARGS++)) ;; esac done #-------------------------------- # verbose mode is stronger than quiet mode #-------------------------------- if $VERBOSE; then if $QUIET_MODE; then QUIET_MODE=false EchoWarningMsg "you asked both verbose and quiet mode, verbose mode is stronger" fi fi #-------------------------------- # check min/max of not opt args - #-------------------------------- if [ $NOToptARGS -lt $SCRIPT_ARG_MIN_NB ] ; then EchoErrorMsg "not enough arguments given! ($ARG_NB<$SCRIPT_ARG_MIN_NB=min). Display help:" DisplayHelpMsg; exit 1 elif [ $NOToptARGS -gt $SCRIPT_ARG_MAX_NB ] ; then EchoErrorMsg "to much arguments given! ($ARG_NB>$SCRIPT_ARG_MAX_NB=max, see help with -h)" exit 1 fi } ############################# # EchoTXTColors # ############################# EchoInGreen() { echo -n -e "\033[39;32;49m" #$(BashTextStyles green) } EchoInRed() { echo -n -e "\033[39;31;49m" #$(BashTextStyles red) } EchoInYellow() { echo -n -e "\033[39;33;49m" #$(BashTextStyles yellow) } EchoInCyan() { echo -n -e "\033[39;36;49m" #$(BashTextStyles cyan) } EchoInLBlue() { echo -n -e "\033[39;94;49m" #$(BashTextStyles light-blue) } EchoInLBlack() { echo -n -e "\033[39;90;49m" #$(BashTextStyles light-black) } EchoInDefaultStyle() { echo -n -e "\033[39;0;49m" #$(BashTextStyles default) } ############################# # EchoXXXXXMsg # ############################# EchoErrorMsg() { EchoInRed echo -n "[ERROR] " EchoInDefaultStyle echo "$1" } EchoWarningMsg() { EchoInYellow echo -n "[WARNING] " EchoInDefaultStyle echo "$1" } EchoDebugMsg() { EchoInLBlack echo -n "[DEBUG] " EchoInDefaultStyle echo "$1" } EchoVerboseMsg() { EchoInLBlue echo -n "[VERBOSE] " EchoInDefaultStyle echo "$1" } EchoOKMsg() { EchoInGreen echo -n "[OK] " EchoInDefaultStyle echo "$1" } ########################################################################## # " MAIN SubFunctions" # ########################################################################## ############################# # PrepareLog # ############################# PrepareLog() { LOGFILENAME="$SCRIPT_NAME" LOGFILENAME+=".log" LOGFILE="$LOGPATH/$LOGFILENAME" if $VERBOSE; then EchoVerboseMsg "Check log ability for $LOGFILE"; fi if [[ -d $LOGPATH ]]; then if $VERBOSE ; then EchoOKMsg "$LOGPATH exists"; fi else $(mkdir -p $LOGPATH) if [[ -d $LOGPATH ]]; then EchoWarningMsg "$LOGPATH created, check permissions!" else EchoErrorMsg "$LOGPATH can NOT be created, quit" exit -1 fi fi if [[ -e $LOGFILE ]]; then if $VERBOSE ; then EchoOKMsg "$LOGFILE ever exists"; fi else $(touch $LOGFILE) if [[ -e $LOGFILE ]]; then EchoWarningMsg "$LOGFILE created" else EchoErrorMsg "$LOGFILE can NOT be created, quit" exit -1 fi fi } ############################# # PrepareLog # ############################# EchoAndLog() { echo "$@"; echo "$@" >> $LOGFILE; } EchoAndLogERROR() { echo "$@" 1>&2; echo "$@" >> $LOGFILE; } ################################ # CheckInternetLink # ################################ CheckInternetLink() { EchoAndLog "-> Test Internet Link: ping $HOST_TO_PING ..." PING=$(ping -q -f -c 10 $HOST_TO_PING) PING_RESULT=$? if [ "$PING_RESULT" == "0" ]; then EchoAndLog " |-> OK, $HOST_TO_PING respond to ping" else EchoAndLog " |-> $HOST_TO_PING do NOT respond to ping, wait 15s ..." sleep 15 EchoAndLog " |-> 2nd try to ping $HOST_TO_PING ..." PING=$(ping -q -f -c 10 $HOST_TO_PING) PING_RESULT=$? if [ "$PING_RESULT" == "0" ]; then EchoAndLog " |-> OK, $HOST_TO_PING respond to ping" else EchoAndLog " |-> $HOST_TO_PING do NOT respond to ping, wait 15s ..." sleep 15 EchoAndLog " |-> 3rd try to ping $HOST_TO_PING ..." PING=$(ping -q -f -c 10 $HOST_TO_PING) PING_RESULT=$? if [ "$PING_RESULT" == "0" ]; then EchoAndLog " |-> OK, $HOST_TO_PING respond to ping" else EchoAndLogERROR " |-> $HOST_TO_PING do NOT respond to ping after 3 tries, quit" exit 1 fi fi fi } ################################ # AskTheKey # ################################ AskTheKey() { KF_HTTPS_FULLURL="https://$KF_HTTPS_HOST/$KF_HTTPS_SUBDIR/$KF_HTTPS_SCRIPT?asker=$KF_HTTPS_ASKER&askedkey=$KF_HTTPS_ASKEDKEY&password=$KF_HTTPS_PASSWORD" KF_WGET_ARGS="-q -O - -U firefox" EchoAndLog "-> Asking the key ..." EchoAndLog " |-> Sending wget request to $KF_HTTPS_HOST to get the key for $KF_HTTPS_ASKEDKEY ..." KF_ANSWER="`wget $KF_WGET_ARGS $KF_HTTPS_FULLURL`" KF_RETURN=$? if ! [ "$KF_RETURN" == "0" ]; then EchoAndLogERROR " |-> I failed to get the key $KF_HTTPS_ASKEDKEY from $KF_HTTPS_HOST: wget returned $KF_RETURN" exit 1 else EchoAndLog " |-> OK, I got an aswer" fi EchoAndLog " |-> Checking the answer of wget request ..." if [[ $KF_ANSWER == *"ERROR"* ]]; then EchoAndLogERROR " |-> I failed to get the key '$KF_HTTPS_ASKEDKEY' from '$KF_HTTPS_HOST' : wget answer '$KF_ANSWER'" exit 1 else KF_KEY="$KF_ANSWER" EchoAndLog " |-> OK, no obvious error, I think I got the key" fi } ################################ # MountTheDisk # ################################ MountTheDisk() { EchoAndLog "-> Trying to mount the disk '$MNT_DISK' via mapper '$MNT_MAPPER' on '$MNT_MOUNTP' with command 'MountLuks'..." MountLuks $MNT_DISK $MNT_MAPPER $MNT_MOUNTP $KF_KEY MOUNTLUKS_RETURN=$? if ! [ "$MOUNTLUKS_RETURN" == "0" ]; then EchoAndLogERROR " |-> Can't mount !!!" exit 1 else EchoAndLog " |-> OK, successfully mounted ;-)" exit 0 fi } ############################################################################################ # " MAIN " # ############################################################################################ #Check Dependencies CheckDependencies #Check args (nb needed, help, version, ...) CheckArgs $# $* #Check the log file PrepareLog EchoAndLog "*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-" EchoAndLog "[$(date "+%Y.%m.%d_%H:%M:%S")] [$(whoami)] [$0]" CheckInternetLink AskTheKey MountTheDisk exit 1 <span data-mce-type="bookmark" style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" class="mce_SELRES_start"></span>