Mein Workflow zur Veröffentlichungen von Texten

Ich schrei­be gera­de an zwei Publi­ka­tio­nen. Tei­le dar­aus möch­te ich in unter­schied­li­chen Umge­bun­gen und For­ma­ten ver­wen­den. Im wesent­li­chen sind das folgende:

  • es soll die Mög­lich­keit geben, dass ein gedruck­tes Buch erschei­nen kann
  • es soll die Mög­lich­keit geben, dass ein E‑Book in ver­schie­de­nen For­ma­ten erschei­nen kann
  • ich möch­te Tei­le des Manu­skripts in Doku­Wi­ki wie­der­ver­wen­den kön­nen – Erklär­vi­de­os ver­al­ten schlicht zu schnell

Für mich als Nerd sind Text­ver­ar­bei­tungs­pro­gram­me jeder Art völ­lig unbrauch­bar zur Kon­zep­ti­on län­ge­rer Tex­te – und einer mei­ner Tex­te könn­te sehr lang wer­den. Das liegt vor allem dar­an, dass vor der Wei­ter­ga­be eines Manu­skripts meist For­mat­um­wand­lun­gen anste­hen. Ich ken­ne sogar einen Ver­lag, der mit Word­da­tei­en als Druck­vor­stu­fe arbei­tet. Nach einer For­mat­um­wand­lung ste­hen meist auf­wän­di­ge Über­ar­bei­tun­gen der Ursprungs­vor­la­ge an.

LaTeX als Basis

Mein Aus­gangs­for­mat ist LaTeX. LaTeX ist ein Text­satz­sys­tem, an des­sen Aus­ga­be­qua­li­tät kein ande­res mir bekann­tes Sys­tem her­an­kommt. LaTeX ist kei­ne Text­ver­ar­bei­tung, son­dern mag Unbe­darf­te eher an eine Pro­gram­mier­um­ge­bung erin­nern – eine LaTeX-Datei muss immer „über­setzt“ und in z.B. ein PDF umge­wan­delt wer­den, bevor man sehen kann, was man geschrie­ben hat.

Aber selbst simp­ler Text sieht in LaTeX schon auf den ers­ten Blick wesent­lich pro­fes­sio­nel­ler aus als mit einer Text­ver­ar­bei­tung (bit­te auf den Text kli­cken für die voll auf­ge­lös­te Darstellung).

Das liegt an Klei­nig­kei­ten, z.B. Liga­tu­ren (hier mal eine schwarz umran­det). Auch im For­mel­satz setzt LaTeX gleich zwei Maßstäbe:

    \[ U_{H(Ox/Red)} = U_{H(Ox/Red)}^0 + \frac{ 8,314472 \frac{J}{mol \cdot K} \cdot 297K}{z \cdot 96485,3399 \frac{C}{mol}}\cdot 2,3 \cdot lg \left( \frac{c(Ox)}{c(Red)} \right) \]

Ori­gi­nal­ein­ga­be­syn­tax der obe­ren Formel:

U_{H(Ox/Red)} = U_{H(Ox/Red)}^0 + \frac{ 8,314472 \frac{J}{mol \cdot K} \cdot 297K}{z \cdot 96485,3399 \frac{C}{mol}}\cdot 2,3 \cdot lg \left( \frac{c(Ox)}{c(Red)} \right)

Ein­mal sehen For­meln in LaTeX sehr gut aus und zum ande­ren las­sen sie sich ohne Maus und Schal­flä­chen über die Tas­ta­tur schrei­ben. Ich bin auch bei recht ein­fa­chen For­meln meist 2–3x schnel­ler als mit einem gra­fi­schen Formeleditor.

Natür­lich muss man die LaTeX-Syn­tax sehr gut auf dem Kas­ten haben, aber da unter­stüt­zen ent­spre­chen­de LaTeX-Umge­bun­gen wie hier TeX­stu­dio:

LaTeX ist aller­dings nur für Men­schen geeig­net, die sich mit Ent­wick­lungs­um­ge­bun­gen aus­ken­nen und schmerz­frei beim Erler­nen von Syn­tax sind. Belohnt wer­de ich dadurch, dass ich in der ner­vi­gen Nach­be­ar­bei­tung viel weni­ger Auf­wand habe und ein z.B. ein PDF erhal­te, wel­ches sich direkt als Druck­vor­stu­fe für ein gedruck­tes Buch ver­wen­den lässt. Das For­mat lässt sich nach­träg­lich ohne Qua­li­täts­ein­bu­ßen ändern – sogar in Rie­sen­sprün­gen von DINA4 zu DINA5.

In LaTeX lässt sich fast alles set­zen – von Musik­no­ten, mathe­ma­ti­schen For­meln, chi­ne­si­sche Schrift­zei­chen bis hin zu Tabel­len­lay­outs, die mit kei­nem Text­ver­ar­bei­tungs­pro­gramm denk­bar wären.

Pandoc als (mein) Wundermittel

LaTeX-Doku­men­te sind rei­ne Text­da­tei­en. Bil­der oder ande­re exter­ne Datei­en wer­den über Ver­wei­se ein­ge­bun­den. Da LaTeX-Doku­men­te „über­setzt“ wer­den, ist das Aus­ga­be­for­mat zunächst nicht fest­ge­legt. Der Weg von LaTeX zum ver­brei­te­ten E‑Bookformat epub (bzw. epub3) war für mich zunächst stei­nig. Auch in der LaTeX-Com­mu­ni­ty ist da der Kat­zen­jam­mer groß. Ers­te Ver­su­che führ­ten über tex4ht, jedoch war der Auf­wand für die Nach­ar­beit immens.

Gera­de als ich begon­nen hat­te, an mei­ner Ent­schei­dung für LaTeX zu zwei­feln, stieß ich auf pan­doc. Pan­doc ist ein uni­ver­sel­ler Text­kon­ver­ter, der alle mög­li­chen text­ba­sier­ten For­ma­te in alle mög­li­chen text­ba­sier­ten For­ma­te umwan­deln kann. So kom­me ich nun recht unauf­wän­dig z.B. von

  • LaTeX zu epub
  • LaTeX zu Mark­Down (u.a. DokuWiki)
  • LaTeX zu odt oder docx (Aber wer will das außer eini­gen Verlagen?)

Hier mal ein zu epub kon­ver­tier­ter Text in Cal­lib­re geöffnet:

Es gibt tat­säch­lich noch eini­ge Arte­fak­te, die auf die Ver­wen­dung bestimm­ter Erwei­te­run­gen im Ursprungs­do­ku­ment zurück­ge­hen. Da soll­te aber ein ein­fa­cher sed-Lauf als Nach­be­ar­bei­tung genü­gen. Zudem soll­te man Bil­der als png- oder jpg-Datei ins LaTeX-Doku­ment inte­grie­ren – ich muss also beim Schrei­ben das Ziel­for­mat mitdenken.

Aber es funk­tio­niert auch schon so einiges:

  • Inhalts­ver­zeich­nis
  • Fuß­no­ten
  • Bil­der und Bildunterschriften
  • inter­ne und exter­ne Verlinkungen

… das sind nahe­zu 95% des­sen, was ich so benö­ti­ge. In Calib­re kann ich mich dann voll auf das spä­te­re Lay­out des E‑Books konzentrieren.

Noch fluf­fi­ger soll das alles mit Asci­i­Doc statt LaTeX gehen – eini­ge Nerds, die ursprüng­lich mit LaTeX unter­wegs waren, schei­nen dar­auf umzu­schwen­ken oder gleich ein­fa­ches Mark­Down mit pan­doc zu kon­ver­tie­ren. Für mich tut mei­ne Lösung noch genau das, was sie soll.

 

