Betreibt man im Heimnetzwerk Webdienste, die nur übers heimische LAN erreichbar sein sollen, kommt man schnell mit der Frage auf: SSL-Zertifikate! Im abgetrennten Netzwerk ist das leider nicht so Straid-forward wie auf online Servern.
Um nun doch im lokalen Netz dies zu machen, nutzen wir eine bestimmte Challenge: DNS-01. Voraussetzung dafür ist eine registrierte Domain und Zugang zu den DNS-Einstellungen. In diesem Beispiel nutze ich Hetzner Cloud DNS.
Als Basissystem für das Verwalten und erzeugen der Zertifikate nutze ich einen LXC-Container auf meinem Proxmox Server im LAN.
Basisidee
Auf dem LXC-Container wird ein Wildcard-Zertifikat generiert, das *.lan.example.com Zertifikat liegt dann in einem bestimmten Ordner. Für das Erzeugen nutze ich acme.sh ein Shell-Programm, das Zertifikate erstellt und automatisch erneuert.
Diese Zertifikate liegen dann in einem bestimmten Ordner. Dieser Ordner wird dann über ssh freigegeben. Die Clients (also die Webdienste) ziehen sich dann über scp die Certs und laden sie in den richtigen Ordner.
Pakete installieren
Nun beginnen wir mit unserem ACME-Server
apt update
apt install curl git openssh-server socat
SSH aktivieren:
systemctl enable --now ssh
acme.sh installieren
Installieren wir acme.sh im Root-Verzeichnis.
cd ~
git clone --depth 1 https://github.com/acmesh-official/acme.sh.git
cd acme.sh
./acme.sh --install -m mail@example.com
Testen wir acme.sh
./acme.sh
Zertifikate in festen Pfad deployen
Setup Hetzner DNS Challenge
Wie gesagt nutze ich für die DNS-01 Challenge die Hetzner Cloud. Je nach DNS-Provider (er muss es unterstützen) sind andere Schritte nötig. Mehr Infos findest du hier https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#188-use-hetzner-cloud-dns-api
Erstellen wir mal den Zielordner für die Certs
mkdir -p /opt/certs
Jetzt konfigurieren wir das Deployment
export HETZNER_TOKEN="token"
# optional:
# export HETZNER_TTL=120
# export HETZNER_API="https://api.hetzner.cloud/v1"
# export HETZNER_MAX_ATTEMPTS=120
# ECDSA Key
./acme.sh --issue --server letsencrypt --key-file /opt/certs/star-lan.example.com-privkey.pem --fullchain-file /opt/certs/star-lan.example.com-fullchain.pem --dns dns_hetznercloud -d *.lan.example.com --reloadcmd "chown -R certuser:certuser /opt/certs/*"
# RSA Key
./acme.sh --issue --server letsencrypt --keylength 2048 --dns dns_hetznercloud -d *.lan.example.com --key-file /opt/certs/star-lan.example.com-privkey-rsa.pem --fullchain-file /opt/certs/star-lan.example.com-fullchain-rsa.pem --reloadcmd "chown -R certuser:certuser /opt/certs/*"
Mit folgenden Command sehen wir die Certs
./acme.sh list
SSH Benutzer für Zertifikate anlegen
Damit die Clients sich das Zertifikat herunterladen können, erstellen wir einen eigenen User, den wir certuser nennen.
Ein eigener Benutzer ohne Shell, ohne Write-Permission:
adduser --system --home /opt/certs --shell /sbin/nologin certuser
Setzen wir noch die richtigen Ordnerrechte
groupadd certuser
chown -R certuser:certuser /opt/certs
chmod 500 /opt/certs
chmod 400 /opt/certs/*
Der Benutzer darf nur lesen. Wir möchten ja nicht, dass die Clients das Zertifikat verändern oder löschen können.
SSH Key-only Zugriff
Nun bereiten wir noch das authorized_keys für den User vor
mkdir ~certuser/.ssh
chmod 700 ~certuser/.ssh
chown -R certuser:certuser ~certuser/.ssh
Force Command (read-only)
Damit niemand „herumsurfen“ kann:
In /opt/certs/.ssh/authorized_keys:
command="cd /opt/certs; exec /usr/lib/openssh/sftp-server -R" ssh-rsa AAAA...
Das zwingt jede SSH-Verbindung nur zum SFTP im Ordner /opt/certs, ohne Shell.
- Kein Shell-Zugang
- Kein Dateiupload
- Nur Read
SSH Daemon absichern (optional, empfohlen)
In /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin no
AllowUsers certuser
Subsystem sftp internal-sftp
Match User certuser
ForceCommand internal-sftp
Dann:
systemctl restart sshd
Jetzt haben wir so weit alles am Server vorgenommen, was wichtig ist. Gehen wir nun weiter mit dem Client, der vom ACME-Server das Zertifikat herunterlädt.
Client Setup
Der Client meldet sich beim Server mittels Schlüssel an. Diesen erstellen wir mal.
Key generieren:
ssh-keygen -t ed25519 -f ~/.ssh/acme-cert
Public Key dem Server geben → in authorized_keys eintragen. Wichtig ist hier, dass wir die vorhin festgelegten Limitierungen kopieren.
nano /opt/certs/.ssh/authorized_keys
So sollte das File dann Zirka aussehen:
command="cd /opt/certs; exec /usr/lib/openssh/sftp-server -R" ssh-rsa AAAA...
Auf Empfänger: Cert Folder erstellen
mkdir -p /opt/certs
Zertifikat herunterladen
Test für download mit SCP:
scp -i ~/.ssh/acme-cert certuser@192.168.0.105:/opt/certs/fullchain.pem .
scp -i ~/.ssh/acme-cert certuser@192.168.0.105:/opt/certs/privkey.pem .
Automatisch im Bash Script: /opt/_scripts/renew-ssl.sh
#!/usr/bin/env bash
set -euo pipefail
##### CONFIG #####
REMOTE_HOST="192.168.0.105"
REMOTE_USER="certuser"
SSH_KEY="~/.ssh/acme-cert"
REMOTE_PATH="/opt/certs"
FILES=("star-lan.example.com-fullchain.pem" "star-lan.example.com-privkey.pem")
LOCAL_TARGET="/opt/certs"
TEST_COMMAND="nginx -t"
RESTART_COMMAND="systemctl restart nginx"
##################
mkdir -p "$LOCAL_TARGET"
# SCP download
for file in "${FILES[@]}"; do
scp -i "$SSH_KEY" \
"$REMOTE_USER@$REMOTE_HOST:$REMOTE_PATH/$file" \
"$LOCAL_TARGET/$file" > /dev/null
done
# Test
set +e
$TEST_COMMAND 1> /dev/null 2> /dev/null
TEST_EXIT=$?
set -e
if [ $TEST_EXIT -eq 0 ]; then
$RESTART_COMMAND > /dev/null
else
echo "!! Test with error. EXIT Code: $TEST_EXIT"
exit 1
fi
Cronjob
# SSL Cert renew
15 6 * * 3 root /opt/_scripts/renew-ssl.sh
Fertig! So lädt das Script immer in regelmäßigen Abständen das Zertifikat vom Server