Skip to content

Instantly share code, notes, and snippets.

@LucaFilipozzi
Last active March 21, 2024 00:30
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save LucaFilipozzi/5ad0da9e4ffae870ec281ddc46cf429d to your computer and use it in GitHub Desktop.
Save LucaFilipozzi/5ad0da9e4ffae870ec281ddc46cf429d to your computer and use it in GitHub Desktop.
ssh-tls-tunnel and ssh-port-knock

ssh-tls-tunnel

stuck behind a firewall that allows only http/https connections? offer ssh over a TLS tunnel!

configure haproxy server to accept TLS connections with ALPN ssh/2.0

configure ssh client to create TLS connections with ALPN ssh/2.0 using ProxyCommand

ssh-port-knock

tired of constant attacks against sshd? require port knocking!

configure nftables ruleset to permit connections to sshd based on port knocks

configure ssh client to issue port knocks using ProxyCommand

observations

While sslh allows multiplexing of ssh and https connections on the same port, it does not wrap ssh and this allows a firewall to block connections. The use of ALPN allows haproxy to route connections without resorting to payload inspection.

fwknopd offers secure port-knocking and knockd offers simple port-knocking. Both are user-land daemons that require root privileges to observe packets and to update firewall rules, so nftables (kernel-land) is used instead. Additionally, the objective is to reduce the attack surface of sshd, not to add another authentication layer such as what fwknopd would provide.

The haproxy TLS settings and the apache2 headers achieve A+ ratings with Qualys and Mozilla Observatory.

debian packages

This solution requires the following packages:

  • apache2
  • dehydrated
  • dehydrated-apache2
  • haproxy
  • openssh-server
  • nftables