Die ideale Schulcloud

Es ist eini­ge Zeit ver­gan­gen nach einer öffent­li­chen Aus­ein­an­der­set­zung ( hier und hier ), die mei­ner Mei­nung nach eine Men­ge Pro­ble­me auf unter­schied­lichs­ten Ebe­nen auf­zeigt, wenn es dar­um geht, mit Schü­le­rin­nen und Schü­ler in der Schu­le in einer gesi­cher­ten Umge­bung digi­tal zu arbei­ten. In Deutsch­land fir­miert eine sol­che Umge­bung unter dem Schlag­wort „Bil­dungs­cloud“.

Was ist für mich eigent­lich wün­schens­wert, gera­de unter dem Aspekt, dass in Schu­le sehr indi­vi­du­el­le Lern­pro­duk­te von Schü­le­rin­nen und Schü­lern ent­ste­hen? Die sind natür­lich unfer­tig ( das ist das Wesen eines Lern­pro­zes­ses ). Sie erfor­dern je nach Ent­wick­lungs­stand von Schü­le­rin­nen und Schü­lern einen Schutz­raum ( nicht jeder jun­ge Mensch bewegt sich reflek­tiert und kom­pe­tent im Internet).

Und – das ist ja „nur“ die eine Sei­te der Medail­le. Im Vor­der­grund für Lehr­kräf­te, Schü­le­rin­nen und Schü­ler sowie Eltern ste­hen natür­lich auch die Funk­tio­na­li­tät und die intui­ti­ve Bedien­bar­keit – (Daten-)Schutzfragen ste­hen da oft im Hin­ter­grund, weil Bequem­lich­keit ver­lo­ren­geht und dann toben die emo­tio­na­len Gra­ben­kämp­fe ( z.B. hier ).

Der­weil gibt es Lehr­kräf­te, die die­ses Pro­blem indi­vi­du­ell für sich bereits gelöst haben, z.B. Lisa Rosa oder Andre­as Kalt. Auf viel nied­ri­ge­rem Niveau habe ich das in mei­nem Unter­richt auch schon oft gemacht. Nur brauch­ten wir alle dafür kei­ne Lern­platt­form oder Schul­cloud. Aller­dings ist ein Min­dest­maß an Kennt­nis­sen im Bereich Daten­schutz und im Umgang mit Web­tools erfor­der­lich (z.B. um dabei kei­ne per­so­nen­be­zo­ge­nen Daten zu pro­du­zie­ren) – vie­le Lehr­kräf­te sind bereits bei ver­meint­lich klei­nen Schrit­ten im digi­ta­len Raum rest­los über­for­dert. Da macht eine gemein­schaft­li­che Lösung inkl. War­tung, tech­ni­schem- und Anwen­der­sup­port ein­fach Sinn.

In einer idealen Welt

In einer idea­len Welt haben wir

  1. eine Platt­form auf Open­So­ur­ce-Basis, die von allen Betei­lig­ten im Hin­blick auf Funk­tio­na­li­tät und Bedien­bar­keit akzep­tiert ist
  2. einen gut auf­ge­stell­ten tech­ni­schen und Anwendersupport
  3. eine dezi­dier­te Tren­nung von per­so­nen- und inhalts­be­zo­ge­nen Daten
  4. die tech­ni­sche Bereit­sstel­lung (Hos­ting) durch die öffent­li­che Hand
  5. doku­men­tier­te Schnitt­stel­len zu kom­mer­zi­el­len Anbie­tern (z.B. Unter­richts­ma­te­ri­al, Medi­en, inter­ak­ti­ve Übun­gen), die eine Anony­mi­sie­rung der Schü­ler­da­ten ermöglichen
In der Realität

Die Punk­te 1–2 gibt es zur­zeit nur im kom­mer­zi­el­len Bereich – da muss man auf die Punk­te 3–5 ver­zich­ten. Die Punk­te 3–5 gibt es nur bei den größ­ten­teils geschei­ter­ten Län­der­platt­for­men. Da muss man zur­zeit aber auf die Punk­te 1–2 ver­zich­ten – zumin­dest zu 50–60% der jewei­li­gen indi­vi­du­el­len Anforderungen.

For­mal, d.h. recht­lich kom­plett abge­si­chert, ist kei­ne der denk­ba­ren Vari­an­ten – damit haben sämt­li­che Ent­wick­lungs­teams übri­gens auch stark zu kämp­fen – tech­nisch wäre prin­zi­pi­ell viel mög­lich, aber die Rechts­nor­men geben das i.d.R. noch nicht oder nur mit gro­ßen ver­wal­tungs­tech­ni­schen Klimm­zü­gen – z.B. die qua­li­fi­zier­te Ein­wil­li­gung – her.

Das öff­net das Tor für Kri­tik. Kom­mer­zi­el­le Lösun­gen von Goog­le, Micro­soft oder Apple sind „böse“, weil sie Daten der Schü­le­rin­nen und Schü­ler ver­ar­bei­ten. Öffent­li­che Lösun­gen sind ent­we­der „böse“, weil sie – mit weni­gen Aus­nah­men – die Funk­tio­na­li­tät nicht bie­ten, Rechts­nor­men nicht erfül­len oder wahl­wei­se intrans­pa­rent ent­wi­ckelt und finan­ziert wer­den (fak­tisch stimmt das alles sogar).

Was also tun?

Vari­an­te 1:

Man zwingt mul­ti­na­tio­na­le(!) Kon­zer­ne durch Rechts­nor­men dazu, ein hohes, vom Staat defi­nier­tes Schutz­ni­veau für die Ver­ar­bei­tung von Schü­ler- und Ver­wal­tungs­da­ten zu gewähr­leis­ten. Man schafft Rechts­si­cher­heit z.B. durch eine staat­li­che Zer­ti­fi­zie­rung. Das ist nicht so tri­vi­al. Wir haben im Bereich der Poli­tik und teil­wei­se auch in der recht­li­chen Unter­stüt­zung nach mei­ner Ein­schät­zung nicht das not­wen­di­ge Know-How dafür. Ich pro­phe­zeie, dass die dabei ent­ste­hen­den Rechts­nor­men nicht ihren eigent­li­chen Zweck erfül­len wer­den. Die Big5 waren die ers­ten, die z.B. kom­plett auf die DS-GVO ein­ge­stellt waren und nut­zen die­se, um noch mehr Zugriffs­mög­lich­kei­ten zu erhal­ten – kön­nen sie auch. Da gibt es aus­rei­chend juris­ti­sches Know-How.

Vari­an­te 2:

Wir ent­wi­ckeln öffent­lich finan­zier­te Lösun­gen und schaf­fen einen recht­li­chen Rah­men zu deren Nut­zung. Das ist nicht so tri­vi­al. Wir haben im Bereich der Poli­tik, im tech­ni­schen Bereich und teil­wei­se auch in der recht­li­chen Unter­stüt­zung nach mei­ner Ein­schät­zung nicht das not­wen­di­ge Know-How dafür. Zudem ist der Staat als Arbeit­ge­ber für die dazu not­wen­di­gen idea­lis­ti­schen Men­schen z.B. durch Büro­kra­tie­vor­ga­ben aber auch Gehalts­bin­dung durch Tarif­recht nicht kon­kur­renz­fä­hig. Des­we­gen wird ja ver­sucht, sol­che Ent­wick­lun­gen out­zu­s­our­cen – z.B. ans HPI. Das ist aber in der Wahr­neh­mung in der Öffent­lich­keit oft auch „böse“. Was muss also ein kom­mer­zi­el­ler Anbie­ter auf die­sem Feld für Kri­te­ri­en erfül­len, um nicht „böse“ zu sein?

