Improve radius to support eap-tls with ca-dir (#957)

* Improve radius to support eap-tls with ca-dir, and also mschap
This commit is contained in:
Firstyear 2022-08-01 18:55:44 +10:00 committed by GitHub
parent 845cabb206
commit 7f7e882f24
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 26 deletions

View file

@ -2,11 +2,10 @@ FROM opensuse/tumbleweed:latest
EXPOSE 1812 1813 EXPOSE 1812 1813
# TODO: remove this once the freeradius python fix has been rolled into tumbleweed main # These all need to be on one line else the rpm cache ends
RUN zypper ar -f obs://home:firstyear:branches:network home:firstyear:branches:network # up in the layers.
RUN zypper refresh --force && \
RUN zypper --gpg-auto-import-keys refresh --force zypper install -y \
RUN zypper install -y \
freeradius-client \ freeradius-client \
freeradius-server \ freeradius-server \
freeradius-server-python3 \ freeradius-server-python3 \
@ -18,8 +17,9 @@ RUN zypper install -y \
timezone \ timezone \
iproute2 \ iproute2 \
iputils \ iputils \
curl openssl \
RUN zypper clean curl && \
zypper clean
ADD kanidm_rlm_python/mods-available/ /etc/raddb/mods-available/ ADD kanidm_rlm_python/mods-available/ /etc/raddb/mods-available/
COPY kanidm_rlm_python/sites-available/ /etc/raddb/sites-available/ COPY kanidm_rlm_python/sites-available/ /etc/raddb/sites-available/
@ -29,6 +29,7 @@ WORKDIR /etc/raddb
# Enable the python and cache module. # Enable the python and cache module.
RUN ln -s /etc/raddb/mods-available/python3 /etc/raddb/mods-enabled/python3 RUN ln -s /etc/raddb/mods-available/python3 /etc/raddb/mods-enabled/python3
RUN ln -s /etc/raddb/sites-available/check-eap-tls /etc/raddb/sites-enabled/check-eap-tls
# disable auth via methods we don't support! # disable auth via methods we don't support!
RUN rm /etc/raddb/mods-available/sql RUN rm /etc/raddb/mods-available/sql
@ -54,6 +55,7 @@ RUN python3 -m pip install --no-cache-dir --no-warn-script-location /pkg/kanidmr
RUN rm -rf /pkg/* RUN rm -rf /pkg/*
USER radiusd USER radiusd
ENV LD_PRELOAD=/usr/lib64/libpython3.so
COPY kanidm_rlm_python/entrypoint.py /entrypoint.py COPY kanidm_rlm_python/entrypoint.py /entrypoint.py
CMD [ "/usr/bin/python3", "/entrypoint.py" ] CMD [ "/usr/bin/python3", "/entrypoint.py" ]

View file

@ -21,6 +21,7 @@ CONFIG_FILE_PATH = "/data/kanidm"
CERT_SERVER_DEST = "/etc/raddb/certs/server.pem" CERT_SERVER_DEST = "/etc/raddb/certs/server.pem"
CERT_CA_DEST = "/etc/raddb/certs/ca.pem" CERT_CA_DEST = "/etc/raddb/certs/ca.pem"
CERT_CA_DIR = "/etc/raddb/certs/"
CERT_DH_DEST = "/etc/raddb/certs/dh.pem" CERT_DH_DEST = "/etc/raddb/certs/dh.pem"
# pylint: disable=unused-argument # pylint: disable=unused-argument
@ -60,6 +61,20 @@ def setup_certs(
print(f"Copying {cert_ca} to {CERT_CA_DEST}") print(f"Copying {cert_ca} to {CERT_CA_DEST}")
shutil.copyfile(cert_ca, CERT_CA_DEST) shutil.copyfile(cert_ca, CERT_CA_DEST)
# This dir can also contain crls!
if kanidm_config_object.radius_ca_dir:
cert_ca_dir = Path(kanidm_config_object.radius_ca_dir).expanduser().resolve()
if not cert_ca_dir.exists():
print(f"Failed to find radiusd ca dir ({cert_ca_dir}), quitting!", file=sys.stderr)
sys.exit(1)
if cert_ca_dir != CERT_CA_DIR:
print(f"Copying {cert_ca_dir} to {CERT_CA_DIR}")
shutil.copytree(cert_ca_dir, CERT_CA_DIR, dirs_exist_ok=True)
# Setup the ca-dir correctly now. We do this before we add server.pem so that it's
# not hashed as a ca.
subprocess.check_call(["openssl", "rehash", CERT_CA_DIR])
# let's put some dhparams in place # let's put some dhparams in place
if kanidm_config_object.radius_dh_path is not None: if kanidm_config_object.radius_dh_path is not None:
cert_dh = Path(kanidm_config_object.radius_dh_path).expanduser().resolve() cert_dh = Path(kanidm_config_object.radius_dh_path).expanduser().resolve()

View file

@ -138,17 +138,25 @@ def authorize(
dargs = dict(args) dargs = dict(args)
logging.error("Authorise: %s", json.dumps(dargs)) logging.error("Authorise: %s", json.dumps(dargs))
cn_uuid = dargs.get('TLS-Client-Cert-Common-Name', None)
username = dargs['User-Name'] username = dargs['User-Name']
if cn_uuid is not None:
logging.debug("Using TLS-Client-Cert-Common-Name")
user_id = cn_uuid
else:
logging.debug("Using User-Name")
user_id = username
tok = None tok = None
try: try:
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
tok = loop.run_until_complete(_get_radius_token(username=username)) tok = loop.run_until_complete(_get_radius_token(username=user_id))
logging.debug("radius_token: %s", tok) logging.debug("radius_token: %s", tok)
except NoMatchingEntries as error_message: except NoMatchingEntries as error_message:
logging.info( logging.info(
'kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user %s: %s', 'kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user_id %s: %s',
username, user_id,
error_message, error_message,
) )
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_NOTFOUND
@ -158,14 +166,19 @@ def authorize(
logging.info('kanidm RLM_MODULE_NOTFOUND due to no auth token') logging.info('kanidm RLM_MODULE_NOTFOUND due to no auth token')
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_NOTFOUND
# Get values out of the token
name = tok["name"]
secret = tok["secret"]
uuid = tok["uuid"]
# Are they in the required group? # Are they in the required group?
req_sat = False req_sat = False
for group in tok["groups"]: for group in tok["groups"]:
if group['name'] in kanidm_client.config.radius_required_groups: if group['name'] in kanidm_client.config.radius_required_groups:
req_sat = True req_sat = True
logging.info("User %s has a required group (%s)", username, group['name']) logging.info("User %s has a required group (%s)", name, group['name'])
if req_sat is not True: if req_sat is not True:
logging.info("User %s doesn't have a group from the required list.", username) logging.info("User %s doesn't have a group from the required list.", name)
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_NOTFOUND
# look up them in config for group vlan if possible. # look up them in config for group vlan if possible.
@ -179,14 +192,11 @@ def authorize(
logging.info("Invalid uservlan of 0") logging.info("Invalid uservlan of 0")
logging.info("selected vlan %s:%s", username, uservlan) logging.info("selected vlan %s:%s", name, uservlan)
# Convert the tok groups to groups.
name = tok["name"]
secret = tok["secret"]
reply = ( reply = (
('User-Name', str(name)), ('User-Name', str(name)),
('Reply-Message', 'Welcome'), ('Reply-Message', f"Kanidm-Uuid: {uuid}"),
('Tunnel-Type', '13'), ('Tunnel-Type', '13'),
('Tunnel-Medium-Type', '6'), ('Tunnel-Medium-Type', '6'),
('Tunnel-Private-Group-ID', str(uservlan)), ('Tunnel-Private-Group-ID', str(uservlan)),
@ -195,5 +205,5 @@ def authorize(
('Cleartext-Password', str(secret)), ('Cleartext-Password', str(secret)),
) )
logging.info("OK! Returning details to radius for %s ...", username) logging.info("OK! Returning details to radius for %s ...", name)
return (radiusd.RLM_MODULE_OK, reply, config_object) return (radiusd.RLM_MODULE_OK, reply, config_object)

View file

@ -195,7 +195,7 @@ eap {
# In that case, this CA file should contain # In that case, this CA file should contain
# *one* CA certificate. # *one* CA certificate.
# #
ca_file = ${cadir}/ca.pem # ca_file = ${cadir}/ca.pem
# OpenSSL will automatically create certificate chains, # OpenSSL will automatically create certificate chains,
# unless we tell it to not do that. The problem is that # unless we tell it to not do that. The problem is that
@ -287,7 +287,7 @@ eap {
# Accept an expired Certificate Revocation List # Accept an expired Certificate Revocation List
# #
# allow_expired_crl = no allow_expired_crl = no
# #
# If check_cert_issuer is set, the value will # If check_cert_issuer is set, the value will
@ -456,7 +456,7 @@ eap {
# #
# This feature REQUIRES "name" option be set above. # This feature REQUIRES "name" option be set above.
# #
#persist_dir = "${logdir}/tlscache" persist_dir = "${logdir}/tlscache"
} }
# #
@ -595,7 +595,7 @@ eap {
# virtual server has access to these attributes, and can # virtual server has access to these attributes, and can
# be used to accept or reject the request. # be used to accept or reject the request.
# #
# virtual_server = check-eap-tls virtual_server = check-eap-tls
} }

View file

@ -0,0 +1,134 @@
# This virtual server allows EAP-TLS to reject access requests
# based on some attributes of the certificates involved.
#
# To use this virtual server, you must enable it in the tls
# section of mods-enabled/eap as well as adding a link to this
# file in sites-enabled/.
#
#
# Value-pairs that are available for checking include:
#
# TLS-Client-Cert-Subject
# TLS-Client-Cert-Issuer
# TLS-Client-Cert-Common-Name
# TLS-Client-Cert-Subject-Alt-Name-Email
#
# To see a full list of attributes, run the server in debug mode
# with this virtual server configured, and look at the attributes
# passed in to this virtual server.
#
#
# This virtual server is also useful when using EAP-TLS as it is
# only called once, just before the final Accept is about to be
# returned from eap, whereas the outer authorize section is called
# multiple times for each challenge / response. For this reason,
# here may be a good location to put authentication logging, and
# modules that check for further authorization, especially if they
# hit external services such as sql or ldap.
server check-eap-tls {
# Authorize - this is the only section required.
#
# To accept the access request, set Auth-Type = Accept, otherwise
# set it to Reject.
authorize {
#
# By default, we just accept the request:
#
update config {
&Auth-Type := Accept
}
#
# Check the client certificate matches a string, and reject otherwise
#
# if ("%{TLS-Client-Cert-Common-Name}" == 'client.example.com') {
# update config {
# &Auth-Type := Accept
# }
# }
# else {
# update config {
# &Auth-Type := Reject
# }
# update reply {
# &Reply-Message := "Your certificate is not valid."
# }
# }
#
# Check the client certificate common name against the supplied User-Name
#
# if (&User-Name == "host/%{TLS-Client-Cert-Common-Name}") {
# update config {
# &Auth-Type := Accept
# }
# }
# else {
# update config {
# &Auth-Type := Reject
# }
# }
#
# This is a convenient place to call LDAP, for example, when using
# EAP-TLS, as it will only be called once, after all certificates as
# part of the EAP-TLS challenge process have been verified.
#
# An example could be to use LDAP to check that the connecting host, as
# well as presenting a valid certificate, is also in a group based on
# the User-Name (assuming this contains the service principal name).
# Settings such as the following could be used in the ldap module
# configuration:
#
# basedn = "dc=example, dc=com"
# filter = "(servicePrincipalName=%{User-Name})"
# base_filter = "(objectClass=computer)"
# groupname_attribute = cn
# groupmembership_filter = "(&(objectClass=group)(member=%{control:Ldap-UserDn}))"
# ldap
# Now let's test membership of an LDAP group (the ldap bind user will
# need permission to read this group membership):
# if (!(Ldap-Group == "Permitted-Laptops")) {
# update config {
# &Auth-Type := Reject
# }
# }
# or, to be more specific, you could use the group's full DN:
# if (!(Ldap-Group == "CN=Permitted-Laptops,OU=Groups,DC=example,DC=org")) {
python3
#
# This may be a better place to call the files modules when using
# EAP-TLS, as it will only be called once, after the challenge-response
# iteration has completed.
#
# files
#
# Log all request attributes, plus TLS certificate details, to the
# auth_log file. Again, this is just once per connection request, so
# may be preferable than in the outer authorize section. It is
# suggested that 'auth_log' also be in the outer post-auth and
# Post-Auth REJECT sections to log reply packet details, too.
#
auth_log
}
}

View file

@ -143,10 +143,11 @@ class KanidmClientConfig(BaseModel):
username: Optional[str] = None username: Optional[str] = None
password: Optional[str] = None password: Optional[str] = None
radius_cert_path: str = "/etc/raddb/certs/cert.pem" radius_cert_path: str = "/data/cert.pem"
radius_key_path: str = "/etc/raddb/certs/key.pem" # the signing key for radius TLS radius_key_path: str = "/data/key.pem" # the signing key for radius TLS
radius_dh_path: str = "/etc/raddb/certs/dh.pem" # the diffie-hellman output radius_dh_path: str = "/data/dh.pem" # the diffie-hellman output
radius_ca_path: str = "/etc/raddb/certs/ca.pem" # the diffie-hellman output radius_ca_path: Optional[str] = None
radius_ca_dir: Optional[str] = None
radius_required_groups: List[str] = [] radius_required_groups: List[str] = []
radius_default_vlan: int = 1 radius_default_vlan: int = 1