
Il protocollo SSH viene utilizzato dai sistemisti per collegarsi da remoto alle shell dei servers e tipicamente viene fortemente sconsigliato esporre questo servizio su reti non sicure, come ad esempio su internet.
L’utilizzo delle chiavi al posto delle credenziali tradizionali (username e password) per effettuare l’autenticazione può mitigare alcuni problemi di sicurezza, tuttavia in caso di vulnerabilità, potrebbe comunque esserne rischiosa l’esposizione.
Mediante HAProxy è possibile configurare un bastione SSH che permetta di collegarsi ai servers solo in seguito all’autenticazione mediante mTLS con certificati SSL, in modo che gli utenti non autorizzati non possano raggiungere i servers remoti in SSH.
In questo articolo vengono mostrati i passaggi necessari per la configurazione del cluster Keepalived e per la configurazione di HAProxy con gli scripts LUA, nei prossimi due articoli verranno mostrati i passaggi di configurazione della Enterprise CA di Windows e la configurazione sui clients.
REQUISITI
Affinché sia possibile configurare un cluster in alta affidabilità con Keepalived e HAProxy, sono necessari due servers che condividono un collegamento network L2 (con due IP sulla stessa subnet).
Allo scopo della stesura di questo articolo sono stati utilizzati due server Debian 13, se si utilizzano altre distribuzione alcuni comandi potrebbero variare.
Inoltre, per il rilascio dei certificati TLS/SSL è necessaria una CA privata, nel prossimo articolo verranno mostrati i passaggi di configurazione per la Enterprise CA di Windows, ma in alternativa potrebbe venir utilizzata una CA OpenSSL.
SCHEMA DI RETE
In seguito lo schema di rete che raffigura l’architettura proposta in questo articolo:
Come si può notare dal disegno, il mTLS viene utilizzato tra HAProxy ed i clients, invece le connessioni verso i servers interni sono cifrate ed autenticate solamente mediante il procollo SSH.
CONFIGURAZIONE PARAMETRI DEL KERNEL
Prima di proseguire con l’installazione dei servizi, è necessaria la modifica di alcuni parametri del kernel, in particolare il primo parametro viene utilizzato per disabilitare l’IPv6 (operazione consigliata, ma non obbligatoria) ed il secondo per consentire ad HAProxy di mettersi in ascolto su IP che non gli appartengono (operazione obbligatoria per effettuare il bind sui VIP).
Eseguire i seguenti comandi come root su una bash al fine di creare i files di configurazione ed applicare le modfiche:
|
0
1
2
|
echo 'net.ipv6.conf.all.disable_ipv6=1' > /etc/sysctl.d/70-disable-ipv6.conf
echo 'net.ipv4.ip_nonlocal_bind=1' > /etc/sysctl.d/71-allow-nonlocal-bind.conf
sysctl --system
|
MODIFICA LISTEN ADDRESS SSHD
Al fine di evitare che il demone SSH sui servers HAProxy sia in ascolto anche sul VIP, rendendone impossibile l’utilizzo per il bastione, è possibile modificarne la configurazione.
Eseguire i seguenti comandi su una bash come root, opzionalmente è possibile sostituire il valore di LISTEN_ADDRESS nella prima riga con un IP qualunque del server:
|
0
1
|
LISTEN_ADDRESS=$(ip a | grep inet | grep -v '127.0' | head -n1 | awk '{print $2}' | cut -d/ -f1)
echo "ListenAddress $LISTEN_ADDRESS" > /etc/ssh/sshd_config.d/hardening.conf
|
Verificare che nel file /etc/ssh/sshd_config.d/hardening.conf sia presente l’IP corretto e riavviare il servizio:
|
0 |
systemctl restart sshd.service
|
COPIA DELLE CHIAVI SSH
Per semplificare la copia delle configurazione tra i due servers HAProxy è possibile generare due coppie di chiavi SSH ed autorizzarle mediante authorized_keys, in modo che non venga richiesta la password quando si esegue “scp”.
Per generare una coppia di chiavi eseguire in una bash come root su entrambi i nodi il seguente comando:
|
0 |
ssh-keygen
|
Copiare la chiave pubblica (/root/.ssh/id_ed25519.pub o /root/.ssh/id_rsa.pub) nel file /root/.ssh/authorized_keys dell’altro nodo.
Se il file non esiste è possibile crearlo, ma in seguito è necessario lanciare il seguente comando per aggiustare i permessi:
|
0 |
chmod 600 /root/.ssh/authorized_keys
|
Se la configurazione è andata a buon fine è possibile accedere in SSH sull’altro nodo come root senza che vengano richieste le credenziali.
INSTALLAZIONE E CONFIGURAZIONE DI KEEPALIVED
Keepalived è il servizio che si occupa di gestire il VIP, in modo che esso venga automaticamente preso in carico dal nodo secondario in caso di fallimento del primario.
Il VIP non è altro che un IP condiviso tra i due servers su cui HAProxy si mette in ascolto per ricevere le richieste destinate al servizio bastione.
Installare il pacchetto necessario eseguendo il seguente comando su una bash come root:
|
0 |
apt-get install -y keepalived
|
Il file /etc/keepalived/keepalived.conf è simile su entrambi i nodi, allo scopo dimostrativo viene utilizzato 192.168.1.100 come VIP e 45 come router ID, ma è possibile modificare entrambi i parametri a patto che essi non siano già utilizati da altri servizi.
Inoltre, è necessario scegliere una password che per motivi di compatibilità si consiglia che non debba superare gli 8 caratteri.
Aggiungre al file /etc/keepalived/keepalived.conf su entrambi i nodi la seguente configurazione:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
global_defs {
notification_email {
marco.valle@pizza.com
}
notification_email_from HOSTNAME-HERE@pizza.com
smtp_server smtp.pizza.local
smtp_connect_timeout 30
router_id HOSTNAME-HERE
vrrp_skip_check_adv_addr
vrrp_garp_interval 0
vrrp_gna_interval 0
}
vrrp_track_process haproxy {
process haproxy
quorum 1
delay 2
}
|
Sostituire HOSTNAME-HERE con l’hostname dei servers e smtp.pizza.local con il server SMTP (smarthost) che si desidera utilizzare, inoltre popolare notification_email con le emails che devono ricevere gli alerts in caso di mal funzionamenti.
Se non si desidera ricevere notifiche via email è possibile escludere dalla configurazione le righe da (incluso) notification_email a (incluso) smtp_connect_timeout.
Sul server primario aggiungere al file /etc/keepalived/keepalived.conf la seguente configurazione:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
vrrp_instance VIP_SSH_BASTION {
state MASTER
interface eth0
virtual_router_id 45
priority 255
advert_int 1
authentication {
auth_type PASS
auth_pass PASSWORD-HERE
}
virtual_ipaddress {
192.168.1.100/24
}
track_process {
haproxy
}
}
|
Sul server secondario aggiungere al file /etc/keepalived/keepalived.conf la seguente configurazione:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
vrrp_instance VIP_SSH_BASTION {
state SLAVE
interface eth0
virtual_router_id 45
priority 254
advert_int 1
authentication {
auth_type PASS
auth_pass PASSWORD-HERE
}
virtual_ipaddress {
192.168.1.100/24
}
track_process {
haproxy
}
}
|
Nelle precedenti configurazioni è necessario sostituire:
- “eth0” con l’interfaccia sui cui i servers condividono il network layer L2 (l’interfaccia su cui entrambi hanno un IP sulla stessa subnet)
- “45” con un numero inferiore a 255 (uguale per entrambi i nodi) che non viene utilizzato come “virtual_router_id” per altri VIP
- “PASSWORD-HERE” con una password di lunghezza inferiore a 8 caratteri (uguale su entrambi i nodi)
- “192.168.1.100/24” con il VIP e la relativa subnet, l’IP si consiglia che appartenga alla subnet dell’interfaccia specificata con la direttiva precedente
Ad esempio:
- haproxy01: 192.168.1.11/24
- haproxy02: 192.168.1.12/24
- VIP: 192.168.1.100/24
Mediante vrrp_track_process e track_process viene specificato che il fallimento del servizio haproxy è sufficiente per effettuare il fallback sul server secondario, altrimenti sarebbe necessaria la sua disconnessione dalla rete.
Per abilitare ed avviare il servizio è possibile eseguire su entrambi i nodi il seguente comando:
|
0 |
systemctl enable --now keepalived.service
|
Finchè il servizio “haproxy” non viene avviato è possibile che il VIP non sia detenuto da nessuno dei due membri del cluster, quando esso sarà disponibile dovrebbe essere possibile visualizzarlo sul server primario mediante il seguente comando:
|
0 |
ip a
|
Il VIP dovrebbe anche rispondere al ping, salvo configurazioni diverse sul firewall.
CONFIGURAZIONE CERTIFICATI SSL SU HAPROXY
Nel prossimo articolo di questa serie verranno mostrati i passaggi per la configurazione della CA per il rilascio dei certificati agli utenti, dalla stessa CA è necessario che venga rilasciato un certificato wildcard per il cluster HAProxy.
I passaggi per il rilascio di un certificato wildcard non verranno descritti nel dettaglio, se si utilizza la Enterprise CA di Microsoft è possibile clonare il template Web Server, selezionando nel tab Subject Name l’opzione Supply in the request, in modo analogo a come verrà mostrato per i certificati dei clients nel prossimo articolo.
Mediante il template clonato, oppure mediante OpenSSL, è necessario aggiungere alla SAN della richiesta il dominio o i domini con wildcard (ad esempio *.pizza.local), il certificato deve consentire Server Authentication tra i propri usi.
È importante utilizzare un certificato wildcard per far sì che la validazione lato client non fallisca quando ci si connette al bastione specificando l’FQDN di un server interno (il cui dominio deve appartenere alla SAN del certificato).
Se i servers appartengono a più domini è possibile eseguire la medesima procedura per importare più di un certificato SSL, oppure specificare più domini nella SAN dello stesso certificato.
I certificati devono essere tutti in formato PEM codificati in Base64, perciò se li si ha generati in formato DER, è necessario convertirli con OpenSSL.
Creare un unico file nella directory /etc/ssl/private con estensione .pem, strutturato come segue:
- Chiave privata
- Certificato HAProxy (wildcard)
- CA intermedia (se presente)
- CA root
Esempio:
|
0
1
2
3
4
5
6
7
8
9
|
$ cat /etc/ssl/private/star.pizza.local.pem
-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
|
Aggiustare i permessi:
|
0
1
|
chmod 600 /etc/ssl/private/star.pizza.local.pem
chown root:root /etc/ssl/private/star.pizza.local.pem
|
I certificati della CA root e di eventuali CA intermedie sono necessari al fine di effettuare la configurazione di HAProxy ed è possibile salvarli in due files distinti nella directory /etc/ssl.
Nella configurazione proposta in seguito verranno utilizzati i files /etc/ssl/MyRootCA.crt e /etc/ssl/MyIntermediateCA.crt.
Si consiglia, inoltre, di importare il certificato della CA root tra quelle fidate, per farlo creare un file con estensione .crt nella directory /usr/local/share/ca-certificates con il contenuto della CA root.
In seguito un esempio:
|
0
1
2
3
|
$ cat /usr/local/share/ca-certificates/MyRootCA.crt
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
|
Aggiornare i certificati fidati:
|
0 |
update-ca-certificates
|
CONFIGURAZIONE DEI REPOSITORIES DI HAPROXY
Questo passaggio è opzionale, poichè Debian 13 mette a disposizione nei propri repositories stable una versione di HAProxy, tuttavia quest’ultima ha come major la 3.0 che non supporta gli attributi della SAN nelle ACL, perciò non è possibile utilizzare l’email per autorizzare gli utenti ad accedere a determinati servers.
Se ci si accontenta del CN e delle OU per la gestione dei permessi è possibile proseguire con l’installazione, altrimenti è necessario seguire i passaggi di questo paragrafo per configurare un repository aggiuntivo da cui è possibile scaricare i pacchetti delle versioni più recenti di HAProxy.
Navigare sul sito del repository (https://haproxy.debian.net/), selezionare il proprio sistema operativo e la versione di HAProxy che si vuole installare:
La versione 3.2-stable (LTS) è perfetta per lo scopo di questo articolo.
Copiare in una bash aperta come root i comandi di configurazione dei repositories:
Nel mio caso i comandi di configurazione dei repositories sono i seguenti:
|
0
1
|
curl --silent https://haproxy.debian.net/haproxy-archive-keyring.gpg --create-dirs --output /etc/apt/keyrings/haproxy-archive-keyring.gpg
echo deb "[signed-by=/etc/apt/keyrings/haproxy-archive-keyring.gpg]" https://haproxy.debian.net trixie-backports-3.2 main > /etc/apt/sources.list.d/haproxy.list
|
Per ricaricare i repositories eseguire il seguente comando:
|
0 |
apt-get update
|
È possibile installare la nuova versione anche se è presente una versione meno recente, poichè viene gestita come un normale aggiornamento software mediante APT.
INSTALLAZIONE DI HAPROXY
Procedere all’installazione di HAProxy su entrambi i servers, eseguendo su una bash come root il seguente comando:
|
0 |
apt-get install -y haproxy
|
Se si vuole installare la versione più recente facendo utilizzo dei repositories aggiuntivi, eseguire al posto del precedente comando il seguente (sostituire 3.2 con la versione per la quale si ha configurato i repositories):
|
0 |
apt-get install -y haproxy=3.2.\*
|
Per abilitare ed avviare il servizio è possibile eseguire su entrambi i nodi il seguente comando:
|
0 |
systemctl enable --now haproxy
|
La configurazione di HAProxy può essere ricca di direttive, perciò verranno descritte solo le parti principali.
Tutte le configurazioni sono contenute nel file /etc/haproxy/haproxy.cfg salvo che venga specificato diversamente.
Aggiungere le opzioni necessarie per il funzionamento degli script LUA sotto la direttiva global:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
#---------------------------------------------------------------------
# Global settings
#---------------------------------------------------------------------
global
# Other options here
### Set the environment variables used in LUA scripts
setenv ACL_CN_PATH /etc/haproxy/acl/authorized_cn.acl
setenv ACL_EMAIL_PATH /etc/haproxy/acl/authorized_emails.acl
setenv ACL_OU_PATH /etc/haproxy/acl/authorized_ou.acl
### Set LUA options
tune.lua.bool-sample-conversion normal
lua-prepend-path /etc/haproxy/lua/?.lua
### Load LUA scripts
lua-load /etc/haproxy/lua/actions.lua
lua-load /etc/haproxy/lua/converters.lua
|
Se si utilizza la versione di HAProxy distribuita con i repositories di Debian commentare la direttiva tune.lua.bool-sample-conversion normal.
Affinché sia possibile utilizzare la risoluzione dei nomi DNS per collegarsi specificando l’FQDN e non l’IP è necessario configurare un resolver:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#---------------------------------------------------------------------
# DNS resolvers
#---------------------------------------------------------------------
resolvers local-dns
accepted_payload_size 8192
nameserver ns1 192.168.10.30:53
nameserver ns2 192.168.10.31:53
resolve_retries 3
timeout resolve 1s
timeout retry 1s
hold other 30s
hold refused 30s
hold nx 30s
hold timeout 30s
hold valid 10s
hold obsolete 30s
|
Sostituire 192.168.10.30 e 192.168.10.31 con gli IP dei server DNS che si desidera utilizzare.
Opzionalmente è possibile abilitare la pagina delle statistiche:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
|
#---------------------------------------------------------------------
# statistics page
#---------------------------------------------------------------------
listen stats
bind 0.0.0.0:9000 interface eth0 ssl crt /etc/ssl/private
mode http
stats enable
stats show-node
stats refresh 10s
stats hide-version
stats realm Haproxy\ Statistics
stats uri /
stats auth USERNAME-HERE:PASSWORD-HERE
|
Nella precedente configurazione sostituire:
- 9000 con la porta che si desidera utilizzare per la pagina delle statistiche
- eth0 con l’interfaccia da cui si vuole raggiungere la pagina delle statistiche
- USERNAME-HERE:PASSWORD-HERE con le credenziali che si vogliono utilizzare (formato username:password)
Se non si desidera utilizzare HTTPS per la connessione alla pagina delle statistiche è possibile omettere le seguenti direttive:
|
0 |
ssl crt /etc/ssl/private
|
Aggiungere il frontend dedicato al bastione SSH:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
#---------------------------------------------------------------------
# fe_ssh frontend
#---------------------------------------------------------------------
frontend fe_ssh
bind 192.168.1.100:22 ssl strict-sni crt /etc/ssl/private verify required ca-file /etc/ssl/MyIntermediateCA.crt ca-verify-file /etc/ssl/MyRootCA.crt
mode tcp
### Log the requests as soon as possible
option logasap
### Configure TCP keepalives
option clitcpka
clitcpka-idle 30s
clitcpka-cnt 3
clitcpka-intvl 10s
### Set the timeouts
timeout client 1h
timeout client-fin 1m
#### Set the logging format
log-format '%ci:%cp [%t] %ft %b/%s %Tw/%Tc/%Tt %B %ts %ac/%fc/%bc/%sc/%rc %sq/%bq dstIP="%[var(sess.dstIP)]" dstName="%[var(sess.dstName)]" dn="%[ssl_c_s_dn]" emails=[%[var(txn.emails)]] '
### Set the destination from SNI
tcp-request content do-resolve(sess.dstIP,local-dns,ipv4) ssl_fc_sni
tcp-request content set-var(sess.dstName) ssl_fc_sni
### Extract the email from client's certificate SAN
tcp-request content set-var(txn.emails) ssl_c_san,lua.get_emails_from_san
#### Drop invalid SSH connections
tcp-request inspect-delay 5s
acl valid_payload req.payload(0,7) -m str "SSH-2.0"
tcp-request content reject if !valid_payload
tcp-request content accept if { req_ssl_hello_type 1 }
default_backend be_ssh_all
|
La direttiva bind è composta dalle seguenti opzioni:
- 192.168.1.100: il VIP configurato con Keepalived
- ssl: abilita il TLS lato HAProxy
- strict-sni: richiede che l’utente debba specificare uno SNI per il quale HAProxy detiene un certificato SSL valido, rifiutando le connessioni con SNI malformato, come ad esempio quelle effettuate dai bots e dai crawlers
- verify required: abilita il mutual TLS
- ca-file /etc/ssl/MyIntermediateCA.crt: direttiva utilizzata per specificare con che CA HAProxy deve validare il certificato del client (sostituire il percorso con il path in cui è stata salvata la CA intermedia, se esiste, oppure in cui è stata salvata la CA root)
- ca-verify-file /etc/ssl/MyRootCA.crt: direttiva opzionale utilizzata per suggerire al client che certificato utilizzare (sostituire il percorso con il path in cui è stata salvata la CA root)
L’opzione option logasap serve a richiedere ad HAProxy di loggare la richiesta appena ricevuta e non quando la connessione SSH viene terminata.
In seguito, vengono abilitati i keepalives per i clients e vengono specificati i timeouts che possono essere modificati a piacere.
L’utilizzo di keepalives è fondamentale in ambienti in cui sono presenti apparati di rete stateful tra il client ed HAProxy, come per esempio i firewalls, oppure i proxy ZTNA.
Con l’opzione log-format viene specificato il formato dei logs, facendo sì che vengano registrate anche la destinazione specificata mediante lo SNI e le informazioni ottenute dai certificato SSL del client.
Nelle due istruzioni tcp-request vengono impostate due variabili di sessione con rispettivamente l’IP, risolto dal DNS se necessario, ed il nome del server di destinazione.
Con l’istrzione tcp-request successiva viene salvata in una variabile della transazione la lista di emails estratta dalla SAN mediante lo script (converter) LUA get_emails_from_san, questa riga deve essere commentata se si uttilizza la versione di HAProxy distribuita dai repositories di Debian.
Le direttive sotto il commento Drop invalid SSH connections servono a consentire solo il traffico SSH per evitare che venga instradato un altro protocollo.
Infine, la direttiva default_backend be_ssh_all indirizza le richieste verso il backend del bastione, dove successivamente verranno verificate le ACL ed avverrà la connessione con il server di destinazione.
Aggiungere le configurazione del backend:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
#---------------------------------------------------------------------
# be_ssh_all backend
#---------------------------------------------------------------------
backend be_ssh_all
mode tcp
### Set timeouts
timeout connect 1m
timeout server 1h
timeout server-fin 1m
timeout tunnel 1h
### Configure TCP keepalives
option srvtcpka
srvtcpka-idle 30s
srvtcpka-cnt 3
srvtcpka-intvl 10s
### Reject the request if dstIP is not defined
tcp-request content reject if ! { var(sess.dstIP) -m found }
### Run validation action
tcp-request content lua.validate_cn_acl
tcp-request content lua.validate_emails_acl
tcp-request content lua.validate_ou_acl
### Check authorizations
acl authorized var(txn.authorized_by_cn) -m bool
acl authorized var(txn.authorized_by_email) -m bool
acl authorized var(txn.authorized_by_ou) -m bool
### Reject unauthorized requests
tcp-request content reject if !authorized
### Set the request destination
tcp-request content set-dst var(sess.dstIP)
server ssh 0.0.0.0:22
|
La configurazione del backend incomicia specificando la modalità tcp ed i timeouts, poi analogamente a come sono stati abilitati nel frontend i keepalives verso il clients, vengono abilitati i keepalives verso i servers.
L’instruzione tcp-request content reject viene utilizzata per rifiutare le richieste il cui IP di destinazione non è definito, ad esempio quando viene richiesto un FQDN non presente sul DNS.
Successivamente vengono eseguiti gli scripts (actions) LUA di validazione delle ACL e poi mediante le tre direttive acl authorized viene effettuato un OR logico tra le variabili di sessione in cui gli script salvano il risultato della validazione.
Le richieste non autorizzate vengono respinte con l’istruzione tcp-request content reject successiva ed infine viene impostata la destinazione e la richiesta viene girata al server richiesto.
Creare la cartella per gli scripts LUA eseguendo:
|
0 |
mkdir -p /etc/haproxy/lua
|
Creare il file /etc/haproxy/lua/acl_cache.lua con il seguente contenuto:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
|
-- Utils module
local utils = require("utils")
-- Module table
local M = {}
-- Define caches
M.caches = {
cn = {},
email = {},
ou = {}
}
-- Function to load an ACLs file into a specific cache
-- @param env_var: The name of the environment variable with ACLs file path
-- @param cache_name: The cache key in the cache map
local function load_acl_to_cache(env_var, cache_name)
-- Load the ACLs file path
local acl_path = os.getenv(env_var)
if not acl_path or not acl_path:find("%S") then
utils.warn(env_var .. " not defined. Skipping " .. cache_name .. " ACL.")
return
end
-- Open the ACLs file
local f = io.open(acl_path, "r")
if not f then
utils.fatal_error("Could not open ACLs file: " .. acl_path)
end
-- Clear/Initialize cache
M.caches[cache_name] = {}
-- Import ACLs into the map
for line in f:lines() do
-- Strip comments and whitespace
local clean_line = line:match("^%s*([^#%s]+)")
if not clean_line then
goto next_line
end
-- Pattern to capture everything before the last
-- colon as key and everything after as the resource
local key, resource = clean_line:match("^(.-):([^:]+)$")
-- Cache ACLs
if key and resource then
if not M.caches[cache_name][key] then
M.caches[cache_name][key] = {}
end
M.caches[cache_name][key][resource] = true
end
::next_line::
end
-- Close the ACLs file
f:close()
end
-- Initialize all caches
core.register_init(function()
load_acl_to_cache("ACL_CN_PATH", "cn")
load_acl_to_cache("ACL_EMAIL_PATH", "email")
load_acl_to_cache("ACL_OU_PATH", "ou")
end)
-- Helper function to perform the check logic
-- @param cache_name: The cache key in the cache map
-- @param list_str: The string containing the list of attributes to be used for validation
-- @param separator: The separator used in "list_str" to distinguish elements
-- @param dst_ip: The destination IP
-- @param dst_name: The destination hostname or FQDN
-- @return: Whether the user is authorized by by the ACLs stored in the cache
function M.check_authorization(cache_name, list_str, separator, dst_ip, dst_name)
-- Skip empty lists
if list_str == "" then return false end
-- Fetch the cache
local cache = M.caches[cache_name]
for item in list_str:gmatch("([^" .. separator .. "]+)") do
-- Check authorization
local allowed = cache[utils.trim(item)]
if allowed and (allowed[dst_ip] or (dst_name ~= "" and allowed[dst_name])) then
return true
end
end
return false
end
-- Return the table so other scripts can 'require' it
return M
|
Creare il file /etc/haproxy/lua/actions.lua con il seguente contenuto:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
-- Utils module
local utils = require("utils")
-- Import ACLs cache
local acl_cache = require("acl_cache")
-- Action for CNs ACLs validation (extracting directly from DN)
-- @param txt: The current HAProxy transaction
core.register_action("validate_cn_acl", { "tcp-req", "http-req" }, function(txn)
-- Extract all CNs from the certificate DN
local cn_list = utils.extract_dn_attributes(txn.f:ssl_c_s_dn(), "CN", ";")
-- Get the destination IP and hostname from the transaction
local dst_ip = txn.get_var(txn, "sess.dstIP") or ""
local dst_name = txn.get_var(txn, "sess.dstName") or ""
-- Check cached authorizations
local is_authorized = acl_cache.check_authorization("cn", cn_list, ";", dst_ip, dst_name)
-- Set the transaction variable
txn.set_var(txn, "txn.authorized_by_ou", is_authorized)
end)
-- Action for emails ACLs validation (using txn.emails)
-- @param txt: The current HAProxy transaction
core.register_action("validate_emails_acl", { "tcp-req", "http-req" }, function(txn)
-- Get the emails from the transaction
local emails = txn.get_var(txn, "txn.emails") or ""
-- Get the destination IP and hostname from the transaction
local dst_ip = txn.get_var(txn, "sess.dstIP") or ""
local dst_name = txn.get_var(txn, "sess.dstName") or ""
-- Check cached authorizations
local is_authorized = acl_cache.check_authorization("email", emails, "; ", dst_ip, dst_name)
-- Set the transaction variable
txn.set_var(txn, "txn.authorized_by_email", is_authorized)
end)
-- Action for OUs ACLs validation (extracting directly from DN)
-- @param txt: The current HAProxy transaction
core.register_action("validate_ou_acl", { "tcp-req", "http-req" }, function(txn)
-- Extract all OUs from the certificate DN
local ou_list = utils.extract_dn_attributes(txn.f:ssl_c_s_dn(), "OU", ";")
-- Get the destination IP and hostname from the transaction
local dst_ip = txn.get_var(txn, "sess.dstIP") or ""
local dst_name = txn.get_var(txn, "sess.dstName") or ""
-- Check cached authorizations
local is_authorized = acl_cache.check_authorization("ou", ou_list, ";", dst_ip, dst_name)
-- Set the transaction variable
txn.set_var(txn, "txn.authorized_by_ou", is_authorized)
end)
Creare il file "/etc/haproxy/lua/converters.lua" con il seguente contenuto:
-- Utils module
local utils = require("utils")
-- Converter for obtaining emails from the certificate SAN
-- @param san: The string containing the certificate SAN
-- @param args: The arguments of the converter ([1] => the separator)
-- @return: The string containing the concatenated emails, using the specified separator
core.register_converters("get_emails_from_san", function(san, args)
-- Check if an argument was passed, otherwise use the default
local separator = (args and args[1] ~= "") and args[1] or "; "
-- Extract emails from the SAN
local emails = {}
for email in san:gmatch("email:([^, ]+)") do
table.insert(emails, utils.trim(email))
end
-- Return the string of all emails found
return table.concat(emails, separator)
end)
Creare il file "/etc/haproxy/lua/utils.lua" con il seguente contenuto:
-- Create a table to hold the module's functions
local M = {}
-- Function to extract specific attributes from a DN string
-- @param dn_string: The full DN string from HAProxy
-- @param attribute: The key to look for (e.g., "CN", "OU", "DC")
-- @param separator: (Optional) The string used to join multiple values. Defaults to ";"
-- @return: A concatenated string of all values found
function M.extract_dn_attributes(dn_string, attribute, separator)
-- Return an empty string if the DN is empty
if not dn_string or dn_string == "" then return "" end
-- Set the separator
local sep = separator or ";"
-- Define the pattern that look for "attribute="
-- followed by characters that aren't "/" or ","
local pattern = attribute .. "=([^/,]+)"
-- Extract the values from the DN
local results = {}
for value in dn_string:gmatch(pattern) do
table.insert(results, M.trim(value))
end
-- Return the string of all values found
return table.concat(results, sep)
end
-- Stop the script and throws a Lua error
-- @param msg: The message to be printed
function M.fatal_error(msg)
core.log(core.alert, "[FATAL] Lua: " .. msg)
error(msg)
end
-- Print an information
-- @param msg: The message to be printed
function M.info(msg)
core.log(core.info, "[INFO] Lua: " .. msg)
end
-- Trim a string
-- @param s: The string to be trimmed
-- @return: The trimmed string
function M.trim(s)
return s:match("^%s*(.-)%s*$")
end
-- Print a warning
-- @param msg: The message to be printed
function M.warn(msg)
core.log(core.warning, "[WARNING] Lua: " .. msg)
end
-- Return the table so other scripts can 'require' it
return M
|
Lo script acl_cache viene utilizzato per caricare le ACL in memoria in fase di caricamento, per evitare inefficenti letture dal file system durante il runtime.
Nello script actions vengono definite le funzioni per la validazione delle ACL che vengono richiamate nella configurazione di HAProxy.
Nello script converters è definita la funzione get_emails_from_san per estrarre le emails dalla SAN, anch’essa richiamata nella configurazione di HAProxy.
Infine, nello script utils sono presenti alcune funzioni utili agli altri scripts.
Su entrambi i servers creare la directory per le ACL ed i files vuoti:
|
0
1
|
mkdir -p /etc/haproxy/acl
touch {/etc/haproxy/acl/authorized_cn.acl,/etc/haproxy/acl/authorized_emails.acl,/etc/haproxy/acl/authorized_ou.acl}
|
Per verificare la configurazione eseguire:
|
0 |
haproxy -c -f /etc/haproxy/haproxy.cfg
|
Per installare rsync, che necessario per la replica della configurazione, eseguire su entrambi i nodi:
|
0 |
apt-get install -y rsync
|
Per replicare la configurazione eseguire (sostituire DESTINATION con l’hostname o l’IP del server su cui si vuole copiare la configurazione):
|
0 |
rsync -avz -e ssh /etc/haproxy/ DESTINATION:/etc/haproxy/
|
Per applicare localmente la configurazione eseguire:
|
0 |
systemctl reload haproxy.service
|
Se vengono modificate le variabili d’ambiente è necessario riavviare HAProxy eseguendo:
|
0 |
systemctl restart haproxy.service
|
CONFIGURAZIONE DELLE ACL
Mediante le ACL salvate nei files specificati con le variabili d’ambiente ACL_CN_PATH, ACL_EMAIL_PATH e ACL_OU_PATH gli utenti vengono autorizzati ad accedere ai servers.
Tutti i files seguono la seguente sintassi:
ATTRIBUTO: RISORSA
Esempio per CN: VALLE Marco:mylocalserver.pizza.local
Esempio per EMAIL: [email protected]:mylocalserver.pizza.local
Esempio per OU: 01-LAB:mylocalserver.pizza.local e 01-LAB:192.168.1.88
Per applicare le ACL è necessario ricaricare HAProxy:
|
0 |
systemctl reload haproxy.service
|
Dopo averle applicate sul nodo secondario, replicare la configurazione sul primario ed applicarla.
ATTENZIONE:
Per come funziona la logica di validazione delle ACL definita negli scripts, due OU presenti in posti diversi dell’alberatura di Active Directory, se omonime, vengono valutate nello stesso modo.
Per questo motivo un utente con accesso in scrittura su almeno una posizione dell’alberatura, potrebbe creare una OU chiamata come una uilizzata per abilitare gli utenti al bastione, abusando dei suoi permessi.
Potenzialmente si potrebbero modificare gli script per concatenare le OU e garantire una validazione più stringente delle ACL.
CONFIGURAZIONE OPZIONALE DELLA LANDING PAGE
Nonostante HAProxy non sia un web server, è possibile distribuire delle pagine HTML statiche sui frontends di tipo HTTP.
Questa configurazione può risultare utile per creare una landing page in cui viene spiegato come utillizare il bastione.
Aggiungere il seguente frontend:
|
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
#---------------------------------------------------------------------
# fe_http frontend
#---------------------------------------------------------------------
frontend fe_http
bind 192.168.1.100:80
bind 192.168.1.100:443 ssl crt /etc/ssl/private verify required ca-file /etc/ssl/MyIntermediateCA.crt ca-verify-file /etc/ssl/MyRootCA.crt
mode http
option httpslog
#### Redirect to HTTPS
http-request redirect scheme https unless { ssl_fc }
#### Set HTTP request headers
http-request set-header X-Forwarded-Proto https if { ssl_fc }
http-request set-header X-Forwarded-Proto http if !{ ssl_fc }
http-request set-header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload;"
acl req_favicon path_beg /favicon.ico
acl req_ssh hdr(host) -i ssh.pizza.com
http-request return content-type "image/x-icon" file /etc/haproxy/static/favicon.ico if req_favicon
http-request return content-type "text/html" file /etc/haproxy/static/landing_page.html if req_ssh
|
Sostituire ssh.pizza.com con l’FQDN che si desidera utilizzare.
Se non si vuole rendere obbligatoria l’autenticazione per questa pagina mediante mTLS omettere la direttiva verify required.
Creare la directory con i files statici:
|
0 |
mkdir /etc/haproxy/static
|
Aggiungere il file /etc/haproxy/static/favicon.ico con la favicon ed il file /etc/haproxy/static/landing_page.html con la pagina HTML.
Replicare la configurazione ed i files statici su entrambi i servers e ricaricare la configurazione:
|
0 |
systemctl reload haproxy.service
|

0 commenti