(Ich bin kein Freund des Ansat­zes des HPI. Nicht umsonst ist Bay­ern mit Mebis eigent­lich schon an wei­tes­ten, weil dort etwas Bestehen­des genutzt und erwei­tert wird).

Einschätzung

Ich glau­be, dass jeder, der undif­fe­ren­ziert(!) auf öffent­li­che oder „unbe­que­me“ Open­So­ur­ce-Ansät­ze „schießt“, im Grun­de die Vari­an­te 1 för­dert. Dar­auf wird es hin­aus­lau­fen. Die meis­ten Lehr­kräf­te, die digi­tal unter­wegs sind, wird es freu­en. Ob es ein Gewinn für die Demo­kra­tie wird … – wir wer­den sehen. Eine Tren­nung von per­so­nen­be­zo­ge­nen und inhalts­be­zo­ge­nen Daten (das wür­de tech­no­lo­gisch eine effek­ti­ve Kon­trol­le schaf­fen) ist dort erfah­rungs­ge­mäß weder gewünscht noch vor­ge­se­hen – das ist schlicht nicht das Geschäftskonzept.

Kom­plett die Hoff­nung habe ich ver­lo­ren, dass sich der­ar­ti­ge Fra­ge­stel­lun­gen über jour­na­lis­ti­sche Medi­en in den öffent­li­chen Dis­kurs ein­brin­gen las­sen. „Digi­tal“ ist in der Berichts­er­stat­tung in der Regel gleich „Tablet“ oder „inter­ak­ti­ve Tafel“ – Gerä­te­fo­kus­sie­rung von 2003–2019.

Ich bil­de mir noch ein, dass das prin­zi­pi­ell mög­lich wäre und hal­te auf mei­nen Fort­bil­dun­gen tap­fer dage­gen (da kom­men noch drei wei­te­re Teile).

 

 

 

 

 

 

Lustige Portweiterleitungen

Unse­re Schul­home­page besitzt einen eige­nen Log­in­be­reich, für den wir ger­ne auch die Nutzername/Passwortwortkombination nut­zen woll­ten wie für den Schul­ser­ver. Meh­re­re unter­schied­li­che Zugän­ge sind in der Regel nut­zer­un­freund­lich und wer­den kaum akzeptiert.

Das ver­wen­de­te Joom­la! hat zum Glück eine Rei­he von Authen­ti­fi­zie­rungs­plug­ins, unter ande­rem LDAP, POP3, IMAP oder Kebe­ros. Am ein­fachs­ten geht es über IMAP, d.h. Joom­la! ver­sucht sich mit den Nut­zer­da­ten bei Mail­ser­ver des Schul­ser­vers ein­zu­log­gen und wenn das klappt, legt es einen neu­en Benut­zer­ac­count an, den es zukünf­tig immer extern authen­ti­fi­ziert. Dum­mer­wei­se klapp­te das bei uns nur über eine unver­schlüs­sel­te Ver­bin­dung zuver­läs­sig – also kei­ne sinn­vol­le Option.

Glück­li­cher­wei­se läuft unser Joom­la! auf einem VSer­ver, auf den wir Shell­zu­griff haben. Mit einem linux­ty­pi­schen Ein­zei­ler kann man den Mail­ser­ver­port durch einen ver­schlüs­sel­ten Kanal auf den VSer­ver tun­neln – bei uns:

ssh ‑R 1143:localhost:143 unprivileged@vserver.xy ‑N ‑T

Das sorgt dafür, das der unver­schlüs­sel­te Mail­ser­ver­port 143 auf dem VSer­ver unter der Port­num­mer 1143 erreich­bar ist.

Nor­ma­ler­wei­se wür­de nach die­sem Kom­man­do das Pass­wort des Benut­zer „unpri­vi­le­ged“ (der Nut­zer soll­te mög­lichst wenig Rech­te auf dem Ziel­ser­ver haben, wes­halb die Port­num­mer auch grö­ßer als 1024 sein muss) erfragt wer­den. Damit das nicht geschieht, ver­wen­den wir die Authen­ti­fi­zie­rung per Public-Key.

Unser Schul­ser­ver ist nur per VDSL an das Inter­net ange­bun­den und wird ein­mal täg­lich pro­vi­der­sei­tig vom Netz getrennt. Damit wür­de unser Tun­nel zusam­men­bre­chen. Damit das erkannt wird, läuft fol­gen­des Script per cron­job alle fünf Minuten:

#!/bin/bash
COUNT=‚ps aux | grep unpri­vi­le­ged | wc ‑l‚
if [ $COUNT ‑ge 2 ]; then
exit 0
else
ssh ‑R 1143:localhost:143 unprivileged@vserver.xy ‑N ‑T
fi

Die Varia­ble COUNT ent­hält die Aus­ga­be der Befehls­pi­pe zwi­schen den Back­ticks ‚. Wenn der Tun­nel offen ist, ent­hält die Aus­ga­be zwei Zei­len (den eigent­li­chen Tun­nel­pro­zess und den Such­pro­zess nach „unpri­vi­le­ged“). Wenn das so ist, tut das Script nichts, wenn nicht, star­tet es den Tun­nel ein­fach neu. Das Script muss mit Root­rech­ten lau­fen, da ein Port unter­halb von 1024 lokal ver­wen­det wird.

Auf die­se Wei­se kann man im Prin­zip jeden Dienst lokal auf einen VSer­ver wei­ter­lei­ten, z.B. auf den LDAP der Mus­ter­lö­sung aus Baden-Würt­tem­berg und muss dann kei­ne Klar­text­pass­wör­ter mehr durch die Gegend schicken.

Radiusserver gegen LDAP authentifizieren lassen

Allgemein

Die­ser Ein­trag basiert auf die­ser Ori­gi­nal­an­lei­tung. free­ra­di­us ist ein Authen­ti­fi­zie­rungs­ser­ver, der nach außen das Radi­us­pro­to­koll bereit­stellt. Über die­ses Pro­to­koll kann man sich z.B. an einem WLAN anmel­den, ohne die Zugangs­da­ten für OpenLDAP oder jede belie­bi­ge ande­re Authen­ti­fi­zie­rungs­quel­le zu ken­nen und ver­tei­len zu müs­sen. Wenn ein zen­tra­ler Ver­zeich­nis­dienst kon­fi­gu­riert ist, wer­den z.B. sehr ein­fach Din­ge mög­lich wie ein kreis­wei­tes WLAN. In den Schu­len muss dann ledig­lich ein neu­es WLAN-Netz kon­fi­gu­riert wer­den, wel­ches gegen unse­ren zen­tra­len Radi­us authen­ti­fi­ziert und schon kann ich als Leh­rer der Schu­le A im Netz der Schu­le B z.B. bei Fort­bil­dung das WLAN nutzen.

Radiusschema in OpenLDAP integrieren

Damit die Authen­ti­fi­zie­rung über Radi­us mit alle denk­ba­ren Funk­tio­nen klappt, soll­te man ein neu­es Sche­ma zu OpenLDAP hin­zu­fü­gen. Es funk­tio­niert auch ohne, nur kommt man bei spä­te­ren Erwei­te­rungs­w­üb­schen schnell an Gren­zen. Hier ist ein Sche­ma bereits vor­be­rei­tet (freeradius_schema.ldif), wel­ches free­ra­di­us als Text­da­tei mit­bringt. Es lässt sich direkt über die Kon­so­le in cn=config einspielen.

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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
dn: cn=freeradius,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: freeradius
olcAttributeTypes: {0}( 1.3.6.1.4.1.3317.4.3.1.1 NAME 'radiusArapFeatures' DES
 C '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-
 VALUE )
