Fix issues with radius (#1084)

This commit is contained in:
Firstyear 2022-10-02 11:28:58 +10:00 committed by GitHub
parent 2e9a94e703
commit aa9af0705c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 164 additions and 153 deletions

View file

@ -125,21 +125,22 @@ verify_hostnames = true # verify the hostname of the Kanidm server
verify_ca = false # Strict CA verification verify_ca = false # Strict CA verification
ca = /data/ca.pem # Path to the kanidm ca ca = /data/ca.pem # Path to the kanidm ca
username = # Username of the RADIUS service account
password = # Generated secret for the service account auth_token = "ABC..." # Auth token for the service account
# See: kanidm service-account api-token generate
# Default vlans for groups that don't specify one. # Default vlans for groups that don't specify one.
radius_default_vlan = 1 radius_default_vlan = 1
# A list of Kanidm groups which must be a member # A list of Kanidm groups which must be a member
# before they can authenticate via RADIUS. # before they can authenticate via RADIUS.
radius_required_groups = [ radius_required_groups = [
"radius_access_allowed", "radius_access_allowed@idm.example.com",
] ]
# A mapping between Kanidm groups and VLANS # A mapping between Kanidm groups and VLANS
radius_groups = [ radius_groups = [
{ name = "radius_access_allowed", vlan = 10 }, { spn = "radius_access_allowed@idm.example.com", vlan = 10 },
] ]
# A mapping of clients and their authentication tokens # A mapping of clients and their authentication tokens
@ -150,11 +151,11 @@ radius_clients = [
# radius_cert_path = "/etc/raddb/certs/cert.pem" # radius_cert_path = "/etc/raddb/certs/cert.pem"
# the signing key for radius TLS # the signing key for radius TLS
# radius_key_path = "/etc/raddb/certs/key.pem" # radius_key_path = "/etc/raddb/certs/key.pem"
# the diffie-hellman output # the diffie-hellman output
# radius_dh_path = "/etc/raddb/certs/dh.pem" # radius_dh_path = "/etc/raddb/certs/dh.pem"
# the CA certificate # the CA certificate
# radius_ca_path = "/etc/raddb/certs/ca.pem" # radius_ca_path = "/etc/raddb/certs/ca.pem"
``` ```
@ -164,21 +165,20 @@ radius_clients = [
```toml ```toml
url = "https://example.com" url = "https://example.com"
username = "radius_service_account" # The auth token for the service account
# The generated password from above auth_token = "ABC..."
password = "cr4bzr0ol"
# default vlan for groups that don't specify one. # default vlan for groups that don't specify one.
radius_default_vlan = 99 radius_default_vlan = 99
# if the user is in one of these Kanidm groups, # if the user is in one of these Kanidm groups,
# then they're allowed to authenticate # then they're allowed to authenticate
radius_required_groups = [ radius_required_groups = [
"radius_access_allowed", "radius_access_allowed@idm.example.com",
] ]
radius_groups = [ radius_groups = [
{ name = "radius_access_allowed", vlan = 10 } { spn = "radius_access_allowed@idm.example.com", vlan = 10 }
] ]
radius_clients = [ radius_clients = [
@ -196,11 +196,11 @@ radius_clients = [
] ]
``` ```
Then re-create/run your docker instance and expose the ports by adding Then re-create/run your docker instance and expose the ports by adding
`-p 1812:1812 -p 1812:1812/udp` to the command. `-p 1812:1812 -p 1812:1812/udp` to the command.
If you have any issues, check the logs from the RADIUS output, as they tend If you have any issues, check the logs from the RADIUS output, as they tend
to indicate the cause of the problem. To increase the logging level you can to indicate the cause of the problem. To increase the logging level you can
re-run your environment with debug enabled: re-run your environment with debug enabled:
```shell ```shell
@ -215,7 +215,7 @@ docker run --name radiusd \
``` ```
Note: the RADIUS container *is* configured to provide Note: the RADIUS container *is* configured to provide
[Tunnel-Private-Group-ID](https://freeradius.org/rfc/rfc2868.html#Tunnel-Private-Group-ID), [Tunnel-Private-Group-ID](https://freeradius.org/rfc/rfc2868.html#Tunnel-Private-Group-ID),
so if you wish to use Wi-Fi-assigned VLANs on your infrastructure, you can so if you wish to use Wi-Fi-assigned VLANs on your infrastructure, you can
assign these by groups in the configuration file as shown in the above examples. assign these by groups in the configuration file as shown in the above examples.

View file

@ -50,6 +50,7 @@ RUN rm -rf /pkg/*
USER radiusd USER radiusd
ENV LD_PRELOAD=/usr/lib64/libpython3.so ENV LD_PRELOAD=/usr/lib64/libpython3.so
ENV KANIDM_CONFIG_FILE="/data/kanidm"
COPY kanidm_rlm_python/radius_entrypoint.py /radius_entrypoint.py COPY kanidm_rlm_python/radius_entrypoint.py /radius_entrypoint.py
CMD [ "/usr/bin/python3", "/radius_entrypoint.py" ] CMD [ "/usr/bin/python3", "/radius_entrypoint.py" ]

View file

@ -155,7 +155,7 @@ impl QueryServerReadV1 {
#[instrument( #[instrument(
level = "info", level = "info",
name = "online_backup", name = "online_backup",
skip(self, msg, outpath, versions) skip_all,
fields(uuid = ?msg.eventid) fields(uuid = ?msg.eventid)
)] )]
pub async fn handle_online_backup( pub async fn handle_online_backup(

View file

@ -162,45 +162,45 @@ macro_rules! get_idl {
$itype:expr, $itype:expr,
$idx_key:expr $idx_key:expr
) => {{ ) => {{
// SEE ALSO #259: Find a way to implement borrow for this properly. // SEE ALSO #259: Find a way to implement borrow for this properly.
// I don't think this is possible. When we make this dyn, the arc // I don't think this is possible. When we make this dyn, the arc
// needs the dyn trait to be sized so that it *could* claim a clone // needs the dyn trait to be sized so that it *could* claim a clone
// for hit tracking reasons. That also means that we need From and // for hit tracking reasons. That also means that we need From and
// some other traits that just seem incompatible. And in the end, // some other traits that just seem incompatible. And in the end,
// we clone a few times in arc, and if we miss we need to insert anyway // we clone a few times in arc, and if we miss we need to insert anyway
// //
// So the best path could be to replace IdlCacheKey with a compressed // So the best path could be to replace IdlCacheKey with a compressed
// or smaller type. Perhaps even a small cache of the IdlCacheKeys that // or smaller type. Perhaps even a small cache of the IdlCacheKeys that
// are allocated to reduce some allocs? Probably over thinking it at // are allocated to reduce some allocs? Probably over thinking it at
// this point. // this point.
// //
// First attempt to get from this cache. // First attempt to get from this cache.
let cache_key = IdlCacheKeyRef { let cache_key = IdlCacheKeyRef {
a: $attr, a: $attr,
i: $itype, i: $itype,
k: $idx_key, k: $idx_key,
}; };
let cache_r = $self.idl_cache.get(&cache_key as &dyn IdlCacheKeyToRef); let cache_r = $self.idl_cache.get(&cache_key as &dyn IdlCacheKeyToRef);
// If hit, continue. // If hit, continue.
if let Some(ref data) = cache_r { if let Some(ref data) = cache_r {
trace!( trace!(
cached_index = ?$itype, cached_index = ?$itype,
attr = ?$attr, attr = ?$attr,
idl = %data, idl = %data,
); );
return Ok(Some(data.as_ref().clone())); return Ok(Some(data.as_ref().clone()));
} }
// If miss, get from db *and* insert to the cache. // If miss, get from db *and* insert to the cache.
let db_r = $self.db.get_idl($attr, $itype, $idx_key)?; let db_r = $self.db.get_idl($attr, $itype, $idx_key)?;
if let Some(ref idl) = db_r { if let Some(ref idl) = db_r {
let ncache_key = IdlCacheKey { let ncache_key = IdlCacheKey {
a: $attr.into(), a: $attr.into(),
i: $itype.clone(), i: $itype.clone(),
k: $idx_key.into(), k: $idx_key.into(),
}; };
$self.idl_cache.insert(ncache_key, Box::new(idl.clone())) $self.idl_cache.insert(ncache_key, Box::new(idl.clone()))
} }
Ok(db_r) Ok(db_r)
}}; }};
} }
@ -366,6 +366,7 @@ impl<'a> IdlArcSqliteTransaction for IdlArcSqliteReadTransaction<'a> {
exists_idx!(self, attr, itype) exists_idx!(self, attr, itype)
} }
#[instrument(level = "trace", skip_all)]
fn get_idl( fn get_idl(
&mut self, &mut self,
attr: &str, attr: &str,
@ -447,6 +448,7 @@ impl<'a> IdlArcSqliteTransaction for IdlArcSqliteWriteTransaction<'a> {
exists_idx!(self, attr, itype) exists_idx!(self, attr, itype)
} }
#[instrument(level = "trace", skip_all)]
fn get_idl( fn get_idl(
&mut self, &mut self,
attr: &str, attr: &str,

View file

@ -210,6 +210,7 @@ pub trait IdlSqliteTransaction {
self.exists_table(&tname) self.exists_table(&tname)
} }
#[instrument(level = "trace", skip_all)]
fn get_idl( fn get_idl(
&self, &self,
attr: &str, attr: &str,
@ -247,7 +248,6 @@ pub trait IdlSqliteTransaction {
// have a corrupted index ..... // have a corrupted index .....
None => IDLBitRange::new(), None => IDLBitRange::new(),
}; };
trace!( trace!(
miss_index = ?itype, miss_index = ?itype,
attr = ?attr, attr = ?attr,

View file

@ -1,5 +1,5 @@
""" kanidm RADIUS module """
""" kanidm RADIUS module """
import asyncio import asyncio
from functools import reduce from functools import reduce
import json import json
@ -9,68 +9,56 @@ from pathlib import Path
import sys import sys
from typing import Any, Dict, Optional, Union from typing import Any, Dict, Optional, Union
import aiohttp
from kanidm import KanidmClient
from kanidm.types import AuthStepPasswordResponse, RadiusTokenGroup, RadiusTokenResponse
from kanidm.utils import load_config
from kanidm.exceptions import NoMatchingEntries from kanidm.exceptions import NoMatchingEntries
from kanidm.types import AuthStepPasswordResponse, RadiusTokenResponse
from .. import KanidmClient
from . import radiusd from . import radiusd
from .utils import check_vlan
logging.basicConfig(
level=logging.DEBUG,
stream=sys.stderr,
)
# the list of places to try # the list of places to try
config_paths = [ CONFIG_PATHS = [
os.getenv("KANIDM_RLM_CONFIG", "/data/kanidm"), # container goodness os.getenv("KANIDM_RLM_CONFIG", "/data/kanidm"), # container goodness
"~/.config/kanidm", # for a user "~/.config/kanidm", # for a user
"/etc/kanidm/kanidm", # system-wide "/etc/kanidm/kanidm", # system-wide
"../examples/kanidm", # test mode "../examples/kanidm", # test mode
] ]
CONFIG_PATH = None
for config_file_path in config_paths:
CONFIG_PATH = Path(config_file_path).expanduser().resolve()
if CONFIG_PATH.exists():
break
if (CONFIG_PATH is None) or (not CONFIG_PATH.exists()): def instantiate(_: Any) -> Any:
logging.error( """start up radiusd"""
"Failed to find configuration file, checked (%s), quitting!", config_paths logging.basicConfig(
level=logging.DEBUG,
stream=sys.stderr,
) )
sys.exit(1) logging.info("Starting up!")
config = load_config(str(CONFIG_PATH))
KANIDM_CLIENT = KanidmClient(config_file=CONFIG_PATH)
if KANIDM_CLIENT.config.auth_token is None:
logging.error("You need to specify auth_token in the configuration file!")
sys.exit(1)
def authenticate( config_path = None
acct: str, for config_file_path in CONFIG_PATHS:
password: str, config_path = Path(config_file_path).expanduser().resolve()
kanidm_client: KanidmClient = KANIDM_CLIENT, if config_path.exists():
) -> Union[int, AuthStepPasswordResponse]: break
"""authenticate the RADIUS service account to Kanidm"""
logging.error("authenticate - %s:%s", acct, password)
try: if (config_path is None) or (not config_path.exists()):
loop = asyncio.get_event_loop() logging.error(
return loop.run_until_complete(kanidm_client.check_token_valid()) "Failed to find configuration file, checked (%s), quitting!", CONFIG_PATHS
except Exception as error_message: # pylint: disable=broad-except )
logging.error("Failed to run kanidm.check_token_valid: %s", error_message) sys.exit(1)
return radiusd.RLM_MODULE_FAIL
kanidm_client = KanidmClient(config_file=config_path)
if kanidm_client.config.auth_token is None:
logging.error("You need to specify auth_token in the configuration file!")
sys.exit(1)
os.environ["KANIDM_CONFIG_FILE"] = config_path.as_posix()
logging.info("Config file: %s", config_path.as_posix())
return radiusd.RLM_MODULE_OK
async def _get_radius_token( async def _get_radius_token(
username: Optional[str] = None, username: Optional[str] = None,
kanidm_client: KanidmClient = KANIDM_CLIENT,
) -> Optional[Dict[str, Any]]: ) -> Optional[Dict[str, Any]]:
"""pulls the radius token for a client username""" """pulls the radius token for a client username"""
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
if username is None: if username is None:
raise ValueError("Didn't get a username for _get_radius_token") raise ValueError("Didn't get a username for _get_radius_token")
# authenticate as the radius service account # authenticate as the radius service account
@ -86,45 +74,13 @@ async def _get_radius_token(
logging.debug(response.data) logging.debug(response.data)
return response.data return response.data
def check_vlan(
acc: int,
group: RadiusTokenGroup,
kanidm_client: Optional[KanidmClient] = None,
) -> int:
"""checks if a vlan is in the config,
acc is the default vlan
"""
logging.debug("acc=%s", acc)
if kanidm_client is None:
kanidm_client = KANIDM_CLIENT
# raise ValueError("Need to pass this a kanidm_client")
for radius_group in kanidm_client.config.radius_groups:
group_name = group.spn.split("@")[0]
logging.debug(
"Checking '%s' radius_group against group %s", radius_group, group_name
)
if radius_group.name == group_name:
return radius_group.vlan
logging.debug("returning default vlan: %s", acc)
return acc
def instantiate(_: Any) -> Any:
"""start up radiusd"""
logging.info("Starting up!")
return radiusd.RLM_MODULE_OK
# pylint: disable=too-many-locals # pylint: disable=too-many-locals
def authorize( def authorize(
args: Any = Dict[Any, Any], args: Any = Dict[Any, Any],
kanidm_client: KanidmClient = KANIDM_CLIENT,
) -> Any: ) -> Any:
"""does the kanidm authorize step""" """does the kanidm authorize step"""
logging.info("kanidm python module called") logging.info("kanidm python module called")
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
# args comes in like this # args comes in like this
# ( # (
# ('User-Name', '<username>'), # ('User-Name', '<username>'),
@ -153,7 +109,7 @@ def authorize(
tok = RadiusTokenResponse.parse_obj( tok = RadiusTokenResponse.parse_obj(
loop.run_until_complete(_get_radius_token(username=user_id)) loop.run_until_complete(_get_radius_token(username=user_id))
) )
logging.debug("radius_token: %s", tok) logging.debug("radius information 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_id %s: %s", "kanidm RLM_MODULE_NOTFOUND after NoMatchingEntries for user_id %s: %s",
@ -164,8 +120,8 @@ def authorize(
except Exception as error_message: # pylint: disable=broad-except except Exception as error_message: # pylint: disable=broad-except
logging.error("kanidm exception: %s, %s", type(error_message), error_message) logging.error("kanidm exception: %s, %s", type(error_message), error_message)
if tok is None: if tok is None:
logging.info("kanidm RLM_MODULE_NOTFOUND due to no auth token") logging.info("kanidm RLM_MODULE_REJECT - unable to retrieve radius information token")
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_REJECT
# Get values out of the token # Get values out of the token
name = tok.name name = tok.name
@ -174,14 +130,14 @@ def authorize(
# Are they in the required group? # Are they in the required group?
req_sat = False req_sat = False
required_groups = kanidm_client.config.radius_required_groups
for group in tok.groups: for group in tok.groups:
group_name = group.spn.split("@")[0] if group.uuid in required_groups or group.spn in 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)", name, group_name) logging.info("User %s has a required group (%s)", name, group.spn)
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.", name) logging.info("User %s doesn't have a group from the required list.", name)
return radiusd.RLM_MODULE_NOTFOUND return radiusd.RLM_MODULE_REJECT
# look up them in config for group vlan if possible. # look up them in config for group vlan if possible.
# TODO: work out the typing on this, WTF. # TODO: work out the typing on this, WTF.
@ -206,3 +162,21 @@ def authorize(
logging.info("OK! Returning details to radius for %s ...", name) 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)
def authenticate(
acct: str,
password: str,
) -> Union[int, AuthStepPasswordResponse]:
"""authenticate the RADIUS service account to Kanidm"""
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
logging.error("authenticate - %s:%s", acct, password)
try:
loop = asyncio.get_event_loop()
return loop.run_until_complete(kanidm_client.check_token_valid())
except Exception as error_message: # pylint: disable=broad-except
logging.error("Failed to run kanidm.check_token_valid: %s", error_message)
return radiusd.RLM_MODULE_FAIL

View file

@ -0,0 +1,34 @@
""" class utils """
from typing import Optional
import logging
import os
from .. import KanidmClient
from ..types import RadiusTokenGroup
def check_vlan(
acc: int,
group: RadiusTokenGroup,
kanidm_client: Optional[KanidmClient] = None,
) -> int:
"""checks if a vlan is in the config,
acc is the default vlan
"""
logging.debug("acc=%s", acc)
if kanidm_client is None:
if "KANIDM_CONFIG_FILE" in os.environ:
kanidm_client = KanidmClient(config_file=os.environ["KANIDM_CONFIG_FILE"])
else:
raise ValueError("Need to pass this a kanidm_client")
for radius_group in kanidm_client.config.radius_groups:
logging.debug(
"Checking vlan group '%s' against user group %s", radius_group.spn, group.spn
)
if radius_group.spn == group.spn:
logging.info("returning new vlan: %s", radius_group.vlan)
return radius_group.vlan
logging.debug("returning already set vlan: %s", acc)
return acc

View file

@ -93,7 +93,7 @@ class AuthStepPasswordResponse(BaseModel):
class RadiusGroup(BaseModel): class RadiusGroup(BaseModel):
"""group for kanidm radius""" """group for kanidm radius"""
name: str spn: str
vlan: int vlan: int
@validator("vlan") @validator("vlan")

View file

@ -7,7 +7,7 @@ import pytest
from kanidm import KanidmClient from kanidm import KanidmClient
from kanidm.types import KanidmClientConfig, RadiusTokenGroup from kanidm.types import KanidmClientConfig, RadiusTokenGroup
from kanidm.radius import check_vlan from kanidm.radius.utils import check_vlan
@pytest.mark.asyncio @pytest.mark.asyncio
@ -18,8 +18,8 @@ async def test_check_vlan(event_loop: Any) -> None:
""" """
uri='https://kanidm.example.com' uri='https://kanidm.example.com'
radius_groups = [ radius_groups = [
{ name = "crabz", "vlan" = 1234 }, { spn = "crabz@example.com", "vlan" = 1234 },
{ name = "hello world", "vlan" = 12345 }, { spn = "hello@world", "vlan" = 12345 },
] ]
""" """
) )
@ -34,7 +34,7 @@ async def test_check_vlan(event_loop: Any) -> None:
assert ( assert (
check_vlan( check_vlan(
acc=12345678, acc=12345678,
group=RadiusTokenGroup(spn="crabz@domain.com", uuid="crabz"), group=RadiusTokenGroup(spn="crabz@example.com", uuid="crabz"),
kanidm_client=kanidm_client, kanidm_client=kanidm_client,
) )
== 1234 == 1234

View file

@ -10,7 +10,7 @@ from kanidm.types import KanidmClientConfig
from kanidm.utils import load_config from kanidm.utils import load_config
EXAMPLE_CONFIG_FILE = "../../kanidm_rlm_python/examples/config" EXAMPLE_CONFIG_FILE = "../examples/config"
def test_load_config_file() -> None: def test_load_config_file() -> None:
@ -30,7 +30,7 @@ def test_radius_groups() -> None:
config_toml = """ config_toml = """
radius_groups = [ radius_groups = [
{ name = "hello world", "vlan" = 1234 }, { spn = "hello world", "vlan" = 1234 },
] ]
""" """
@ -38,8 +38,8 @@ radius_groups = [
print(config_parsed) print(config_parsed)
kanidm_config = KanidmClientConfig.parse_obj(config_parsed) kanidm_config = KanidmClientConfig.parse_obj(config_parsed)
for group in kanidm_config.radius_groups: for group in kanidm_config.radius_groups:
print(group.name) print(group.spn)
assert group.name == "hello world" assert group.spn == "hello world"
def test_radius_clients() -> None: def test_radius_clients() -> None:

View file

@ -22,7 +22,7 @@ async def test_radius_call(client_configfile: KanidmClient) -> None:
print("Doing auth_init using token") print("Doing auth_init using token")
if client_configfile.config.auth_token is None: if client_configfile.config.auth_token is None:
raise ValueError("This path shouldn't be possible in the test!") pytest.skip("You can't test auth if you don't have an auth_token in ~/.config/kanidm")
result = await client_configfile.get_radius_token(RADIUS_TEST_USER) result = await client_configfile.get_radius_token(RADIUS_TEST_USER)
print(f"{result=}") print(f"{result=}")

View file

@ -33,13 +33,13 @@ def test_radiusgroup_vlan_zero() -> None:
def test_radiusgroup_vlan_4096() -> None: def test_radiusgroup_vlan_4096() -> None:
"""tests RadiusGroup's vlan validator""" """tests RadiusGroup's vlan validator"""
assert RadiusGroup(vlan=4096, name="crabzrool") assert RadiusGroup(vlan=4096, spn="crabzrool@foo")
def test_radiusgroup_vlan_no_name() -> None: def test_radiusgroup_vlan_no_name() -> None:
"""tests RadiusGroup's vlan validator""" """tests RadiusGroup's vlan validator"""
with pytest.raises( with pytest.raises(
pydantic.error_wrappers.ValidationError, match="name\n.*field required" pydantic.error_wrappers.ValidationError, match="spn\n.*field required"
): ):
RadiusGroup( RadiusGroup(
vlan=4096, vlan=4096,