802.1x eap-tls Guide
Über dieses Dokument¶
Diese Anleitung baut eine vollständige 802.1X-Infrastruktur von Grund auf neu auf:
- EAP-TLS als primäre Authentifizierungsmethode (zertifikatsbasiert, kein Passwort-Handling)
- MAB (MAC Authentication Bypass) als Fallback für Geräte ohne 802.1X-Supplicant
- Gast-VLAN für alles, was weder per Zertifikat noch per MAC authentifiziert werden kann
- Kritisches Fallback-VLAN, falls der RADIUS-Server selbst nicht erreichbar ist
- Eine eigene, kleine PKI (step-ca) als Zertifizierungsstelle
- Automatisiertes Client-Enrollment für Windows- und macOS-Geräte, inkl. automatischer Zertifikatserneuerung
Die Anleitung geht davon aus, dass nichts vorhanden ist – kein Active Directory, keine bestehende PKI, keine bestehende RADIUS-Installation. Alle Schritte sind in der Reihenfolge aufgeführt, in der sie ausgeführt werden müssen, und wurden in dieser Form real getestet (inklusive Zertifikatsprüfung, VLAN-Zuweisung und Reauthentifizierung an einem produktiven Cisco-Switch).
Warum keine Active-Directory-Domäne vorausgesetzt wird¶
Diese Anleitung ist bewusst für Workgroup-Umgebungen (keine Windows-Domäne, kein Microsoft Intune/MDM) geschrieben. Alle Mechanismen (SCEP-Enrollment auf macOS, Token-basiertes Enrollment auf Windows) funktionieren ohne Domänenmitgliedschaft. Wer eine AD-Domäne betreibt, kann einzelne Schritte (z. B. Zertifikatsverteilung) durch GPO-basierte Mechanismen ersetzen – das ist in dieser Anleitung nicht abgedeckt.
Platzhalter in diesem Dokument¶
Ersetzen Sie durchgängig folgende Platzhalter durch Ihre eigenen Werte, bevor Sie die Befehle ausführen:
| Platzhalter | Bedeutung | Beispiel |
|---|---|---|
domain.tld |
Ihre interne Domain/DNS-Zone | firma.ch |
ca.domain.tld |
Hostname der Zertifizierungsstelle | ca.firma.ch |
freeradius.domain.tld |
Hostname des FreeRADIUS-Servers | radius.firma.ch |
<CA_VM_IP> |
IP-Adresse der VM, auf der step-ca läuft | 10.13.0.10 |
<RADIUS_VM_IP> |
IP-Adresse der VM, auf der FreeRADIUS/daloRADIUS läuft | 10.13.0.11 |
<SWITCH_MGMT_IP> |
Management-IP des Cisco-Switches | 10.13.0.2 |
<ORG-8021X-CA> |
Anzeigename Ihrer internen CA | firma-8021x-CA |
Falls step-ca und FreeRADIUS auf derselben VM laufen sollen, sind <CA_VM_IP> und <RADIUS_VM_IP> identisch – das ändert nichts an den folgenden Schritten.
Voraussetzungen¶
- Eine Linux-VM (Debian oder Ubuntu, getestet mit Debian Bookworm/Trixie-Basis), mindestens 2 GB RAM, 20 GB Speicher
- Docker Engine und Docker Compose Plugin installiert (
docker compose versionmuss funktionieren) - Root- bzw. sudo-Zugriff auf die VM
- Ein Cisco Switch mit IOS, das IBNS 2.0 unterstützt (getestet mit einem Catalyst 2960-X, IOS 15.2(4)E6 – ältere IOS-Versionen benötigen teils andere Befehle, siehe Hinweis in Abschnitt Cisco Switch)
- Netzwerkzugriff zwischen VM und Switch (Management-Interface)
- Ein Passwort-Safe (z. B. Vaultwarden, KeePass, 1Password) – mehrere der folgenden Schritte erzeugen Geheimnisse, die zwingend dort abgelegt werden müssen. Jede Stelle, an der das nötig ist, ist im Text mit 🔐 Passwort-Safe markiert.
Architektur-Überblick¶
flowchart TB
subgraph Client["Client-Gerät"]
Cert["Client-Zertifikat<br/>(EAP-TLS)"]
end
subgraph Switch["Cisco Switch"]
Port["Access-Port<br/>dot1x + MAB"]
end
subgraph RadiusVM["VM: FreeRADIUS + daloRADIUS"]
FR["FreeRADIUS<br/>(EAP-TLS Terminierung)"]
DB[("MariaDB<br/>radcheck/radgroupreply")]
Dalo["daloRADIUS<br/>(Web-GUI)"]
end
subgraph CaVM["VM: step-ca"]
CA["step-ca<br/>Root + Intermediate CA"]
SCEP["SCEP-Endpoint<br/>(macOS-Enrollment)"]
end
Client -- EAPOL --> Port
Port -- RADIUS Access-Request --> FR
FR -- TLS-Handshake, Zertifikatsprüfung --> Cert
FR -- CA-Kette prüfen --> CA
FR <--> DB
Dalo <--> DB
Client -. SCEP-Enrollment .-> SCEP
Port -- Access-Accept + VLAN --> Client
Der Datenfluss in Kurzform:
- Ein Client verbindet sich am Switchport. Der Switch versucht zuerst 802.1X, dann MAB, und weist bei beidem Fehlschlag ein Gast-VLAN zu.
- FreeRADIUS terminiert den EAP-TLS-Handshake und prüft das Client-Zertifikat gegen die eigene CA-Kette.
- Der Benutzername (= Zertifikats-CN) wird gegen MariaDB (
radusergroup/radgroupreply) abgeglichen, um das passende VLAN zu ermitteln. - Der Switch erhält im Access-Accept die VLAN-Zuweisung und schaltet den Port entsprechend.
Teil 1: Zertifizierungsstelle (step-ca)¶
1.1 Warum step-ca und nicht OpenSSL/easy-rsa¶
step-ca unterstützt ACME und SCEP nativ, läuft als einfacher Docker-Container und ermöglicht automatisiertes Zertifikats-Enrollment inklusive automatischer Erneuerung – das ist mit reinem OpenSSL nur mit erheblichem Zusatzaufwand nachzubauen.
1.2 Ein wichtiger Design-Punkt vorab: Root 20 Jahre, Intermediate 10 Jahre, RSA statt ECDSA¶
Der Root ist der Vertrauensanker, der auf jedem Client-Gerät importiert wird – ihn neu zu verteilen ist aufwändig, daher eine lange Laufzeit. Die Intermediate signiert aktiv laufend Zertifikate und lässt sich bei Bedarf rotieren, ohne dass Clients ihr Root-Vertrauen neu einrichten müssen – daher kürzer.
Kritisch für SCEP (macOS-Enrollment): Die Intermediate-CA muss RSA verwenden, nicht ECDSA. SCEP verschlüsselt die Zertifikatsanfrage (PKCS#7-Envelope) mit dem öffentlichen Schlüssel der Intermediate – diese Operation kann mit einem ECDSA-Schlüssel nicht durchgeführt werden. Der Root selbst darf ECDSA bleiben, das hat auf SCEP keinen Einfluss.
1.3 Verzeichnisstruktur¶
1.4 CA-Administrationspasswort generieren¶
🔐 Passwort-Safe: Dieses Passwort verschlüsselt den privaten Schlüssel der Intermediate-CA. Ohne dieses Passwort ist nach einem Neustart/Wiederherstellen kein Zugriff mehr auf die Intermediate möglich – es muss unbedingt sicher hinterlegt werden.
1.5 Root-CA mit 20 Jahren Laufzeit erzeugen¶
docker run --rm \
-v /docker/stepca/data:/home/step \
smallstep/step-ca step certificate create \
"<ORG-8021X-CA> Root CA" \
/home/step/root_ca.crt /home/step/root_ca_key \
--profile root-ca \
--not-after 175200h \
--no-password --insecure
chown -R 1000:1000 /docker/stepca/data
🔐 Passwort-Safe: Der private Schlüssel
data/root_ca_keyist der wichtigste Schlüssel der gesamten Infrastruktur – wer ihn besitzt, kann beliebige neue Intermediates ausstellen und damit jedes Client-Zertifikat fälschen. Nach dem Init (Schritt 1.7) sollte diese Datei von der VM entfernt und offline gesichert werden (siehe Abschnitt 1.9).
1.6 Passwortdatei für den automatischen Init vorbereiten¶
mkdir -p data/secrets
cp ca_password.txt data/secrets/password
chown -R 1000:1000 data
chmod 400 data/secrets/password
1.7 docker-compose.yaml¶
services:
step-ca:
image: smallstep/step-ca:latest
container_name: step-ca
restart: unless-stopped
ports:
- "9000:9000"
- "8080:8080"
volumes:
- ./data:/home/step
extra_hosts:
- "ca.domain.tld:127.0.0.1"
environment:
- DOCKER_STEPCA_INIT_NAME=<ORG-8021X-CA>
- DOCKER_STEPCA_INIT_DNS_NAMES=ca.domain.tld
- DOCKER_STEPCA_INIT_PROVISIONER_NAME=admin
- DOCKER_STEPCA_INIT_PASSWORD_FILE=/home/step/secrets/password
- DOCKER_STEPCA_INIT_ROOT_FILE=/home/step/root_ca.crt
- DOCKER_STEPCA_INIT_KEY_FILE=/home/step/root_ca_key
Erklärung der zwei Ports:
- 9000/tcp: regulärer HTTPS-API-Port der CA (Zertifikatsausstellung, Verwaltung)
- 8080/tcp: ein unverschlüsselter HTTP-Listener, ausschliesslich für SCEP. Grund: Bei manueller (Nicht-MDM) Profilinstallation auf macOS ist der frisch importierte Root zum Zeitpunkt der SCEP-Anfrage noch nicht als vertrauenswürdig für TLS markiert – ein SCEP-Request über HTTPS schlägt daher fehl (
Zum Server kann keine sichere Verbindung aufgebaut werden). SCEP selbst ist bereits durch das Challenge-Secret und PKCS#7-Signierung/Verschlüsselung abgesichert, daher ist Klartext-HTTP für diesen einen Zweck ein akzeptierter, von SCEP-Implementierungen üblicher Kompromiss.
Das extra_hosts-Mapping ist nötig, damit der Container seinen eigenen konfigurierten Hostnamen (ca.domain.tld) auch dann auflösen kann, wenn dafür (noch) kein echter DNS-Eintrag existiert – für interne CLI-Aufrufe innerhalb des Containers.
1.8 CA initialisieren¶
Beim ersten Start erzeugt der Entrypoint automatisch die Intermediate-CA (signiert vom vorbereiteten Root) und startet danach den Daemon. Sie sollten in den Logs sehen:
Copying root certificate... done!
Generating intermediate certificate... done!
...
Serving HTTPS on :9000 ...
Danach in den Hintergrund wechseln:
1.9 Root-Key offline auslagern (empfohlen)¶
Der laufende Container benötigt den Root-Key nach der Initialisierung nicht mehr – nur der Intermediate-Key signiert laufend Zertifikate.
cp /docker/stepca/data/root_ca_key /pfad/zu/einem/sicheren/offline-backup/
shred -u /docker/stepca/data/root_ca_key
🔐 Passwort-Safe: Der Speicherort dieses Offline-Backups (z. B. verschlüsselter USB-Stick im Tresor) muss dokumentiert sein. Ohne diesen Key kann im Schadensfall keine neue Intermediate mehr signiert werden – die bestehende Intermediate bleibt davon aber 10 Jahre lang unberührt funktionsfähig.
Die Datei data/certs/root_ca.crt (das öffentliche Zertifikat, kein Geheimnis) muss auf der VM bleiben.
1.10 Dedizierte RSA-Intermediate-CA für SCEP¶
Falls Ihre Intermediate (Schritt 1.8) automatisch als ECDSA erzeugt wurde (Standardverhalten von step-ca), muss sie durch eine RSA-Variante ersetzt werden – sonst funktioniert SCEP für macOS-Enrollment nicht (siehe Erklärung in 1.2).
Root-Key temporär zurückkopieren (falls bereits ausgelagert):
RSA-Intermediate erzeugen (10 Jahre, signiert vom bestehenden Root):
docker run --rm \
-v /docker/stepca/data:/home/step \
smallstep/step-ca step certificate create \
"<ORG-8021X-CA> Intermediate CA" \
/home/step/certs/intermediate_ca_rsa.crt \
/home/step/secrets/intermediate_ca_rsa_key \
--profile intermediate-ca \
--ca /home/step/certs/root_ca.crt \
--ca-key /home/step/root_ca_key \
--kty RSA --size 3072 \
--not-after 87600h \
--password-file /home/step/secrets/password
ca.json auf die neue Intermediate umbiegen:
sed -i \
-e 's#/home/step/certs/intermediate_ca.crt#/home/step/certs/intermediate_ca_rsa.crt#' \
-e 's#/home/step/secrets/intermediate_ca_key#/home/step/secrets/intermediate_ca_rsa_key#' \
/docker/stepca/data/config/ca.json
Root-Key wieder entfernen, falls temporär zurückgeholt:
Kontrolle:
Erwartete Ausgabe muss RSA als Schlüsseltyp zeigen.
1.11 Zertifikatslaufzeiten für den Admin-Provisioner anpassen¶
Standardmässig lässt der admin-Provisioner nur 24-Stunden-Zertifikate zu – zu kurz für Server-Zertifikate und Windows-Enrollment-Tokens.
docker exec -it step-ca step ca provisioner update admin \
--x509-min-dur 2160h \
--x509-default-dur 8760h \
--x509-max-dur 8760h
docker compose restart step-ca
1.12 SCEP-Provisioner für macOS-Enrollment einrichten¶
1.12.1 Dediziertes SCEP-Entschlüsselungszertifikat erzeugen¶
Wichtiger Hintergrund: macOS prüft seit Ventura zwingend, dass das Zertifikat, das SCEP-Nachrichten signiert/entschlüsselt, eine passende Key-Usage-Erweiterung hat (digitalSignature/keyEncipherment). Ein reines CA-Zertifikat (wie unsere Intermediate, mit Certificate Sign, CRL Sign) erfüllt das nicht – ohne dieses dedizierte Zertifikat schlägt jedes macOS-Enrollment mit MDM-SCEP:15002 / „Die Schlüsselverwendung ist falsch" fehl.
docker exec -it step-ca step certificate create scep-decrypter \
/home/step/scep-decrypter.crt \
/home/step/scep-decrypter.key \
--ca /home/step/certs/intermediate_ca_rsa.crt \
--ca-key /home/step/secrets/intermediate_ca_rsa_key \
--kty RSA --size 2048 \
--not-after 8760h \
--no-password --insecure
1.12.2 SCEP-Challenge-Secret generieren¶
🔐 Passwort-Safe: Dieses Secret authentifiziert jedes enrollende Gerät gegenüber SCEP. Es wird in jedem macOS-Enrollment-Profil eingebettet – wer es kennt, kann (im Rahmen der konfigurierten Limits) beliebige Zertifikate von dieser CA anfordern.
1.12.3 SCEP-Provisioner anlegen¶
docker exec -it step-ca step ca provisioner add scep-8021x \
--type SCEP \
--challenge "<SCEP_CHALLENGE_SECRET>" \
--encryption-algorithm-identifier 2 \
--include-root \
--scep-decrypter-certificate-file /home/step/scep-decrypter.crt \
--scep-decrypter-key-file /home/step/scep-decrypter.key \
--x509-min-dur 2160h \
--x509-default-dur 8760h \
--x509-max-dur 8760h
docker compose restart step-ca
--include-root sorgt dafür, dass macOS-Clients bei der SCEP-GetCACert-Anfrage sowohl Root als auch Intermediate erhalten (von Apple explizit für den macOS-SCEP-Client vorgesehene Option).
1.13 Root-CA-Zertifikat exportieren¶
Wird später für FreeRADIUS und für Client-Enrollment gebraucht:
docker exec step-ca cat /home/step/certs/root_ca.crt > /docker/stepca/root_ca.pem
docker exec step-ca cat /home/step/certs/intermediate_ca_rsa.crt > /docker/stepca/intermediate_ca.pem
cat /docker/stepca/root_ca.pem /docker/stepca/intermediate_ca.pem > /docker/stepca/ca-chain.pem
1.14 Server-Zertifikat für FreeRADIUS ausstellen¶
docker exec -it step-ca step ca certificate \
"freeradius.domain.tld" \
/home/step/freeradius-server.crt \
/home/step/freeradius-server-key \
--provisioner admin \
--ca-url https://ca.domain.tld:9000 \
--root /home/step/certs/root_ca.crt \
--not-after 8760h
(Fragt nach dem CA-Administrationspasswort aus Schritt 1.4.)
docker cp step-ca:/home/step/freeradius-server.crt /docker/stepca/freeradius-server.pem
docker cp step-ca:/home/step/freeradius-server-key /docker/stepca/freeradius-server.key
docker exec step-ca rm /home/step/freeradius-server.crt /home/step/freeradius-server-key
Diese drei Dateien (ca-chain.pem, freeradius-server.pem, freeradius-server.key) werden in Teil 2 auf die RADIUS-VM übertragen.
Teil 2: FreeRADIUS, MariaDB und daloRADIUS¶
2.1 Warum FreeRADIUS aus dem Debian-Paket statt aus dem offiziellen Docker-Image¶
Das offizielle freeradius/freeradius-server-Image ist ausschliesslich als linux/amd64 verfügbar (kein Multi-Arch) und würde auf ARM-Plattformen (z. B. Raspberry Pi) nur emuliert laufen. Wir bauen daher ein eigenes, schlankes Image aus dem Debian-Paket – das funktioniert architekturunabhängig und ist zusätzlich transparenter (Standard-Debian-Konfigurationspfade).
2.2 Verzeichnisstruktur¶
2.3 Zertifikate von der CA-VM übertragen¶
Kopieren Sie die drei Dateien aus Teil 1.13/1.14 hierher:
cp /pfad/zur/ca-chain.pem /docker/freeradius/certs/ca.pem
cp /pfad/zur/freeradius-server.pem /docker/freeradius/certs/server.pem
cp /pfad/zur/freeradius-server.key /docker/freeradius/certs/server.key
chmod 600 /docker/freeradius/certs/server.key
Wichtig:
ca.pemmuss beide Zertifikate enthalten (Root und Intermediate) – nicht nur den Root. FreeRADIUS muss die komplette Kette kennen, um Client-Zertifikate zu verifizieren, die von der Intermediate signiert wurden. Fehlt die Intermediate hier, schlägt jede Client-Authentifizierung mitunable to get local issuer certificatefehl.
2.4 MariaDB-Zugangsdaten generieren¶
🔐 Passwort-Safe: Beide Passwörter (root und
radius-User) gehören in den Passwort-Safe. Dasradius-User-Passwort wird ausserdem an drei Stellen referenziert (MariaDB-Env, FreeRADIUS SQL-Modul, daloRADIUS-Config) – bei einer Änderung müssen alle drei synchron aktualisiert werden.
openssl rand -base64 24 # -> MYSQL_ROOT_PASSWORD
openssl rand -base64 24 # -> MYSQL_PASSWORD (radius-User)
2.5 FreeRADIUS Dockerfile¶
/docker/freeradius/freeradius/Dockerfile:
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y \
freeradius \
freeradius-utils \
freeradius-mysql \
&& rm -rf /var/lib/apt/lists/*
EXPOSE 1812/udp 1813/udp 1814/udp
CMD ["freeradius", "-f", "-l", "stdout"]
2.6 daloRADIUS Dockerfile¶
Hintergrund: daloRADIUS nutzt intern die klassische PEAR::DB-Bibliothek. Aktuelle Debian-Versionen (Trixie) führen die dafür nötigen php-pear/php-db-Pakete nicht mehr. Wir ziehen die Bibliothek daher über Composer von Packagist und kopieren die Dateien an den PHP-include_path.
/docker/freeradius/daloradius/Dockerfile:
FROM composer:2 AS composer
FROM php:8.2-apache
RUN apt-get update && apt-get install -y \
git \
default-mysql-client \
libxml2-dev \
unzip \
&& docker-php-ext-install mysqli pdo pdo_mysql \
&& rm -rf /var/lib/apt/lists/*
# Deprecation-Warnings aus PHP 8.2 in der Produktions-GUI unterdrücken
RUN { \
echo 'display_errors = Off'; \
echo 'log_errors = On'; \
echo 'error_reporting = E_ALL & ~E_DEPRECATED & ~E_NOTICE'; \
} > /usr/local/etc/php/conf.d/daloradius-errors.ini
COPY --from=composer /usr/bin/composer /usr/bin/composer
# PEAR::DB per Composer beziehen (klassisches include_once('DB.php') von daloRADIUS)
RUN mkdir -p /tmp/pear-build \
&& composer require pear/db --no-interaction --working-dir=/tmp/pear-build \
&& DBPHP=$(find /tmp/pear-build/vendor -iname "DB.php" | head -1) \
&& cp "$DBPHP" /usr/local/lib/php/DB.php \
&& cp -r "$(dirname "$DBPHP")/DB" /usr/local/lib/php/DB \
&& PEARPHP=$(find /tmp/pear-build/vendor -iname "PEAR.php" | head -1) \
&& cp "$PEARPHP" /usr/local/lib/php/PEAR.php \
&& { cp -r "$(dirname "$PEARPHP")/PEAR" /usr/local/lib/php/PEAR 2>/dev/null || true; } \
&& rm -rf /tmp/pear-build
RUN git clone --depth 1 https://github.com/lirantal/daloradius.git /var/www/html/daloradius
RUN cp /var/www/html/daloradius/app/common/includes/daloradius.conf.php.sample \
/var/www/html/daloradius/app/common/includes/daloradius.conf.php
# Datenbank-Zugangsdaten fest im Image hinterlegen
# WICHTIG: <RADIUS_DB_PASSWORD> vor dem Build durch das echte, generierte Passwort ersetzen
RUN sed -i \
-e "s/CONFIG_DB_HOST'\] = 'localhost'/CONFIG_DB_HOST'] = 'mariadb'/" \
-e "s/CONFIG_DB_USER'\] = 'raduser'/CONFIG_DB_USER'] = 'radius'/" \
-e "s/CONFIG_DB_PASS'\] = 'radpass'/CONFIG_DB_PASS'] = '<RADIUS_DB_PASSWORD>'/" \
-e "s/CONFIG_DB_NAME'\] = 'raddb'/CONFIG_DB_NAME'] = 'radius'/" \
/var/www/html/daloradius/app/common/includes/daloradius.conf.php
RUN chown -R www-data:www-data /var/www/html/daloradius
RUN a2enmod rewrite
RUN printf '<VirtualHost *:80>\n\
DocumentRoot /var/www/html/daloradius/app\n\
<Directory /var/www/html/daloradius/app>\n\
AllowOverride All\n\
Require all granted\n\
</Directory>\n\
</VirtualHost>\n' > /etc/apache2/sites-available/000-default.conf
RUN echo '<?php header("Location: operators/"); ?>' > /var/www/html/daloradius/app/index.php
EXPOSE 80
Hinweis zur Passwort-Handhabung im Dockerfile: Das Passwort wird hier bewusst zur Build-Zeit fest eingebaut (nicht per Environment-Variable zur Laufzeit), da daloRADIUS die Konfigurationsdatei nicht aus Umgebungsvariablen liest. Das Docker-Image selbst sollte daher nicht in eine öffentliche Registry gepusht werden, solange dieses Vorgehen genutzt wird.
2.7 docker-compose.yaml (kompletter Stack)¶
services:
mariadb:
image: mariadb:10.11
container_name: radius-mariadb
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: <MARIADB_ROOT_PASSWORD>
MYSQL_DATABASE: radius
MYSQL_USER: radius
MYSQL_PASSWORD: <RADIUS_DB_PASSWORD>
volumes:
- ./mariadb-data:/var/lib/mysql
healthcheck:
test: ["CMD", "mariadb-admin", "ping", "-h", "localhost", "-u", "root", "-p<MARIADB_ROOT_PASSWORD>"]
interval: 10s
timeout: 5s
retries: 5
freeradius:
build: ./freeradius
container_name: freeradius
restart: unless-stopped
ports:
- "1812:1812/udp"
- "1813:1813/udp"
volumes:
- ./config:/etc/freeradius/3.0
- ./certs:/etc/freeradius/3.0/certs
depends_on:
mariadb:
condition: service_healthy
daloradius:
build: ./daloradius
container_name: daloradius
restart: unless-stopped
ports:
- "8000:80"
depends_on:
mariadb:
condition: service_healthy
Ersetzen Sie <MARIADB_ROOT_PASSWORD> und <RADIUS_DB_PASSWORD> durch die in Schritt 2.4 generierten Werte (an beiden Stellen im mariadb-Service identisch).
2.8 FreeRADIUS-Basiskonfiguration extrahieren¶
docker compose build freeradius
docker run --rm -v /docker/freeradius/config:/tmp/out \
--entrypoint sh freeradius-freeradius:latest \
-c "cp -r /etc/freeradius/3.0/* /tmp/out/"
rm -rf /docker/freeradius/config/certs
chown -R 100:101 /docker/freeradius/config /docker/freeradius/certs
Der mitgelieferte certs/-Ordner (Debian-Testzertifikate) wird entfernt, da wir unsere eigenen über den Bind-Mount ./certs:/etc/freeradius/3.0/certs einhängen.
2.9 clients.conf – RADIUS-Clients eintragen¶
/docker/freeradius/config/clients.conf am Ende ergänzen:
client cisco-switch-1 {
ipaddr = <SWITCH_MGMT_IP>
secret = <RADIUS_SHARED_SECRET>
nas_type = cisco
require_message_authenticator = no
}
🔐 Passwort-Safe:
<RADIUS_SHARED_SECRET>muss identisch auf dem Switch (radius server-Konfiguration, siehe Teil 4) hinterlegt sein. Für jeden weiteren Switch einen eigenenclient-Block mit eigenem Secret anlegen.
2.10 SQL-Modul konfigurieren¶
In mods-available/sql folgende Werte anpassen (Zeilen suchen und wie unten setzen):
Im Abschnitt „Connection info" (auskommentierte Zeilen einkommentieren und Werte setzen):
Im mysql { }-Unterblock den tls { }-Abschnitt komplett auskommentieren (verweist standardmässig auf nicht existierende Zertifikatspfade und würde den Modul-Start verhindern, da wir intern im Docker-Netz ohnehin keine TLS-Verschlüsselung zur Datenbank benötigen):
mysql {
# tls {
# ca_file = "/etc/ssl/certs/my_ca.crt"
# ca_path = "/etc/ssl/certs/"
# certificate_file = "/etc/ssl/certs/private/client.crt"
# private_key_file = "/etc/ssl/certs/private/client.key"
# cipher = "DHE-RSA-AES256-SHA:AES128-SHA"
#
# tls_required = yes
# tls_check_cert = no
# tls_check_cert_cn = no
# }
warnings = auto
}
2.11 EAP-TLS konfigurieren¶
In mods-available/eap:
Im tls-config tls-common { }-Block:
private_key_file = /etc/freeradius/3.0/certs/server.key
certificate_file = /etc/freeradius/3.0/certs/server.pem
ca_file = /etc/freeradius/3.0/certs/ca.pem
Die Zeile private_key_password auskommentieren (unser Server-Key hat kein Passwort):
2.12 Windows-Kompatibilität: host/-Präfix korrekt behandeln¶
Hintergrund: Windows sendet bei Computer-Authentifizierung den Benutzernamen mit dem Präfix host/ (z. B. host/laptop-01.domain.tld). Für eine einheitliche Namenskonvention in daloRADIUS (gleich wie bei macOS, ohne Präfix) muss dieses Präfix vor der Datenbankabfrage entfernt werden.
Kritischer Punkt: Das Attribut User-Name selbst darf dabei nicht verändert werden – FreeRADIUS' EAP-Modul vergleicht bei jedem Paket der laufenden TLS-Handshake-Konversation die ursprüngliche EAP-Identität gegen User-Name und bricht bei einer Abweichung mit Identity does not match User-Name. Authentication failed ab. Der gestrippte Name muss daher in ein separates temporäres Attribut geschrieben werden.
In sites-enabled/default, im authorize { }-Block, direkt nach dem eap { ok = return }-Block und vor files/sql einfügen:
In mods-config/sql/main/mysql/queries.conf die Zeile
ersetzen durch:
Das nutzt den gestrippten Namen, falls vorhanden (Windows), und fällt sonst automatisch auf User-Name unverändert zurück (macOS und alle anderen Clients ohne Präfix).
2.13 Konfiguration validieren und starten¶
Exit-Code 0 bestätigt eine syntaktisch korrekte Konfiguration.
Sollte mit „Ready to process requests" enden, ohne SQL- oder TLS-Fehler.
2.14 Datenbankschema importieren¶
docker exec freeradius cat /etc/freeradius/3.0/mods-config/sql/main/mysql/schema.sql | \
docker exec -i radius-mariadb mariadb -u radius -p'<RADIUS_DB_PASSWORD>' radius
Zusätzlich die daloRADIUS-eigenen Schemata (Operatoren, Billing-Tabellen, GUI-Metadaten):
for f in fr3-mariadb-freeradius.sql mariadb-daloradius.sql mariadb-daloradius-dictionaries.sql; do
docker exec daloradius cat /var/www/html/daloradius/contrib/db/$f | \
docker exec -i radius-mariadb mariadb -u radius -p'<RADIUS_DB_PASSWORD>' radius
done
Kontrolle:
Erwartete Tabellen (Auszug): radacct, radcheck, radgroupcheck, radgroupreply, radusergroup, operators, nas.
2.15 daloRADIUS-Login und Erstkonfiguration¶
Im Browser: http://<RADIUS_VM_IP>:8000/
🔐 Passwort-Safe: Das Standardpasswort ist öffentlich bekannt. Sofort nach dem ersten Login unter Management → Operators → Edit Operator ändern und das neue Passwort im Safe hinterlegen.
Teil 3: VLAN-Zuweisung über daloRADIUS-Gruppen¶
3.1 Prinzip¶
Beim EAP-TLS-Handshake setzt FreeRADIUS User-Name automatisch auf den CN des Client-Zertifikats. Die SQL-Abfrage sucht danach in radusergroup, welcher Gruppe dieser Name zugeordnet ist, und liefert die radgroupreply-Attribute dieser Gruppe zurück – inklusive VLAN-Zuweisung.
3.2 Gruppen und VLAN-Zuweisung per SQL anlegen (reproduzierbar, ohne GUI-Klicks)¶
Passen Sie Gruppennamen und VLAN-IDs an Ihre Umgebung an:
docker exec -it radius-mariadb mariadb -u radius -p'<RADIUS_DB_PASSWORD>' radius <<'SQL'
-- Beispiel-Gruppe fuer Corporate-Geraete, VLAN 100
INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES
('corp-vlan100', 'Tunnel-Type', ':=', 'VLAN'),
('corp-vlan100', 'Tunnel-Medium-Type', ':=', 'IEEE-802'),
('corp-vlan100', 'Tunnel-Private-Group-Id', ':=', '100'),
('corp-vlan100', 'Session-Timeout', ':=', '28800'),
('corp-vlan100', 'Termination-Action', ':=', 'RADIUS-Request');
SQL
Bedeutung der einzelnen Attribute:
| Attribut | Zweck |
|---|---|
Tunnel-Type / Tunnel-Medium-Type / Tunnel-Private-Group-Id |
Die drei RFC-2868-Attribute, die Cisco-Switches auswerten, um ein VLAN dynamisch zuzuweisen |
Session-Timeout |
Nach wie vielen Sekunden eine Reauthentifizierung angestossen wird (RFC 2865) |
Termination-Action = RADIUS-Request |
Weist den Switch an, nach Ablauf des Timeouts automatisch und nahtlos zu reauthentifizieren, statt die Session zu beenden. Ohne dieses Attribut (Default: Default) wird die Session bei Ablauf getrennt – bei Geräten mit seltenem Traffic (z. B. Infoscreens, IoT) ein spürbarer Unterbruch. |
Weitere Gruppen (z. B. für IoT-Geräte, andere VLANs) nach demselben Muster mit eigenem Gruppennamen und eigener VLAN-ID anlegen.
3.3 Geräte anlegen und zuordnen¶
Für jedes Gerät (Windows/macOS), dessen Zertifikats-CN Sie beim Enrollment vergeben (siehe Teile 5 und 6):
docker exec -it radius-mariadb mariadb -u radius -p'<RADIUS_DB_PASSWORD>' radius <<SQL
INSERT INTO radusergroup (username, groupname, priority) VALUES
('geraetename.domain.tld', 'corp-vlan100', 0);
SQL
Wichtig: Fügen Sie keinen Eintrag in radcheck (z. B. kein Cleartext-Password) für diese Benutzer hinzu. Ein vorhandenes Passwort-Check-Item kann das rlm_pap-Modul dazu bringen, den bereits durch EAP-TLS gesetzten Auth-Type zu überschreiben, wodurch die Authentifizierung fehlschlägt.
Alternativ lässt sich dieselbe Zuordnung auch über die daloRADIUS-Weboberfläche unter Management → Users → New User (Gruppe zuweisen unter „Assign Group") vornehmen – funktional identisch zum SQL-Weg oben.
3.4 MAC-Adressen für MAB anlegen¶
Für Geräte ohne 802.1X-Supplicant (MAB-Fallback), z. B. Drucker, IoT-Geräte:
docker exec -it radius-mariadb mariadb -u radius -p'<RADIUS_DB_PASSWORD>' radius <<SQL
INSERT INTO radusergroup (username, groupname, priority) VALUES
('aabbccddeeff', 'corp-vlan100', 0);
SQL
Die MAC-Adresse muss dabei exakt im Format vorliegen, das der Switch als Username sendet: 12 Hex-Zeichen, Kleinbuchstaben, ohne Trennzeichen (z. B. aabbccddeeff, nicht AA:BB:CC:DD:EE:FF).
Teil 4: Cisco Switch Konfiguration¶
4.1 Kompatibilitätshinweis¶
Die folgende Konfiguration nutzt die moderne IBNS-2.0-Syntax (authentication ...-Befehle), verifiziert auf einem Catalyst 2960-X mit IOS 15.2(4)E6. Diese Syntax funktioniert ab ca. IOS 12.2(55)SE aufwärts. Bei älteren IOS-Versionen sind stattdessen die klassischen dot1x-Befehle nötig (dot1x port-control auto, dot1x guest-vlan, ohne authentication order/priority – dot1x hat dort implizit Vorrang vor MAB).
4.2 NTP (empfohlen)¶
Ohne Zeitsynchronisation zeigen Logs und Accounting-Daten falsche Zeitstempel:
ntp server <RADIUS_VM_IP>
clock timezone CET 1
clock summer-time CEST recurring last Sun Mar 2:00 last Sun Oct 3:00
4.3 Globale AAA/RADIUS-Konfiguration¶
aaa new-model
aaa authentication dot1x default group radius
aaa authorization network default group radius
aaa accounting dot1x default start-stop group radius
dot1x system-auth-control
radius server FREERADIUS-1
address ipv4 <RADIUS_VM_IP> auth-port 1812 acct-port 1813
key <RADIUS_SHARED_SECRET>
<RADIUS_SHARED_SECRET> muss identisch zum secret-Wert im client-Block in clients.conf sein (siehe 2.9).
4.4 VLANs anlegen¶
900 (Gast) und 999 (kritischer Fallback, falls RADIUS nicht erreichbar) sind Beispielwerte – an Ihre bestehende VLAN-Nummerierung anpassen.
4.5 Interface-Konfiguration (pro Access-Port)¶
interface GigabitEthernet1/0/1
switchport mode access
switchport access vlan 100
authentication event fail action next-method
authentication event server dead action authorize vlan 999
authentication event no-response action authorize vlan 900
authentication event server alive action reinitialize
authentication host-mode multi-auth
authentication order dot1x mab
authentication priority dot1x mab
authentication port-control auto
authentication violation restrict
authentication periodic
authentication timer reauthenticate server
mab
dot1x pae authenticator
dot1x timeout tx-period 5
dot1x max-reauth-req 2
spanning-tree portfast edge
Bedeutung der zentralen Zeilen:
| Befehl | Zweck |
|---|---|
authentication order dot1x mab / authentication priority dot1x mab |
Reihenfolge: zuerst 802.1X (EAPOL) versuchen, bei Timeout auf MAB zurückfallen |
authentication event no-response action authorize vlan 900 |
Gast-VLAN-Zuweisung, falls weder 802.1X noch MAB erfolgreich sind |
authentication event server dead action authorize vlan 999 |
Kritisches Fallback-VLAN, falls FreeRADIUS selbst nicht erreichbar ist – ohne dieses hätten Geräte bei RADIUS-Ausfall gar keinen Netzzugriff mehr |
authentication host-mode multi-auth |
Mehrere Geräte pro Port können unabhängig authentifiziert werden (z. B. Hub/kleiner Switch dahinter) |
authentication periodic + authentication timer reauthenticate server |
Periodische Reauthentifizierung, gesteuert über das Session-Timeout-Attribut aus daloRADIUS (siehe 3.2), statt eines festen lokalen Werts |
4.6 Verifikation¶
Nach erfolgreicher Authentifizierung sollte erscheinen:
Kompakter Überblick über alle Ports:
RADIUS-Kommunikationsstatistik (Access-Requests, Rejects, Timeouts):
Teil 5: macOS Client-Enrollment (SCEP)¶
5.1 Funktionsweise¶
Ein einzelnes .mobileconfig-Profil enthält drei Payloads: Root-CA-Vertrauen, SCEP-Zertifikatsanfrage und die Wired-802.1X-Netzwerkkonfiguration. Der Nutzer installiert das Profil einmalig; macOS fordert daraufhin automatisch ein Zertifikat von der CA an und konfiguriert die Netzwerkverbindung.
5.2 Wahl des Netzwerk-Payload-Typs¶
Für Geräte mit wechselnden USB-Ethernet-Dongles oder Docking-Stations ist com.apple.globalethernet.managed die richtige Wahl – dieser Payload-Typ bindet sich an jede Ethernet-Schnittstelle, unabhängig von der intern von macOS geführten Geräte-Reihenfolge. Die Alternativen com.apple.firstactiveethernetnetwork/com.apple.firstethernetnetwork binden sich nur an eine bestimmte Position in dieser Liste und können bei Hardware-Wechsel „leer laufen".
5.3 Automatische Zertifikatserneuerung¶
Seit macOS Ventura erneuern sich SCEP-ausgestellte Zertifikate automatisch im Hintergrund, auch bei manuell (nicht per MDM) installierten Profilen – ein zusätzliches Renewal-Skript ist auf macOS nicht nötig. Diese automatische Erneuerung lässt sich bei Bedarf deaktivieren:
Hinweis: Testen Sie die automatische Erneuerung an einem Gerät einmalig kurz vor Ablauf der ersten Zertifikatslaufzeit (z. B. nach 11 Monaten bei 1 Jahr Laufzeit), um sicherzustellen, dass es in Ihrer Umgebung reibungslos funktioniert, bevor sich das massenhaft im Feld zeigt.
5.4 Profil-Generator-Skript¶
generate-macos-profile.sh (auf der step-ca-VM ablegen):
#!/bin/bash
# Nutzung: ./generate-macos-profile.sh <geraetename>
# Beispiel: ./generate-macos-profile.sh corp-macbook-01
#
# Erzeugt ein .mobileconfig-Profil fuer SCEP-Enrollment + Wired 802.1X.
set -euo pipefail
DEVICE_NAME="${1:?Nutzung: $0 <geraetename>}"
SCEP_CHALLENGE="<SCEP_CHALLENGE_SECRET>"
SCEP_URL="http://ca.domain.tld:8080/scep/scep-8021x"
CN="${DEVICE_NAME}.domain.tld"
ROOT_CERT_DER_B64=$(docker exec step-ca cat /home/step/certs/root_ca.crt | openssl x509 -outform DER | base64 | tr -d '\n')
ROOT_UUID=$(cat /proc/sys/kernel/random/uuid)
SCEP_UUID=$(cat /proc/sys/kernel/random/uuid)
NET_UUID=$(cat /proc/sys/kernel/random/uuid)
PROFILE_UUID=$(cat /proc/sys/kernel/random/uuid)
OUTDIR="/docker/stepca/profiles"
mkdir -p "$OUTDIR"
OUTFILE="${OUTDIR}/${DEVICE_NAME}.mobileconfig"
cat > "$OUTFILE" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PayloadContent</key>
<array>
<dict>
<key>PayloadType</key>
<string>com.apple.security.root</string>
<key>PayloadIdentifier</key>
<string>tld.domain.8021x.rootca</string>
<key>PayloadUUID</key>
<string>${ROOT_UUID}</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadDisplayName</key>
<string><ORG-8021X-CA> Root</string>
<key>PayloadCertificateFileName</key>
<string>root_ca.der</string>
<key>PayloadContent</key>
<data>${ROOT_CERT_DER_B64}</data>
</dict>
<dict>
<key>PayloadType</key>
<string>com.apple.security.scep</string>
<key>PayloadIdentifier</key>
<string>tld.domain.8021x.scep</string>
<key>PayloadUUID</key>
<string>${SCEP_UUID}</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadDisplayName</key>
<string>802.1X Client Certificate Enrollment</string>
<key>PayloadContent</key>
<dict>
<key>URL</key>
<string>${SCEP_URL}</string>
<key>Name</key>
<string>${CN}</string>
<key>Subject</key>
<array>
<array>
<array>
<string>CN</string>
<string>${CN}</string>
</array>
</array>
</array>
<key>Challenge</key>
<string>${SCEP_CHALLENGE}</string>
<key>Keysize</key>
<integer>2048</integer>
<key>KeyType</key>
<string>RSA</string>
<key>KeyUsage</key>
<integer>5</integer>
<key>Retries</key>
<integer>3</integer>
<key>RetryDelay</key>
<integer>10</integer>
</dict>
</dict>
<dict>
<key>PayloadType</key>
<string>com.apple.globalethernet.managed</string>
<key>PayloadIdentifier</key>
<string>tld.domain.8021x.wired</string>
<key>PayloadUUID</key>
<string>${NET_UUID}</string>
<key>PayloadVersion</key>
<integer>1</integer>
<key>PayloadDisplayName</key>
<string>Wired 802.1X - domain.tld</string>
<key>EAPClientConfiguration</key>
<dict>
<key>AcceptEAPTypes</key>
<array>
<integer>13</integer>
</array>
<key>PayloadCertificateAnchorUUID</key>
<array>
<string>${ROOT_UUID}</string>
</array>
<key>TLSCertificateIsRequired</key>
<true/>
</dict>
<key>PayloadCertificateUUID</key>
<string>${SCEP_UUID}</string>
</dict>
</array>
<key>PayloadDisplayName</key>
<string>802.1X Wired Network - ${DEVICE_NAME}</string>
<key>PayloadIdentifier</key>
<string>tld.domain.8021x.profile.${DEVICE_NAME}</string>
<key>PayloadOrganization</key>
<string>domain.tld</string>
<key>PayloadRemovalDisallowed</key>
<false/>
<key>PayloadType</key>
<string>Configuration</string>
<key>PayloadUUID</key>
<string>${PROFILE_UUID}</string>
<key>PayloadVersion</key>
<integer>1</integer>
</dict>
</plist>
EOF
echo "Profil erstellt: $OUTFILE"
echo "CN des Client-Zertifikats: $CN"
echo "Nicht vergessen: '$CN' in daloRADIUS als User anlegen und einer VLAN-Gruppe zuordnen (siehe Teil 3.3)."
5.5 Profil erzeugen und verteilen¶
Verteilung z. B. per AirDrop, internem Download-Link oder USB. Der Nutzer öffnet die Datei; macOS führt durch die Installation (Systemeinstellungen → Profil installieren).
Nach der Installation: Zertifikats-CN in daloRADIUS als User anlegen (siehe 3.3).
Teil 6: Windows Client-Enrollment¶
6.1 Warum kein SCEP auf Windows¶
Ohne Active-Directory-Domäne bzw. Microsoft Intune bietet Windows keinen zuverlässigen nativen SCEP-Client. Statt eines Drittanbieter-SCEP-Clients nutzen wir die step-CLI direkt mit einem Einmal-Token pro Gerät – das ist robuster und benötigt keine Zusatzsoftware ausser der step-CLI selbst.
6.2 Token pro Gerät generieren (auf der step-ca-VM)¶
docker exec -it step-ca step ca token \
"geraetename.domain.tld" \
--provisioner admin \
--not-after 1h
(Fragt nach dem CA-Administrationspasswort. Die CA begrenzt die Gültigkeit eines Bootstrap-Tokens standardmässig auf 1 Stunde – das reicht für einen einzelnen Enrollment-Vorgang; die tatsächliche Zertifikatslaufzeit richtet sich nach den in 1.11 gesetzten Werten, hier 1 Jahr.)
🔐 Passwort-Safe: Das Token selbst ist nur 1 Stunde gültig und muss nicht dauerhaft gespeichert werden – es dient ausschliesslich als einmaliges Bootstrap-Geheimnis für genau diesen einen Enrollment-Vorgang.
6.3 Root-CA auf dem Windows-Gerät importieren¶
curl.exe -k https://<CA_VM_IP>:9000/roots.pem -o C:\temp\root_ca.pem
certutil -addstore -f "Root" C:\temp\root_ca.pem
(-k/-addstore sind hier für den einmaligen Vertrauens-Bootstrap unproblematisch, da wir genau dieses Root-Zertifikat gerade zum ersten Mal als vertrauenswürdig einrichten.)
6.4 step CLI installieren¶
curl.exe -LO https://dl.smallstep.com/cli/docs-cli-install/latest/step_windows_amd64.zip
Expand-Archive -LiteralPath .\step_windows_amd64.zip -DestinationPath C:\step -Force
$env:Path += ";C:\step\step_windows_amd64\bin"
step version
6.5 Vollständiges Enrollment-Skript¶
enroll-8021x.ps1 (als Administrator ausführen):
# ============================================
# 802.1X Windows Enrollment + Auto-Renewal Setup
# Nutzung: .\enroll-8021x.ps1 -DeviceName "geraetename" -Token "<TOKEN>"
# ============================================
param(
[Parameter(Mandatory=$true)][string]$DeviceName,
[Parameter(Mandatory=$true)][string]$Token
)
$CN = "$DeviceName.domain.tld"
$CaUrl = "https://ca.domain.tld:9000"
$WorkDir = "C:\ProgramData\8021x"
$RootCaPath = "$WorkDir\root_ca.pem"
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
# --------------------------------------------
# 1. Root-CA importieren
# --------------------------------------------
Write-Host "Importiere Root-CA..."
curl.exe -k "https://<CA_VM_IP>:9000/roots.pem" -o $RootCaPath
certutil -addstore -f "Root" $RootCaPath
# --------------------------------------------
# 2. step CLI sicherstellen
# --------------------------------------------
if (-not (Get-Command step -ErrorAction SilentlyContinue)) {
Write-Host "Installiere step CLI..."
curl.exe -LO https://dl.smallstep.com/cli/docs-cli-install/latest/step_windows_amd64.zip
Expand-Archive -LiteralPath .\step_windows_amd64.zip -DestinationPath C:\step -Force
$env:Path += ";C:\step\step_windows_amd64\bin"
[Environment]::SetEnvironmentVariable("Path", $env:Path, "Machine")
}
# --------------------------------------------
# 3. Zertifikat anfordern
# --------------------------------------------
Write-Host "Fordere Zertifikat fuer $CN an..."
step ca certificate $CN "$WorkDir\client.crt" "$WorkDir\client.key" `
--token $Token `
--ca-url $CaUrl `
--root $RootCaPath
if ($LASTEXITCODE -ne 0) {
Write-Error "Zertifikatsanfrage fehlgeschlagen. Abbruch."
exit 1
}
# --------------------------------------------
# 4. PFX bauen (step CLI, kein externes OpenSSL noetig) und importieren
# --------------------------------------------
Write-Host "Importiere Zertifikat in den Windows-Zertifikatsspeicher..."
Add-Type -AssemblyName System.Web
$tempPass = [System.Web.Security.Membership]::GeneratePassword(20, 5)
step certificate p12 "$WorkDir\client.pfx" "$WorkDir\client.crt" "$WorkDir\client.key" `
--ca $RootCaPath --password-file <(Write-Output $tempPass) 2>$null
if (-not (Test-Path "$WorkDir\client.pfx")) {
# Fallback falls --password-file auf dieser step-Version nicht unterstuetzt wird:
$tempPass | step certificate p12 "$WorkDir\client.pfx" "$WorkDir\client.crt" "$WorkDir\client.key" --ca $RootCaPath
}
Import-PfxCertificate -FilePath "$WorkDir\client.pfx" `
-CertStoreLocation Cert:\LocalMachine\My `
-Password (ConvertTo-SecureString $tempPass -AsPlainText -Force)
Remove-Item "$WorkDir\client.pfx" -Force
# --------------------------------------------
# 5. Root-CA-Thumbprint fuer das Netzwerkprofil ermitteln
# --------------------------------------------
$rootThumbprint = (Get-ChildItem Cert:\LocalMachine\Root |
Where-Object { $_.Subject -like "*<ORG-8021X-CA>*" } |
Select-Object -First 1 -ExpandProperty Thumbprint)
if (-not $rootThumbprint) {
Write-Error "Root-CA-Thumbprint nicht gefunden. Bitte manuell in Cert:\LocalMachine\Root pruefen."
exit 1
}
# Thumbprint in das von Windows erwartete Format bringen (durch Leerzeichen getrennte Byte-Paare)
$formattedThumbprint = ($rootThumbprint -split '(?<=\G.{2})(?!$)') -join ' '
# --------------------------------------------
# 6. Wired AutoConfig Dienst aktivieren
# --------------------------------------------
Set-Service -Name "dot3svc" -StartupType Automatic
Start-Service -Name "dot3svc"
# --------------------------------------------
# 7. 802.1X-Netzwerkprofil erzeugen und anwenden
# --------------------------------------------
# WICHTIG: authMode=machineOnly + allowDialog=no vermeiden interaktive Prompts;
# TrustedRootCA vermeidet die manuelle Zertifikatsauswahl-Abfrage, da sonst
# JEDES im Zertifikatsspeicher vorhandene Zertifikat als Kandidat infrage kaeme.
$profileXml = @"
<?xml version="1.0"?>
<LANProfile xmlns="http://www.microsoft.com/networking/LAN/profile/v1">
<MSM>
<security>
<OneXEnforced>false</OneXEnforced>
<OneXEnabled>true</OneXEnabled>
<OneX xmlns="http://www.microsoft.com/networking/OneX/v1">
<EAPConfig>
<EapHostConfig xmlns="http://www.microsoft.com/provisioning/EapHostConfig">
<EapMethod>
<Type xmlns="http://www.microsoft.com/provisioning/EapCommon">13</Type>
<VendorId xmlns="http://www.microsoft.com/provisioning/EapCommon">0</VendorId>
<VendorType xmlns="http://www.microsoft.com/provisioning/EapCommon">0</VendorType>
<AuthorId xmlns="http://www.microsoft.com/provisioning/EapCommon">0</AuthorId>
</EapMethod>
<Config xmlns="http://www.microsoft.com/provisioning/EapHostConfig">
<Eap xmlns="http://www.microsoft.com/provisioning/BaseEapConnectionPropertiesV1">
<Type>13</Type>
<EapType xmlns="http://www.microsoft.com/provisioning/EapTlsConnectionPropertiesV1">
<CredentialsSource>
<CertificateStore>
<SimpleCertSelection>true</SimpleCertSelection>
</CertificateStore>
</CredentialsSource>
<ServerValidation>
<DisableUserPromptForServerValidation>true</DisableUserPromptForServerValidation>
<ServerNames></ServerNames>
<TrustedRootCA>$formattedThumbprint</TrustedRootCA>
</ServerValidation>
<DifferentUsername>false</DifferentUsername>
</EapType>
</Eap>
</Config>
</EapHostConfig>
</EAPConfig>
</OneX>
</security>
</MSM>
</LANProfile>
"@
$profileXml | Out-File -FilePath "$WorkDir\8021x-profile.xml" -Encoding utf8
# Bestehendes Maschinenprofil entfernen (ohne interface= -> betrifft das globale Maschinenprofil)
netsh lan delete profile 2>$null | Out-Null
# Ohne interface= gilt das Profil fuer alle aktuellen UND kuenftigen Ethernet-Adapter
netsh lan add profile filename="$WorkDir\8021x-profile.xml"
netsh lan set profileparameter authMode=machineOnly allowDialog=no
# --------------------------------------------
# 8. Renewal-Skript ablegen
# --------------------------------------------
$renewScript = @'
$WorkDir = "C:\ProgramData\8021x"
$RootCaPath = "$WorkDir\root_ca.pem"
$CaUrl = "https://ca.domain.tld:9000"
$LogFile = "$WorkDir\renewal.log"
function Log($msg) {
"$(Get-Date -Format "yyyy-MM-dd HH:mm:ss") - $msg" | Out-File -Append $LogFile
}
Log "Renewal-Check gestartet."
step ca renew "$WorkDir\client.crt" "$WorkDir\client.key" `
--ca-url $CaUrl --root $RootCaPath --force
if ($LASTEXITCODE -eq 0) {
Log "Renewal erfolgreich, aktualisiere Zertifikatsspeicher."
$oldCerts = Get-ChildItem Cert:\LocalMachine\My | Where-Object { $_.Subject -like "*$env:COMPUTERNAME*" }
foreach ($c in $oldCerts) { Remove-Item $c.PSPath -Force }
Add-Type -AssemblyName System.Web
$tempPass = [System.Web.Security.Membership]::GeneratePassword(20, 5)
$tempPass | step certificate p12 "$WorkDir\client.pfx" "$WorkDir\client.crt" "$WorkDir\client.key" --ca $RootCaPath
Import-PfxCertificate -FilePath "$WorkDir\client.pfx" -CertStoreLocation Cert:\LocalMachine\My `
-Password (ConvertTo-SecureString $tempPass -AsPlainText -Force)
Remove-Item "$WorkDir\client.pfx" -Force
Log "Zertifikat erfolgreich erneuert und importiert."
} else {
Log "WARNUNG: Renewal fehlgeschlagen! Exit-Code $LASTEXITCODE"
}
'@
$renewScript | Out-File -FilePath "$WorkDir\renew-8021x-cert.ps1" -Encoding UTF8 -Force
# --------------------------------------------
# 9. Taegliche Renewal-Aufgabe einrichten
# --------------------------------------------
$action = New-ScheduledTaskAction -Execute "powershell.exe" `
-Argument "-ExecutionPolicy Bypass -File $WorkDir\renew-8021x-cert.ps1"
$trigger = New-ScheduledTaskTrigger -Daily -At 3am
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable -RunOnlyIfNetworkAvailable
Register-ScheduledTask -TaskName "Renew-8021x-Cert" `
-Action $action -Trigger $trigger -Settings $settings `
-RunLevel Highest -User "SYSTEM" -Force
Write-Host ""
Write-Host "=== Fertig ===" -ForegroundColor Green
Write-Host "Geraet $CN ist fuer 802.1X konfiguriert." -ForegroundColor Green
Write-Host "Automatische Erneuerung laeuft taeglich um 03:00 Uhr." -ForegroundColor Green
Write-Host ""
Write-Host "Nicht vergessen: '$CN' in daloRADIUS als User anlegen und einer VLAN-Gruppe zuordnen." -ForegroundColor Yellow
Hinweis: Der Schritt
step certificate p12mit--password-file <(...)funktioniert nur, falls Process Substitution unterstützt wird; das Skript enthält einen einfachen Fallback über Pipe. Prüfen Sie bei Ihrer installiertenstep-CLI-Version (step certificate p12 --help), welche Passwortübergabe unterstützt wird, und passen Sie den Block bei Bedarf an.
6.6 Nutzung¶
Danach in daloRADIUS den User corp-winlaptop-01.domain.tld anlegen und einer VLAN-Gruppe zuordnen (siehe 3.3) – ohne host/-Präfix, da die serverseitige Regel aus 2.12 das Präfix automatisch für die Gruppenzuordnung entfernt.
6.7 Warum kein Active-Directory-GPO-Ansatz¶
Auf Workgroup-Geräten ohne Domänenmitgliedschaft fehlt in gpedit.msc häufig die Möglichkeit, unter „Richtlinien für kabelgebundene Netzwerke (IEEE 802.3)" eine neue Richtlinie zu erstellen (das Kontextmenü zeigt nur „Anzeigen/Aktualisieren/Exportieren", aber keine „Neu erstellen"-Option) – dieser GPO-Mechanismus ist primär für domänengebundene Umgebungen gedacht. Der oben beschriebene netsh lan-Weg funktioniert dagegen unabhängig von einer Domänenmitgliedschaft zuverlässig.
Zusammenfassung: Passwort-Safe-Checkliste¶
Vor dem produktiven Einsatz sicherstellen, dass alle folgenden Geheimnisse im Passwort-Safe hinterlegt sind:
| # | Geheimnis | Fundort | Warum kritisch |
|---|---|---|---|
| 1 | CA-Administrationspasswort | ca_password.txt / data/secrets/password |
Entschlüsselt den Intermediate-Private-Key; ohne dieses Passwort kein Zugriff nach Neustart |
| 2 | Root-CA-Private-Key (Datei) | root_ca_key, offline ausgelagert |
Wichtigster Schlüssel der Infrastruktur – Kompromittierung erlaubt beliebige Zertifikatsausstellung |
| 3 | SCEP-Challenge-Secret | Provisioner-Konfiguration, macOS-Profile | Authentifiziert jedes SCEP-Enrollment; bei Bekanntwerden können unautorisierte Geräte Zertifikate anfordern |
| 4 | MariaDB Root-Passwort | docker-compose.yaml (RADIUS-Stack) |
Voller Datenbankzugriff |
| 5 | MariaDB radius-User-Passwort |
docker-compose.yaml, mods-available/sql, daloRADIUS-Config |
Wird an drei Stellen referenziert – bei Änderung überall synchron aktualisieren |
| 6 | RADIUS Shared Secret (pro Switch) | clients.conf, Switch-Konfiguration |
Ermöglicht das Fälschen von RADIUS-Antworten gegenüber dem Switch bei Bekanntwerden |
| 7 | daloRADIUS-Administrator-Passwort | Nach erstem Login geändert | Default-Passwort ist öffentlich bekannt |
Verifikations-Checkliste nach Abschluss¶
- [ ]
docker exec freeradius freeradius -Cliefert Exit-Code 0 - [ ] Client-Zertifikat wird im Switch-Log mit
Authc Successbestätigt (show authentication sessions ... details) - [ ]
Server Policies: Vlan Group: Vlan: <X>erscheint im Session-Detail (nicht nur der statische Port-Default – am besten einmalig verifizieren, indem das statische Port-VLAN testweise auf einen anderen Wert gesetzt wird) - [ ] Ein Gerät ohne Zertifikat/MAC-Eintrag landet zuverlässig im Gast-VLAN
- [ ] Reauthentifizierung nach Ablauf des
Session-Timeouterfolgt nahtlos (Timeout action: Reauthenticate, unveränderteCommon Session ID) - [ ] Root-CA-Private-Key ist offline ausgelagert, nicht mehr auf der VM vorhanden
- [ ] Alle Einträge aus der Passwort-Safe-Checkliste sind hinterlegt