olcAttributeTypes: {1}( 1.3.6.1.4.1.3317.4.3.1.2 NAME 'radiusArapSecurity' DES
 C '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-
 VALUE )
olcAttributeTypes: {2}( 1.3.6.1.4.1.3317.4.3.1.3 NAME 'radiusArapZoneAccess' D
 ESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGL
 E-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.4.1.3317.4.3.1.44 NAME 'radiusAuthType' DESC '
 checkItem: Auth-Type' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115
 .121.1.26 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.4.1.3317.4.3.1.4 NAME 'radiusCallbackId' DESC 
 'replyItem: Callback-Id' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.
 115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.4.1.3317.4.3.1.5 NAME 'radiusCallbackNumber' D
 ESC 'replyItem: Callback-Number' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4
 .1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {6}( 1.3.6.1.4.1.3317.4.3.1.6 NAME 'radiusCalledStationId' 
 DESC 'checkItem: Called-Station-Id' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.
 1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {7}( 1.3.6.1.4.1.3317.4.3.1.7 NAME 'radiusCallingStationId'
  DESC 'checkItem: Calling-Station-Id' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.
 6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.4.1.3317.4.3.1.8 NAME 'radiusClass' DESC 'repl
 yItem: Class' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.2
 6 )
olcAttributeTypes: {9}( 1.3.6.1.4.1.3317.4.3.1.45 NAME 'radiusClientIPAddress'
  DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SIN
 GLE-VALUE )
olcAttributeTypes: {10}( 1.3.6.1.4.1.3317.4.3.1.9 NAME 'radiusFilterId' DESC '
 replyItem: Filter-Id' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115
 .121.1.26 )
olcAttributeTypes: {11}( 1.3.6.1.4.1.3317.4.3.1.10 NAME 'radiusFramedAppleTalk
 Link' DESC 'replyItem: Framed-AppleTalk-Link' EQUALITY caseIgnoreIA5Match SYN
 TAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {12}( 1.3.6.1.4.1.3317.4.3.1.11 NAME 'radiusFramedAppleTalk
 Network' DESC 'replyItem: Framed-AppleTalk-Network' EQUALITY caseIgnoreIA5Mat
 ch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {13}( 1.3.6.1.4.1.3317.4.3.1.12 NAME 'radiusFramedAppleTalk
 Zone' DESC 'replyItem: Framed-AppleTalk-Zone' EQUALITY caseIgnoreIA5Match SYN
 TAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {14}( 1.3.6.1.4.1.3317.4.3.1.13 NAME 'radiusFramedCompressi
 on' DESC 'replyItem: Framed-Compression' EQUALITY caseIgnoreIA5Match SYNTAX 1
 .3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {15}( 1.3.6.1.4.1.3317.4.3.1.14 NAME 'radiusFramedIPAddress
 ' DESC 'replyItem: Framed-IP-Address' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.
 6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {16}( 1.3.6.1.4.1.3317.4.3.1.15 NAME 'radiusFramedIPNetmask
 ' DESC 'replyItem: Framed-IP-Netmask' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.
 6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {17}( 1.3.6.1.4.1.3317.4.3.1.16 NAME 'radiusFramedIPXNetwor
 k' DESC 'replyItem: Framed-IPX-Network' EQUALITY caseIgnoreIA5Match SYNTAX 1.
 3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {18}( 1.3.6.1.4.1.3317.4.3.1.17 NAME 'radiusFramedMTU' DESC
  'replyItem: Framed-MTU' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.
 115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {19}( 1.3.6.1.4.1.3317.4.3.1.18 NAME 'radiusFramedProtocol'
  DESC 'replyItem: Framed-Protocol' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1
 .4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {20}( 1.3.6.1.4.1.3317.4.3.1.19 NAME 'radiusFramedRoute' DE
 SC 'replyItem: Framed-Route' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1
 466.115.121.1.26 )
olcAttributeTypes: {21}( 1.3.6.1.4.1.3317.4.3.1.20 NAME 'radiusFramedRouting' 
 DESC 'replyItem: Framed-Routing' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4
 .1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {22}( 1.3.6.1.4.1.3317.4.3.1.46 NAME 'radiusGroupName' DESC
  '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {23}( 1.3.6.1.4.1.3317.4.3.1.47 NAME 'radiusHint' DESC '' E
 QUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE 
 )
olcAttributeTypes: {24}( 1.3.6.1.4.1.3317.4.3.1.48 NAME 'radiusHuntgroupName' 
 DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {25}( 1.3.6.1.4.1.3317.4.3.1.21 NAME 'radiusIdleTimeout' DE
 SC 'replyItem: Idle-Timeout' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1
 466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {26}( 1.3.6.1.4.1.3317.4.3.1.22 NAME 'radiusLoginIPHost' DE
 SC 'replyItem: Login-IP-Host' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.
 1466.115.121.1.26 )
olcAttributeTypes: {27}( 1.3.6.1.4.1.3317.4.3.1.23 NAME 'radiusLoginLATGroup' 
 DESC 'replyItem: Login-LAT-Group' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.
 4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {28}( 1.3.6.1.4.1.3317.4.3.1.24 NAME 'radiusLoginLATNode' D
 ESC 'replyItem: Login-LAT-Node' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.
 1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {29}( 1.3.6.1.4.1.3317.4.3.1.25 NAME 'radiusLoginLATPort' D
 ESC 'replyItem: Login-LAT-Port' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.
 1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {30}( 1.3.6.1.4.1.3317.4.3.1.26 NAME 'radiusLoginLATService
 ' DESC 'replyItem: Login-LAT-Service' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.
 6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {31}( 1.3.6.1.4.1.3317.4.3.1.27 NAME 'radiusLoginService' D
 ESC 'replyItem: Login-Service' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1
 .1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {32}( 1.3.6.1.4.1.3317.4.3.1.28 NAME 'radiusLoginTCPPort' D
 ESC 'replyItem: Login-TCP-Port' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.
 1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {33}( 1.3.6.1.4.1.3317.4.3.1.29 NAME 'radiusPasswordRetry' 
 DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SING
 LE-VALUE )
olcAttributeTypes: {34}( 1.3.6.1.4.1.3317.4.3.1.30 NAME 'radiusPortLimit' DESC
  'replyItem: Port-Limit' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.
 115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {35}( 1.3.6.1.4.1.3317.4.3.1.49 NAME 'radiusProfileDn' DESC
  '' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SING
 LE-VALUE )
olcAttributeTypes: {36}( 1.3.6.1.4.1.3317.4.3.1.31 NAME 'radiusPrompt' DESC ''
  EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALU
 E )
olcAttributeTypes: {37}( 1.3.6.1.4.1.3317.4.3.1.50 NAME 'radiusProxyToRealm' D
 ESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGL
 E-VALUE )
olcAttributeTypes: {38}( 1.3.6.1.4.1.3317.4.3.1.51 NAME 'radiusReplicateToReal
 m' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 S
 INGLE-VALUE )
olcAttributeTypes: {39}( 1.3.6.1.4.1.3317.4.3.1.52 NAME 'radiusRealm' DESC '' 
 EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE
  )
olcAttributeTypes: {40}( 1.3.6.1.4.1.3317.4.3.1.32 NAME 'radiusServiceType' DE
 SC 'replyItem: Service-Type' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1
 466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {41}( 1.3.6.1.4.1.3317.4.3.1.33 NAME 'radiusSessionTimeout'
  DESC 'replyItem: Session-Timeout' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1
 .4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {42}( 1.3.6.1.4.1.3317.4.3.1.34 NAME 'radiusTerminationActi
 on' DESC 'replyItem: Termination-Action' EQUALITY caseIgnoreIA5Match SYNTAX 1
 .3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {43}( 1.3.6.1.4.1.3317.4.3.1.35 NAME 'radiusTunnelAssignmen
 tId' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
  )
olcAttributeTypes: {44}( 1.3.6.1.4.1.3317.4.3.1.36 NAME 'radiusTunnelMediumTyp
 e' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {45}( 1.3.6.1.4.1.3317.4.3.1.37 NAME 'radiusTunnelPassword'
  DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SIN
 GLE-VALUE )
olcAttributeTypes: {46}( 1.3.6.1.4.1.3317.4.3.1.38 NAME 'radiusTunnelPreferenc
 e' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {47}( 1.3.6.1.4.1.3317.4.3.1.39 NAME 'radiusTunnelPrivateGr
 oupId' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.
 26 )
olcAttributeTypes: {48}( 1.3.6.1.4.1.3317.4.3.1.40 NAME 'radiusTunnelServerEnd
 point' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.
 26 )
olcAttributeTypes: {49}( 1.3.6.1.4.1.3317.4.3.1.41 NAME 'radiusTunnelType' DES
 C '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {50}( 1.3.6.1.4.1.3317.4.3.1.42 NAME 'radiusVSA' DESC '' EQ
 UALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 )
olcAttributeTypes: {51}( 1.3.6.1.4.1.3317.4.3.1.43 NAME 'radiusTunnelClientEnd
 point' DESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.
 26 )
olcAttributeTypes: {52}( 1.3.6.1.4.1.3317.4.3.1.53 NAME 'radiusSimultaneousUse
 ' DESC 'checkItem: Simultaneous-Use' SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SIN
 GLE-VALUE )
olcAttributeTypes: {53}( 1.3.6.1.4.1.3317.4.3.1.54 NAME 'radiusLoginTime' DESC
  '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-V
 ALUE )
olcAttributeTypes: {54}( 1.3.6.1.4.1.3317.4.3.1.55 NAME 'radiusUserCategory' D
 ESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGL
 E-VALUE )
olcAttributeTypes: {55}( 1.3.6.1.4.1.3317.4.3.1.56 NAME 'radiusStripUserName' 
 DESC '' SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {56}( 1.3.6.1.4.1.3317.4.3.1.57 NAME 'dialupAccess' DESC ''
  EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALU
 E )
olcAttributeTypes: {57}( 1.3.6.1.4.1.3317.4.3.1.58 NAME 'radiusExpiration' DES
 C 'checkItem: Expiration' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466
 .115.121.1.26 SINGLE-VALUE )
olcAttributeTypes: {58}( 1.3.6.1.4.1.3317.4.3.1.59 NAME 'radiusCheckItem' DESC
  'checkItem: $GENERIC$' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.1
 15.121.1.26 )
olcAttributeTypes: {59}( 1.3.6.1.4.1.3317.4.3.1.60 NAME 'radiusReplyItem' DESC
  'replyItem: $GENERIC$' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.1
 15.121.1.26 )
olcAttributeTypes: {60}( 1.3.6.1.4.1.3317.4.3.1.61 NAME 'radiusNASIpAddress' D
 ESC '' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGL
 E-VALUE )
olcAttributeTypes: {61}( 1.3.6.1.4.1.3317.4.3.1.62 NAME 'radiusReplyMessage' D
 ESC 'replyItem: Reply-Message' EQUALITY caseIgnoreIA5Match SYNTAX 1.3.6.1.4.1
 .1466.115.121.1.26 )
olcObjectClasses: {0}( 1.3.6.1.4.1.3317.4.3.2.1 NAME 'radiusprofile' DESC '' S
 UP top AUXILIARY MUST cn MAY ( radiusArapFeatures $ radiusArapSecurity $ radi
 usArapZoneAccess $ radiusAuthType $ radiusCallbackId $ radiusCallbackNumber $
  radiusCalledStationId $ radiusCallingStationId $ radiusClass $ radiusClientI
 PAddress $ radiusFilterId $ radiusFramedAppleTalkLink $ radiusFramedAppleTalk
 Network $ radiusFramedAppleTalkZone $ radiusFramedCompression $ radiusFramedI
 PAddress $ radiusFramedIPNetmask $ radiusFramedIPXNetwork $ radiusFramedMTU $
  radiusFramedProtocol $ radiusCheckItem $ radiusReplyItem $ radiusFramedRoute
  $ radiusFramedRouting $ radiusIdleTimeout $ radiusGroupName $ radiusHint $ r
 adiusHuntgroupName $ radiusLoginIPHost $ radiusLoginLATGroup $ radiusLoginLAT
 Node $ radiusLoginLATPort $ radiusLoginLATService $ radiusLoginService $ radi
 usLoginTCPPort $ radiusLoginTime $ radiusPasswordRetry $ radiusPortLimit $ ra
 diusPrompt $ radiusProxyToRealm $ radiusRealm $ radiusReplicateToRealm $ radi
 usServiceType $ radiusSessionTimeout $ radiusStripUserName $ radiusTerminatio
 nAction $ radiusTunnelClientEndpoint $ radiusProfileDn $ radiusSimultaneousUs
 e $ radiusTunnelAssignmentId $ radiusTunnelMediumType $ radiusTunnelPassword 
 $ radiusTunnelPreference $ radiusTunnelPrivateGroupId $ radiusTunnelServerEnd
 point $ radiusTunnelType $ radiusUserCategory $ radiusVSA $ radiusExpiration 
 $ dialupAccess $ radiusNASIpAddress $ radiusReplyMessage ) )
olcObjectClasses: {1}( 1.3.6.1.4.1.3317.4.3.2.2 NAME 'radiusObjectProfile' DES
 C 'A Container Objectclass to be used for creating radius profile object' SUP
  top STRUCTURAL MUST cn MAY ( uid $ userPassword $ description ) )

Ein­ge­spielt wird es mit:

1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f freeradius_schema.ldif

Die Objekt­de­fi­ni­tio­nen geben durch ihren Namen schon einen Hin­weis dar­auf, was noch alles mög­lich ist. Inter­net­pro­vi­der set­zen des­we­gen oft genau auf die­ses Protokoll.

Freeradius für die Nutzung von LDAP konfigurieren

Falls noch nicht gesche­hen, muss free­ra­di­us zunächst instal­liert wer­den. Bei Debi­an und sei­nen Deri­va­ten tut es ein Einzeiler:

1
apt-get install freeradius

Jetzt sind eini­ge Kon­fi­gu­ra­ti­ons­da­tei­en zu bearbeiten: 

/etc/freeradius/modules/ldap
1
2
3
4
5
6
7
8
server = "localhost"
identity = "cn=admin,dc=domain,dc=tld"
password = <secret>
basedn = "ou=test,dc=domain,dc=tld"
filter = "(uid=%{%{Stripped-User-Name}:-%{User-Name}})"
base_filter = "(objectclass=radiusprofile)"
access_attr = "dialupAccess"
password_attribute = userPassword

dc=domain,dc=tld ist natür­lich an den eige­nen LDAP anzu­pas­sen. Der Bin­du­ser unter „iden­ti­ty“ muss Lese­zu­griff auf Attri­bu­te des Radi­us-Sche­mas haben. Die­se müs­sen in der Regel extra gewährt / kon­fi­gu­riert wer­den, eigent­lich soll­te man das nicht so ger­ne über den Haupt­ad­min des Bau­mes lösen.

/etc/freeradius/sites-enabled/default & /etc/freeradius/sites-enabled/inner-tunnel

Vor fol­gen­de Zei­len in bei­den Datei­en die Kom­men­tar­zei­chen ent­fer­nen (Abschnitt aut­ho­ri­ze / authen­ti­ca­te):

1
2
3
4
ldap
Auth-Type LDAP {
   ldap
}

Jetzt kann man bei­de Diens­te neu starten:

1
2
service slapd restart
service freeradius restart

Testen des Einstellungen

Nun kann man über­prü­fen, ob das Log­in gegen LDAP funktioniert:

1
radtest "test_ldap_user" "test_ldap_passwort" localhost 18120 "secret"

secret fin­det man in /etc/freeradius/clients.conf in der Sek­ti­on „local­host“. Wenn man von wei­te­ren IPs aus authen­ti­fi­zie­ren möch­te, muss man ein­fach für jeden Rech­ner einen neu­en Block anle­gen. Vor­kon­fi­gu­riert ist bei Debi­an und Deri­va­ten für local­host „testing123“. Wenn alles klappt, soll­te die Aus­ga­be etwa so aussehen:

1
2
3
4
5
6
7
Sending Access-Request of id 213 to 127.0.0.1 port 1812
	User-Name = "<test_ldap_user>"
	User-Password = "<test_ldap_passwort>"
	NAS-IP-Address = 127.0.0.1
	NAS-Port = 18120
	Message-Authenticator = 0x00000000000000000000000000000000
rad_recv: Access-Accept packet from host 127.0.0.1 port 1812, id=213, length=20

Wich­tig ist das Access-Accept packet am Schluss. Klappt es aus irgend­wel­chen Grün­den nicht, gibt es ein Access-Reject packet.

OpenLDAP ab 2.4 installieren und einrichten

Vorweg

Mir ist kei­ne Quel­le im Netz bekannt, die die Ein­rich­tung von OpenLDAP wirk­lich umfas­send dar­stellt – schon gar nicht auf Deutsch. Die­se Infor­ma­tio­nen hier sind aus allen mög­li­chen Ecken zusam­men­ge­klaubt – selbst die meis­ten Bücher zu OpenLDAP emp­fin­de ich als sehr wenig hilfreich.

Grundinstallation

Hin­weis: domain.tld muss man natür­lich immer an den eige­nen openLDAP anpassen.

Zuerst instal­lie­ren wir die Bina­ries, in Debi­an und sei­nen Abkömm­lin­gen (Ubun­tu, Mint etc.) z.B. so:

1
apt-get install slapd ldap-utils

Die Instal­la­ti­ons­rou­ti­ne von Debi­an legt dabei nur eine sehr rudi­men­tä­re Kon­fi­gu­ra­ti­on an, sodass etwas Nach­ar­beit von­nö­ten ist. Bei ande­ren Dis­tri­bu­tio­nen ken­ne ich mich nicht so gut aus. Ein

1
dpkg-reconfigure slapd

ermög­licht uns hier die Ein­ga­be einer kor­rek­ten Basis-DN (auf die muss unser SSL-Zer­ti­fi­kat aus­ge­stellt sein), meist sowas wie

  • dc=domain, dc=tld

und zusätz­lich defi­nie­ren wir dabei ein Root­pass­wort für den LDAP-User

  • cn=admin,dc=domain,dc=de

.

Handling von OpenLDAP

Ab Debi­an Squeeze spei­chert der OpenLDAP-Ser­ver sei­ne Kon­fi­gu­ra­ti­on in einem inter­nen LDAP-Baum und nicht mehr in einem Kon­fi­gu­ra­ti­ons­file. Das macht die Pfle­ge auf den ers­ten Blick erheb­lich auf­wän­di­ger, weil man an die­sen Baum in der Stan­dard­kon­fi­gu­ra­ti­on nur umständ­lich über Kon­so­len­tools her­an­kommt. Zudem kann eine feh­ler­haf­te Daten­bank dazu füh­ren, dass der OpenLDAP nach einer Kon­fi­gu­ra­ti­ons­än­de­rung gar nicht mehr hochkommt.
Nur der Root­be­nut­zer des Sys­tems kommt immer auch direkt an die Daten. Man kann sich die bestehen­den Inhalt nur als root anzei­gen las­sen mit:

1
ldapsearch -Y EXTERNAL -H ldapi:/// -b "cn=config"

Neue Ein­trä­ge kön­nen über *.ldif-Files hin­zu­ge­fügt werden:

1
ldapmodify/ldapadd -Y EXTERNAL -H ldapi:/// -f
Hinweis zu Ubuntu 14.04 LTS

Der Instal­ler setzt den Account­na­men für den Benut­zer mit Zugriff auf den cn=config-Baum stan­dard­mä­ßig auf: cn=admin,dc=domain,dc=tld. Dann macht man Befol­gen die­ser Anlei­tung ein lan­ges Gesicht. Um das auf den Stan­dard zu ändern, benö­tigt man nur für Ubun­tu 14.04 LTS noch eine klei­ne Ände­rung (change_admin.ldif):

1
2
3
4
dn: olcDatabase={0}config,cn=config
changetype: modify
replace: olcRootDN
olcRootDN: cn=admin,cn=config

Obli­ga­to­risch:

1
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f change_admin.ldif

Sicherheit

OpenLDAP-Verbindungen per TLS absichern

Vor­weg: Hier kann ganz viel schief­ge­hen, obwohl ich die­ses Kapi­tel mit am wich­tigs­ten fin­de. Wenn OpenLDAP aus irgend­wel­chen Grün­den die Zer­ti­fi­kat­files nicht frisst, kann man mit der Kon­fi­gu­ra­ti­on von vor­ne begin­nen oder man hat vor­her ein Back­up der alten Daten­bank gemacht. Des­we­gen über­sprin­ge ich die­sen Schritt ger­ne und bin­de den OpenLDAP ein­fach nicht an öffent­lich erreich­ba­re Netz­werk­de­vices. Wenn man das machen muss, führt aus Daten­schutz­grün­den aber kein Weg an die­ser Pro­ze­dur hier vor­bei. LDAP ist genau wie FTP ein Klar­text­pro­to­koll, dass ohne Trans­port­ver­schlüs­se­lung auf dem gesam­ten Daten­weg offen­liegt und gera­de in WLAN-Umge­bun­gen sehr leicht belauscht wer­den kann.
Gene­rell gibt es zwei Mög­lich­kei­ten, wie man an kos­ten­lo­se Zer­ti­fi­ka­te kom­men kann. Wosign oder Start­S­SL. Es gibt diver­se Tuto­ri­als im Netz zur Nut­zung die­ser Diens­te. Von Let­sen­crypt wür­de ich im Kon­text von OpenLDAP eher abra­ten. Start­S­SL und WoSign sind mitt­ler­wei­le Geschich­te. Wenn es kos­ten­los sein soll, führt kein Weg an let­sen­crypt vor­bei. Man muss dann dafür sor­gen, dass OpenLDAP nach jedem Cert­up­date neu gestar­tet wird, also alle drei Mona­te mindestens.

Man hat am Ende des Zer­ti­fi­zie­rungs­pro­zes­ses in der Regel drei Datei­en vorliegen:

  1. domain.tld-crt.pem (ent­hält das Zertifikat)
  2. domain.tld-key.pem (ent­hält den pri­va­ten Schlüssel)
  3. ca_chain.pem (ent­hält die Zer­ti­fi­zie­rungs­chain der CA)

domain.tld ist dabei der Wur­zel­baum des openLDAP. Ich habe die Datei­en nach /etc/ldap/ssl gelegt. Bes­ser auf­ho­ben sind sie in /etc/ssl/cert – dann muss slapd Lese­rech­te dort bekommen.

Fol­gen­de Datei (tls_ldap.ldif) anlegen:

1
2
3
4
5
6
7
8
9
dn: cn=config
add: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ldap/ssl/ca_chain.pem
-
add: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: /etc/ldap/ssl/domain.tld-key.pem
-
add: olcTLSCertificateFile
olcTLSCertificateFile: /etc/ldap/ssl/domain.tld-crt.pem

… und über die Kon­so­le einspielen:

1
ldapadd -Y EXTERNAL -H ldapi:/// -f tls_ldap.ldif

oder auch

1
ldapmodify -Y EXTERNAL -H ldapi:/// -f tls_ldap.ldif

Ich bin zusätz­lich ein Freund davon, siche­re Ver­bin­dun­gen zu erzwin­gen. Cli­ents, die das nicht wol­len oder kön­nen, sol­len bit­te draußenbleiben.

Fol­gen­de Datei (force_tls.ldif) anlegen:

1
2
3
4
dn: olcDatabase={1}hdb,cn=config
changetype:  modify
add: olcSecurity
olcSecurity: tls=1

Und wie­der einspielen:

1
ldapadd -Y EXTERNAL -H ldapi:/// -f force_tls.ldif

oder auch

1
ldapmodify -Y EXTERNAL -H ldapi:/// -f force_tls.ldif

Jetzt noch in /etc/default/slapd nach­schau­en, ob die Ser­vices stim­men (ldaps über Port 639 gilt als ver­al­tet und soll­te nicht mehr ver­wen­det wer­den). In der Regel steht da so etwas:

1
SLAPD_SERVICES="ldap://0.0.0.0:389/ ldapi:///"

Wenn man meh­re­re NICs besitzt, kann man natür­lich statt 0.0.0.0 auch die IP einer spe­zi­fi­schen Netz­werk­kar­te ange­ben oder den OpenLDAP nur an local­host (127.0.0.1) binden.

Ein

1
service slapd restart

bringt Auf­klä­rung, ob das Gan­ze funk­tio­niert hat. Theo­re­tisch ist das nicht not­wen­dig, da OpenLDAP durch das neue Ver­fah­ren ohne Kon­fi­gu­ra­ti­ons­da­tei qua­si live im Betrieb gepatcht wird. Jetzt soll­te der OpenLDAP Ver­bin­dun­gen von außen nur noch ver­schlüs­selt akzep­tie­ren. Von der Kon­so­le aus ( ldapi:/// ) klappt das nach wie vor auch nor­mal. Wir ver­trau­en uns ja schon selbst.

Bruteforce erschweren

Ein offe­ner LDAP-Ser­ver ist anfäl­lig für bru­te-force Atta­cken – zumal gera­de im Schul­be­reich vie­le unsi­che­re Pass­wör­ter im Umlauf sein dürf­ten. Durch das ppolicy.schema kann man z.B. nach eini­gen fehl­ge­schla­ge­nen Log­ins den Account für eine Wei­le auto­ma­tisch sper­ren. openLDAP bringt das dafür not­wen­di­ge Sche­ma in /etc/ldap/schema/ppolicy.ldif schon mit.

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
dn: cn=ppolicy,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: ppolicy
olcAttributeTypes: {0}( 1.3.6.1.4.1.42.2.27.8.1.1 NAME 'pwdAttribute' EQUALITY
  objectIdentifierMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.38 )
olcAttributeTypes: {1}( 1.3.6.1.4.1.42.2.27.8.1.2 NAME 'pwdMinAge' EQUALITY in
 tegerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {2}( 1.3.6.1.4.1.42.2.27.8.1.3 NAME 'pwdMaxAge' EQUALITY in
 tegerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {3}( 1.3.6.1.4.1.42.2.27.8.1.4 NAME 'pwdInHistory' EQUALITY
  integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {4}( 1.3.6.1.4.1.42.2.27.8.1.5 NAME 'pwdCheckQuality' EQUAL
 ITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {5}( 1.3.6.1.4.1.42.2.27.8.1.6 NAME 'pwdMinLength' EQUALITY
  integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {6}( 1.3.6.1.4.1.42.2.27.8.1.7 NAME 'pwdExpireWarning' EQUA
 LITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {7}( 1.3.6.1.4.1.42.2.27.8.1.8 NAME 'pwdGraceAuthNLimit' EQ
 UALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {8}( 1.3.6.1.4.1.42.2.27.8.1.9 NAME 'pwdLockout' EQUALITY b
 ooleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {9}( 1.3.6.1.4.1.42.2.27.8.1.10 NAME 'pwdLockoutDuration' E
 QUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {10}( 1.3.6.1.4.1.42.2.27.8.1.11 NAME 'pwdMaxFailure' EQUAL
 ITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )
olcAttributeTypes: {11}( 1.3.6.1.4.1.42.2.27.8.1.12 NAME 'pwdFailureCountInter
 val' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE
 )
olcAttributeTypes: {12}( 1.3.6.1.4.1.42.2.27.8.1.13 NAME 'pwdMustChange' EQUAL
 ITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {13}( 1.3.6.1.4.1.42.2.27.8.1.14 NAME 'pwdAllowUserChange'
 EQUALITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {14}( 1.3.6.1.4.1.42.2.27.8.1.15 NAME 'pwdSafeModify' EQUAL
 ITY booleanMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.7 SINGLE-VALUE )
olcAttributeTypes: {15}( 1.3.6.1.4.1.4754.1.99.1 NAME 'pwdCheckModule' DESC 'L
 oadable module that instantiates "check_password() function' EQUALITY caseExa
 ctIA5Match SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE )
olcObjectClasses: {0}( 1.3.6.1.4.1.4754.2.99.1 NAME 'pwdPolicyChecker' SUP top
  AUXILIARY MAY pwdCheckModule )
olcObjectClasses: {1}( 1.3.6.1.4.1.42.2.27.8.2.1 NAME 'pwdPolicy' SUP top AUXI
 LIARY MUST pwdAttribute MAY ( pwdMinAge $ pwdMaxAge $ pwdInHistory $ pwdCheck
 Quality $ pwdMinLength $ pwdExpireWarning $ pwdGraceAuthNLimit $ pwdLockout $
  pwdLockoutDuration $ pwdMaxFailure $ pwdFailureCountInterval $ pwdMustChange
  $ pwdAllowUserChange $ pwdSafeModify ) )

Ein­ge­spielt wird das Sche­ma mit:

1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f ppolicy.ldif

Das Sche­ma ppolicy.ldif selbst defi­niert nur Objek­te für das ent­spre­chen­de Modul, was jetzt noch gela­den wer­den muss, wofür wir eine Datei policy_module.ldif mit fol­gen­dem Inhalt anlegen:

1
2
3
4
dn: cn=module{0},cn=config
changetype: modify
add: olcModuleLoad
olcModuleLoad: ppolicy.la

Und das alte Spiel:

1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f policy_module.ldif

Jetzt brau­chen wir noch eine Abla­ge (policy_context.ldif) für die ver­schie­de­nen Regelsätze:

1
2
3
4
dn: ou=policies,dc=domain,dc=tld
objectClass: organizationalUnit
objectClass: top
ou: policies
1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f policy_context.ldif

Und als nächs­tes eine Default-Poli­cy (default_policy.ldif):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
dn: cn=default,ou=policies,dc=domain,dc=tld
objectClass: top
objectClass: device
objectClass: pwdPolicy
cn: default
pwdAttribute: 2.5.4.35
pwdMaxAge: 15552000
pwdInHistory: 3
pwdMinLength: 6
pwdMaxFailure: 3
pwdLockout: TRUE
pwdLockoutDuration: 1800
pwdGraceAuthNLimit: 3
pwdMustChange: TRUE
pwdAllowUserChange: TRUE
pwdSafeModify: TRUE

In die­sen Bei­spiel wird nach drei fehl­ge­schla­ge­nen Log­in­ver­su­chen ( pwd­Max­Fail­ure: 3 ) das Log­in für 1800 Sekun­den ( pwd­Lock­out­Du­ra­ti­on: 1800 ) gesperrt. Das soll­te kleb­rig genug sein.

Muss ich es noch schreiben?

1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f default_policy.ldif

Da OpenLDAP ja so fluf­fig und intui­tiv ist, brau­chen wir jetzt noch ein Over­lay (policy_overlay.ldif), dass dem OpenLDAP sagt, dass statt des nor­ma­len Log­in­hand­lings jetzt immer auch die Default-Poli­cy gel­ten soll:

1
2
3
4
5
6
7
dn: olcOverlay=ppolicy,olcDatabase={1}hdb,cn=config
objectClass: olcOverlayConfig
objectClass: olcPPolicyConfig
olcOverlay: ppolicy
olcPPolicyDefault: cn=default,ou=policies,dc=domain,dc=tld
olcPPolicyHashCleartext: TRUE
olcPPolicyUseLockout: TRUE

Ihr wisst, was jetzt kommt:

1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f policy_overlay.ldif

Damit hät­ten wir grund­sätz­lich ver­schlüs­sel­te Ver­bin­dun­gen erzwun­gen und zusätz­lich Bru­teforce-Angrif­fe erschwert. Bleibt noch eines zu tun:

Anonymous-Bind verbieten

Stan­dard­mä­ßig erlaubt openLDAP einen soge­nann­te anony­mous bind, d.h. man erhält lesend Zugriff auch ohne die Ein­ga­be eines Pass­wor­tes. Die­se lesen­de Zugriff ist sehr ein­ge­schränkt, z.B. gibt es kei­nen Zugriff auf bestimm­te Objekt­klas­sen oder gar Pass­wort­hash­es. Mir ist die Vor­stel­lung trotz­dem nicht geheu­er, dass sich u.a. Nut­zer­na­men auf die­sem Weg aus­le­sen las­sen. Daher ver­wen­de ich für den lesen­den Zugriff einen sepa­ra­ten User, der sich mit Pass­wort authen­ti­fi­zie­ren muss, ansons­ten aber nicht mehr Rech­te als beim anony­mous bind hat. Des­we­gen unter­bin­den wir das mit einer neu­en Datei noanonymous.ldif:

1
2
3
4
5
6
7
dn: olcDatabase={1}hdb,cn=config
add: olcRequires
olcRequires: authc
 
dn: olcDatabase={-1}frontend,cn=config
add: olcRequires
olcRequires: authc

Und jetzt kommt etwas ande­res, weil wir einen bereits bestehen­den Daten­bank­ein­trag aktualisieren:

1
ldapmodify -Y EXTERNAL -H ldapi:/// -f noanonymous.ldif

Nach dem Ein­spie­len der letz­ten Ände­rung hat man ohne Authen­ti­fi­zie­rung auch über die Kon­so­le kei­nen Zugriff mehr auf den Haupt­baum des OpenLDAP (dc=domain, dc=tld) – die war bis­her auch sowas wie „anonym“ aus Sicht des LDAP. Man muss dann aus­wei­chen auf eine ande­re Befehls­zei­le (cn=config ist davon nicht betroffen):

1
ldapadd -x -D cn=admin,dc=domain,dc=tld -W -f

Danach wird man zur Ein­ga­be des Admin­pass­wor­tes auf­ge­for­dert und kann so den Haupt­baum beschrei­ben und verändern.

Optionale Arbeiten

Performancetuning

Die­se Datei ( index.ldif )

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
dn: olcDatabase={1}hdb,cn=config
changetype: modify
add: olcDbIndex
olcDbIndex: cn pres,sub,eq
-
add: olcDbIndex
olcDbIndex: sn pres,sub,eq
-
add: olcDbIndex
olcDbIndex: uid pres,sub,eq
-
add: olcDbIndex
olcDbIndex: displayName pres,sub,eq
-
add: olcDbIndex
olcDbIndex: default sub
-
add: olcDbIndex
olcDbIndex: uidNumber eq
-
add: olcDbIndex
olcDbIndex: gidNumber eq
-
add: olcDbIndex
olcDbIndex: mail,givenName eq,subinitial
-
add: olcDbIndex
olcDbIndex: dc eq

ein­spie­len mit

1
ldapadd -Q -Y EXTERNAL -H ldapi:/// -f index.ldif
Benutzer für die Administration von cn=config einrichten

Wenn man sau­ch den cn=config-Baum auch über kom­for­ta­ble­re Frontends wie phplda­pad­min oder LAM ver­wal­ten möch­te, muss man das Objekt cn=admin, cn=config noch um wei­te­re Ein­trä­ge ergän­zen. Zunächst erzeu­gen wir uns über die Kon­so­le ein Passwort:

1
slappasswd -h {SSHA}

Wir erhal­ten einen Hash zurück, den wir in die Zwi­schen­ab­la­ge kopie­ren. Jetzt erstell­ten wir ein ldif-File (manager.ldif):

1
2
3
4
5
6
7
8
9
10
11
dn: olcDatabase={0}config,cn=config
changetype: modify
add: olcRootPW
olcRootPW: {SSHA}
 
# auskommentieren, wenn wir den Zugriff von root  auf cn=config 
# ohne Passwort sperren wollen. Sollte als Fallback besser erhalten bleiben
 
#dn: olcDatabase={0}config,cn=config
#changetype: modify
#delete: olcAccess
1
ldapmodify -Q -Y EXTERNAL -H ldapi:/// -f manager.ldif

Wenn wir jetzt in phplda­pad­min (ab Ver­si­on 1.2.2) oder lam als Base-DN cn=config ver­wen­den und als Log­in cn=admin, cn=config, kön­nen wir cn=config auch gra­fisch verwalten.

Quel­le: https://wiki.debian.org/PhpLdapAdmin

War doch ganz einfach oder?

OpenLDAP ist extrem sper­rig, aber eine her­vor­ra­gen­de Authen­ti­fi­zie­rungs­mög­lich­keit, da im Gegen­satz zu Daten­bank­sys­te­men die Struk­tur hoch­gra­dig stan­dar­di­siert sowie mus­ter­gül­tig objekt­ori­en­tiert ist und sich OpenLDAP so recht schnell in belie­bi­ge Anwen­dun­gen inte­grie­ren lässt – fast alle ernst­zu­neh­men­den Online­tools unter­stüt­zen die Authen­ti­fi­zie­rung über LDAP. OpenLDAP dien­te nicht umsonst nicht als Vor­la­ge für die LDAP-Funk­tio­nen von Samba4 – weil es eben so sper­rig ist.