Beim Lesen dieser Seiten könnte evt. der Eindruck entstehen, ich wüsste, was ich
da tue, legte Wert auf Qualität oder würde sonst in irgendeiner Weise Ergebnisse
produzieren, die man weiter verwenden könne.
Dem ist nicht so :-D
Warnung: Man sollte die Dinge, die ich hier beschreibe, eher nicht nachmachen.
Ich habe gestern meine private PKI von XCA auf
CFSSL umgestellt. Und ich nutze das jetzt
“in anger”, also ernsthaft - die alte CA mitsamt der Keys habe ich weggeworfen.
Und ja, es war ein wenig “rage” beteiligt. Nicht, weil XCA ein schlechtes
Produkt ist, ganz im Gegenteil. Das Problem war, dass es so gut funktioniert
hat, dass ich eine alte Regel viel zu lange befolgt habe: Die Rede ist von
“80/20”, aka Paretoprinzip. Und
während das für viele Bereiche stimmen mag, habe ich in den letzten Jahren das
Gefühl gehabt, in der IT verbringt man irgendwann mindestens 80% seiner Zeit
mit den 20%, die man nicht weg automatisiert hat.
Intermezzo: Lustigerweise hat Martin Alfke das in einem Nebensatz in seinem
diesjährigen OSDC-Talk genau
so gesagt.
Eigentlich[tm] wollte ich mir nur ein Wildcard-Zertifikat für meine lokale
Kubernetes-Testumgebung erstellen, genauer gesagt halt für den Ingress. Dabei
habe ich dann aber übersehen, dass X.509 V3 SANs voneinander durch Kommata
getrennt werden müssen - und nicht nur ich, sondern auch XCA. Also stand ich
dann irgendwann kurz vor elf Uhr abends nicht mit der ersehnten Webseite da,
sondern mit einer Zertifikat-Warnung vom Default-Ingress-Fake-Zertifikat und
einem nginx-Ingress, der sich über das Zertifikat beschwert hat. Und da wurde mir
plötzlich klar: Stefan, das musst Du anders machen.
Die Idee war dann also, das CFSSL-Toolkit zu benutzen. Mein hauptsächlicher
Anwendungszweck sind OpenVPN-Zertifikate, das mit den HTTPS-Sachen ist nur
Beiwerk. OpenVPN kann kein OCSP, aber CRLs, also wäre es irgendwie wichtig, dass
das CFSSL auch CRLs generieren kann. Dazu braucht es Persistenz, i.e. eine
Datenbank. Und, na ja, wie soll ich sagen: Das ganze geht auch in “ziemlich
dreckig”.
Fangen wir mal mit den einfachen Sachen an: CFSSL kann man als Service
betreiben, und dank systemd
ist das ziemlich stressfrei, als Vorlage mag das
folgende cfssl.service
-File dienen:
1
2
3
4
5
6
7
8
9
10
|
[Unit]
Description=Certificate Authority Server
Requires=network-online.target
After=network-online.target
[Service]
ExecStart=/usr/bin/cfssl serve -ca /etc/cfssl/ca.pem -ca-key /etc/cfssl/ca-key.pem -config /etc/cfssl/config.json -db-config /etc/cfssl/sqlite_db.json -address 0.0.0.0
Restart=always
Restart=always
RestartSec=10
|
Man sieht schon, das ist alles “oddly specific”, ihr merkt schon noch, warum.
Als Datenbank habe ich mich für SQLite entschieden, die Konfiguration in
sqlite_db.json
ist denkbar simpel:
1
2
3
4
|
{
"driver": "sqlite3",
"data_source": "/var/lib/cfssl/certdb.db"
}
|
Zertifikat und Key sollten glaube ich klar sein, bleibt noch die Konfiguration.
Ich wollte unbedingt wieder Zwischen-CAs haben, damit ich den Schlüssel für die
root-CA irgendwo hin packen und vergessen kann. Eine Zwischen-CA verwende ich
dafür, End-Device-Zertifikate auszustellen, und eine weitere für Kubernetes, da
muss noch eine Ebene dazwischen gehen. Zudem wollte ich sinnvolle Profile für
OpenVPN. Das ganze sieht dann so aus:
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
|
{
"signing": {
"default": {
"auth_key": "ca_auth",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
],
"expiry": "8760h"
},
"profiles": {
"server_ca": {
"auth_key": "ca_auth",
"expiry": "43800h",
"usages": [
"cert sign",
"crl sign"
],
"ca_constraint": {
"is_ca": true,
"max_path_len": 0,
"max_path_len_zero": true
}
},
"k8s_ca": {
"auth_key": "ca_auth",
"expiry": "43800h",
"usages": [
"cert sign",
"crl sign"
],
"ca_constraint": {
"is_ca": true,
"max_path_len": 1
}
},
"openvpn_server": {
"auth_key": "ca_auth",
"expiry": "8760h",
"usages": [
"digital signature",
"key encipherment",
"server auth"
]
},
"openvpn_client": {
"auth_key": "ca_auth",
"expiry": "8760h",
"usages": [
"signing",
"client auth"
]
}
}
},
"auth_keys": {
"ca_auth": {
"type": "standard",
"key": "0123456789ABCDEF0123456789ABCDEF"
}
}
}
|
Der abgedruckte Authentifizierungs-Key ist aus der Doku ;-)
Bleibt noch das Erstellen der SQLite-Datenbank. Dazu nimmt man aus dem
Sourcecode das File
001_CreateCertificate.sql,
entledigt sich der DROP TABLE
-Statements am Ende und jagt das ganze nach
sqlite3
; das Skript dazu nannte ich create-sqlitedb.sh
:
1
2
3
4
5
6
7
8
|
#!/bin/bash
db=/var/lib/cfssl/certdb.db
sql=/usr/lib/cfssl/001_CreateCertificates.sql
if [ ! -f $db ]; then
sqlite3 $db < $sql
fi
|
Et voilà. Das ganze verpackt man dann noch schön in ein Paket - in meinem Fall
für Debian. Dazu muss man eigentlich nur go
installieren, den Source ziehen,
kompilieren, verstehen, wie man ein Debian-Paket baut und… naja, ne. Es gibt
da was, das nennt sich fpm - Jordan hat
das auf einem Konferenz-Vortrag mal als “Rageware” bezeichnet, und weil ich eh
schon mies drauf war (und fpm
in der Vergangenheit schon benutzt hatte) dachte
ich mir, “nimmste das halt her”. Zudem saß ich ja an einem Arch Linux, und da war
das CFSSL-Kit schon installiert,
und Binaries, die man mit go
erstellt hat… und so. Vor allem “und so”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
#!/bin/bash
# THIS NEEDS FPM!
set -e
VERSION="1.3.2-1"
tmpdir=$(mktemp -d)
trap "rm -rf $tmpdir" EXIT
# director setup
mkdir -p $tmpdir{/usr/bin,/etc/cfssl,/var/lib/cfssl,/lib/systemd/system,/usr/lib/cfssl}
cp $(which cfssl) $tmpdir/usr/bin/
cp $(which cfssljson) $tmpdir/usr/bin/
cp sqlite_db.json config.json $tmpdir/etc/cfssl
cp cfssl.service $tmpdir/lib/systemd/system/
cp 001_CreateCertificates.sql $tmpdir/usr/lib/cfssl
# generate DEB
fpm -s dir -t deb -n cfssl-service -v $VERSION \
-d sqlite3 -C $tmpdir \
--after-install create-sqlitedb.sh \
usr/bin etc/cfssl var/lib/cfssl usr/lib/cfssl lib/systemd/system
|
Man sieht, da sind nicht mal die Basics drin, sowas wie Dienst stoppen wenn man
das Paket deinstalliert und so. Aber soll ich Euch was sagen? Ist mir egal! ;-)
Jetzt kann man wunderbar cfssl
nutzen, um sich Zertifikate von der Gegenstelle
ausstellen zu lassen, die Konfiguration dazu ist:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
{
"auth_keys": {
"ca_key": {
"type": "standard",
"key": "0123456789ABCDEF0123456789ABCDEF"
}
},
"signing": {
"default": {
"auth_remote": {
"remote": "cfssl_server",
"auth_key": "ca_key"
}
}
},
"remotes": {
"cfssl_server": "remote.hostname:8888"
}
}
|
Zu guter Letzt habe ich etwas Shell-Kleber drum rum gemacht - und obwohl ich
alles andere als stolz darauf bin gibt’s den jetzt der Vollständigkeit halber:
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
|
#!/bin/bash
usage() {
{
echo "Usage: $(basename $0) [ -p <profile> ] [ -h <comma-sep SAN list> ] [ -t <csr_template> ] [ -c <config> ]<cn>";
echo;
echo "Valid profiles:";
echo "* openvpn_server";
echo "* openvpn_client";
} 1>&2
exit 1
}
profile=""
hostnames=""
cn=""
template=~/.cfssl/csr.json
config=~/.cfssl/remote.json
# parse params
while getopts ":p:h:t:c:" o; do
case "$o" in
p)
profile=$OPTARG
;;
h)
hostnames=$OPTARG
;;
t)
template=$OPTARG
;;
c)
config=$OPTARG
;;
*)
usage
;;
esac
done
shift $((OPTIND-1))
# check for cn
if [ $# -ne 1 ]; then
usage
fi
cn=$1
# check for template and config
if [ ! -f $template ]; then
echo "template CSR $template not found"
exit 1
fi
if [ ! -f $config ]; then
echo "(remote) signing config $config not found"
exit 1
fi
# check profile, if given
if [ -n "$profile" ]; then
case "$profile" in
openvpn_server)
;;
openvpn_client)
;;
*)
echo "Invalid profile given"
usage
;;
esac
fi
# some sanity checking for insane users
if [ -z "$profile" -a -z "$hostnames" ]; then
echo "WARNING: default certificate reqested, yet no hostnames given"
echo "setting hostnames == $cn"
echo "press Ctrl + C if you don't want this"
read dummy
hostnames="$cn"
fi
if [ -n "$profile" -a -n "$hostnames" ]; then
echo "WARNING: non-SAN enabled profile given, yet hostnames added"
echo "unsetting hostnames"
echo "press Ctrl + C if you don't want this"
read dummy
hostnames=""
fi
# generate template csr
csr=$(mktemp)
trap "rm -f $csr" EXIT
sed "s,COMMON_NAME,$cn,g" $template > $csr
# generate certificate
# cfssl gencert -config remote_signing.json -profile openvpn_client /tmp/csr.json \
#| cfssljson -bare n8_h2
if [ -n "$hostnames" ]; then
h_p="-hostname=$hostnames"
else
h_p=""
fi
cfssl gencert -config=$config -profile=$profile $h_p $csr | cfssljson -bare $cn
|
Der CSR dazu ist ein Template:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
{
"cn": "COMMON_NAME",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "DE",
"ST": "Bayern",
"L": "Muenchen",
"O": "incertum.net"
}
]
}
|
Eigentlich[tm] wollte ich helm-Charts schreiben. Oder
mich mit Prometheus beschäftigen. Jetzt habe ich TLS.
Wie das Leben manchmal so spielt…