Dez. 25, 2025 Linux

ACME.sh LAN Cert Dispencer

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

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert