Serveur de clé distant sécurisé

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>