802.1x Wi-Fi with FreeIPA workstation certificates
FreeRADIUS: Check against LDAP
LDAP checks theory
As I noted in very first part of this guide, I made impossible to normally revoke
certificate issued using caHostCert
profile.
But I still need mechanism to prevent computer from connecting Wi-Fi network.
In case I become aware of a possible compromise of the certificate and private key of the workstation, I should normally block this workstation from authorizing to anything.
FreeIPA, by default, does not have any possibility to disable computer, only to unenroll
.
Unenrolling may be less secure than deleting computer from directory in some cases, so I
will delete computer from directory in case it is potentially compromised.
The first check that the provided certificate is valid is to check whether the computer entity for which the certificate was issued exists in LDAP.
Certificates issued with caHostCert
profile have Subject Alternative Name (SAN) with
type UPN
which looks like host/hostname.od.freeipa.xyz@OD.FREEIPA.XYZ
.
Within FreeRADIUS, this value is stored in request:TLS-Client-Cert-Subject-Alt-Name-Upn
variable in sections authenticate
(not at the first step) and post-auth
.
Computers have the save value in LDAP attribute krbPrincipalName
.
I will make server to check if there is computer with this UPN in LDAP.
Secondary, I will check certificate issue date (aka notBefore
) stored in
request:TLS-Client-Cert-Valid-Since
against computer creation date. In FreeIPA,
computer (and any other object) creation date is stored in createTimestamp
attribute
which is operational
attribute. This means this attribute is for internal LDAP server
usage.
LDAP account for RADIUS service
Create system account for radius service.
Check if this system account have permissions to read see all necessary attributes
by searching for workstation
computer:
ldapsearch -x \
-H 'ldaps://ds1.od.freeipa.xyz:636' \
-D 'uid=svc_radius_wifi,cn=sysaccounts,cn=etc,dc=od,dc=freeipa,dc=xyz' \
-b 'cn=computers,cn=accounts,dc=od,dc=freeipa,dc=xyz' \
-s one \
-LLL \
-W \
'(krbPrincipalName=host/workstation.od.freeipa.xyz@OD.FREEIPA.XYZ)' \
krbPrincipalName createTimestamp
As a result you will see something like this:
dn: fqdn=workstation.od.freeipa.xyz,cn=computers,cn=accounts,dc=od,dc=freeipa,dc=xyz
krbPrincipalName: host/workstation.od.freeipa.xyz@OD.FREEIPA.XYZ
createTimestamp: 20241019002622Z
LDAP filter
We use request:TLS-Client-Cert-Valid-Since
(string) and
request:TLS-Client-Cert-Subject-Alt-Name-Upn
(string) as data for our LDAP request.
Both are strings and typical values are:
TLS-Client-Cert-Valid-Since = "241020164306Z"
TLS-Client-Cert-Subject-Alt-Name-Upn = "host/workstation.od.freeipa.xyz@OD.FREEIPA.XYZ"
From LDAP side corresponding attribute names, typical values, syntax and rules are:
createTimestamp = "20241019002622Z"
Syntax: GeneralizedTime
Equality match: generalizedTimeMatch
Substring match: none
Ordering match: generalizedTimeOrderingMatch
krbPrincipalName = "host/workstation.od.freeipa.xyz@OD.FREEIPA.XYZ"
Syntax: IA5String
Equality match: caseExactIA5Match
Substring match: caseExactSubstringsMatch
Ordering match: none
The createTimestamp
attribute has ordering mathc generalizedTimeOrderingMatch
. This
means we can correctly compare it’s values with another date value with
<=
, >=
operators. Note: LDAP does not support strict less than (<
) and strict
greater than (>
) operators.
Important note: in FreeRADIUS, value of TLS-Client-Cert-Valid-Since
is written without
the centry information. This is OK for 389DS and it converts year 24
to 2024
.
In FreeRADIUS debug output, find lines your client provides in form like this:
...
(7) eap_tls: TLS-Client-Cert-Valid-Since := "241020164306Z"
(7) eap_tls: TLS-Client-Cert-Common-Name := "workstation.od.freeipa.xyz"
(7) eap_tls: TLS-Client-Cert-Subject-Alt-Name-Dns := "workstation.od.freeipa.xyz"
(7) eap_tls: TLS-Client-Cert-Subject-Alt-Name-Upn := "host/workstation.od.freeipa.xyz@OD.FREEIPA.XYZ"
...
and query LDAP like this:
ldapsearch -x \
-H 'ldaps://ds1.od.freeipa.xyz:636' \
-D 'uid=svc_radius_wifi,cn=sysaccounts,cn=etc,dc=od,dc=freeipa,dc=xyz' \
-b 'cn=computers,cn=accounts,dc=od,dc=freeipa,dc=xyz' \
-s one \
-LLL \
-W \
'(&(krbPrincipalName=host/workstation.od.freeipa.xyz@OD.FREEIPA.XYZ)(createTimestamp<=241020164306Z))' \
krbPrincipalName
LDAP, FreeRADIUS and EAP-TLS
Attributes TLS-Client-Cert-Valid-Since
and TLS-Client-Cert-Subject-Alt-Name-Upn
are
available in authenticate
(after calling eap_wifi
) and post-authenticate
sections,
but not in authorize
. This means that we cannot call ldap
module in authorize
.
But, according to
ldap docs,
ldap
module, when used in authenticate
section performs authentication by binding to
ldap with User-Password
attribute. I do not have user’s password, so LDAP is unavailable
in way usually used.
That is why I will use ldap_wifi.authorize
inside authenticate{Auth-Type eap_wifi{}} block
just after eap_wifi succeeds.
Create LDAP config
Edit /etc/freeradius/3.0/mods-available/ldap_wifi
.
# /etc/freeradius/3.0/mods-available/ldap_wifi
ldap ldap_wifi {
server = 'ldaps://ds1.od.freeipa.xyz:636'
identity = 'uid=svc_radius_wifi,cn=sysaccounts,cn=etc,dc=od,dc=freeipa,dc=xyz'
password = 'Ylq4PRFMELRtIz9mPFcG'
base_dn = 'dc=od,dc=freeipa,dc=xyz'
update {
&session-state:Tmp-String-0 := 'createTimestamp'
&session-state:Tmp-String-1 := 'krbPrincipalName'
}
user_dn = "${.:instance}-LDAP-UserDn"
user {
base_dn = "cn=computers,cn=accounts,${..base_dn}"
filter = "(&(objectClass=ipahost)(krbPrincipalName=%{request:TLS-Client-Cert-Subject-Alt-Name-Upn})(createTimestamp<=%{request:TLS-Client-Cert-Valid-Since}))"
scope = 'one'
}
options {
dereference = 'never'
chase_referrals = no
rebind = no
res_timeout = 30
srv_timelimit = 30
net_timeout = 3
idle = 60
probes = 3
interval = 3
ldap_debug = 0x0000
}
tls {
start_tls = no
ca_file = /etc/ipa/ca.crt
require_cert = 'hard'
tls_min_version = "1.2"
}
pool {
start = ${thread[pool].start_servers}
min = ${thread[pool].min_spare_servers}
max = ${thread[pool].max_servers}
spare = ${thread[pool].max_spare_servers}
uses = 0
retry_delay = 30
lifetime = 0
idle_timeout = 60
}
}
Add call of ldap_wifi.authorize after EAP authentication succeds:
authenticate {
Auth-Type eap_wifi {
eap_wifi {
fail = reject
invalid = reject
reject = reject
}
if (&request:TLS-Client-Cert-X509v3-Extended-Key-Usage-OID[*] != "1.3.6.1.5.5.7.3.14") {
update request {
&Module-Failure-Message += 'Rejected: No EAPoL EKU'
}
reject
}
ldap_wifi.authorize {
noop = reject
fail = reject
userlock = reject
notfound = reject
}
}
}
And that’s done.