# /etc/haproxy/crt-list.txt
/var/lib/dehydrated/certs/foo.example.com/concat.pem [alpn ssh/2.0,h2,http/1.1]
/var/lib/dehydrated/certs/example.com/concat.pem [alpn h2,http/1.1]
/var/lib/dehydrated/certs/www.example.com/concat.pem [alpn h2,http/1.1]
#!/bin/sh
# /etc/cron.daily/dehydrated
/usr/bin/dehydrated --cron --keep-going
/usr/bin/dehydrated --cleanup --keep-going
# /etc/dehydrated/domains.txt
foo.example.com
example.com
www.example.com
# /etc/haproxy/haproxy.cfg
global
# stuff
defaults
# stuff
ssl-default-bind-ciphers !DHE:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-bind-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
ssl-default-server-ciphers !DHE:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256
ssl-default-server-options no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
listen common:80
bind ipv4@:80,ipv6@:80
redirect scheme https code 301 unless { path_beg /.well-known/acme-challenge/ }
server httpd 127.0.0.1:8080 send-proxy
frontend common:443
bind ipv4@:443,ipv6@:443 strict-sni ssl crt-list /etc/haproxy/crt-list.txt
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req.ssl_hello_type 1 }
use_backend sshd if { ssl_fc_alpn -i ssh/2.0 }
default_backend httpd
backend sshd
mode tcp
server sshd 127.0.0.1:22
backend httpd
mode tcp # required for h2
server httpd 127.0.0.1:8080 send-proxy
#!/bin/bash
# /etc/dehydrated/hook.sh
deploy_challenge() {
:
}
clean_challenge() {
:
}
deploy_cert() {
local DOMAIN="${1}" KEY="${2}" CRT="${3}" CHN="${5}"
cat ${CRT} ${CHN} ${KEY} > $(dirname ${CRT})/concat.pem
systemctl reload-or-restart haproxy
}
deploy_ocsp() {
local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}"
cat ${OCSPFILE} > $(dirname ${OCSPFILE})/concat.pem.ocsp
echo "set ssl ocsp-response $(base64 -w 0 ${OCSPFILE})" | socat stdio unix-connect:/run/haproxy/admin.sock
}
unchanged_cert() {
:
}
invalid_challenge() {
:
}
request_failure() {
:
}
generate_csr() {
:
}
startup_hook() {
:
}
exit_hook() {
:
}
HANDLER="$1"; shift
if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then
"$HANDLER" "$@"
fi
# /etc/dehydrated/conf.d/local.sh
HOOK=/etc/dehydrated/hook.sh
CONTACT_EMAIL="webmaster@example.com"
OCSP_FETCH="yes"
RENEW_DAYS=35
# /etc/nftables.conf
flush ruleset
table inet filter {
set tmp4 {
type ipv4_addr
timeout 5s
size 100
}
set ssh4 {
type ipv4_addr
flags timeout
size 100
elements = {
209.87.16.68 comment "ssh.debian.org",
}
}
set tmp6 {
type ipv6_addr
timeout 5s
size 100
}
set ssh6 {
type ipv6_addr
flags timeout
size 100
elements = {
2607:f8f0:614:1::1274:68 comment "ssh.debian.org",
}
}
chain prerouting {
type filter hook prerouting priority -300
policy accept
udp dport { 7001 } set update ip saddr @tmp4 drop
udp dport { 7002 } ip saddr @tmp4 set update ip saddr timeout 5s @ssh4 drop
udp dport { 7001 } set update ip6 saddr @tmp6 drop
udp dport { 7002 } ip6 saddr @tmp6 set update ip6 saddr timeout 5s @ssh6 drop
}
chain input {
type filter hook input priority 0
policy drop
iif lo accept
ct state {established, related} accept
ct state invalid drop
icmp type { echo-request, echo-reply, time-exceeded, parameter-problem, destination-unreachable } accept
icmpv6 type { echo-request, echo-reply, time-exceeded, parameter-problem, destination-unreachable, packet-too-big, nd-router-advert, nd-router-solicit, nd-neighbor-solicit, nd-neighbor-advert, mld-listener-query } accept
tcp dport { ssh } ip saddr @ssh4 accept
tcp dport { ssh } ip6 saddr @ssh6 accept
tcp dport { http, https } accept
tcp dport { smtp } accept
}
chain forward {
type filter hook forward priority 0
policy drop
}
chain output {
type filter hook output priority 0
policy accept
}
}
# ~/.ssh/config
Host foo-tls-tunnel.example.com
HostKeyAlias foo.example.com
HostName foo.example.com
Port 443
ProxyCommand /opt/local/bin/gnutls-cli --port %p --sni-hostname %h --alpn ssh/2.0 %h
Host foo-port-knock.example.com
HostKeyAlias foo.example.com
HostName foo.example.com
ProxyCommand /bin/bash -c "/opt/local/bin/knock -d 100 -v %h 7001:udp 7002:udp ; /usr/bin/nc %h %p"
# /etc/apache2/sites-available/websites.conf
# vim: set syntax=apache:
# disable listening on port :80 and :443
# enable modules: headers http2 remoteip
# disable modules: ssl
LogFormat "%v:%p %a %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" proxy_vhost_combined
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log proxy_vhost_combined
RemoteIPProxyProtocol On
Listen 127.0.0.1:8080
<VirtualHost 127.0.0.1:8080>
ServerName foo.example.com
Header always set Content-Security-Policy "base-uri 'none'; default-src 'none'; font-src 'self' data: https://fonts.gstatic.com; img-src 'self'; form-action 'none'; manifest-src 'self'; object-src 'none'; script-src-elem 'self' https://cdnjs.cloudflare.com; style-src 'self' https://cdnjs.cloudflare.com https://fonts.googleapis.com; frame-ancestors 'none'"
Header always set Feature-Policy "geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; fullscreen 'none'; payment 'none'"
Header always set Referrer-Policy "same-origin"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "deny"
DocumentRoot /srv/foo.example.com/
# stuff
</VirtualHost>
<VirtualHost 127.0.0.1:8080>
ServerName www.example.com
RedirectPermanent / https://example.com/
</VirtualHost>
<VirtualHost 127.0.0.1:8080>
ServerName example.com
Header always set Content-Security-Policy "base-uri 'none'; default-src 'none'; font-src 'self' data: https://fonts.gstatic.com; img-src 'self'; form-action 'none'; manifest-src 'self'; object-src 'none'; script-src-elem 'self' https://cdnjs.cloudflare.com; style-src 'self' https://cdnjs.cloudflare.com https://fonts.googleapis.com; frame-ancestors 'none'"
Header always set Feature-Policy "geolocation 'none'; midi 'none'; notifications 'none'; push 'none'; sync-xhr 'none'; microphone 'none'; camera 'none'; magnetometer 'none'; gyroscope 'none'; speaker 'none'; vibrate 'none'; fullscreen 'none'; payment 'none'"
Header always set Referrer-Policy "same-origin"
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "deny"
DocumentRoot /srv/example.com/
# stuff
</VirtualHost>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment