Menu

Show posts

This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.

Show posts Menu

Messages - Moeni

#1
Hi everyone,

I wanted to share a small side project that might be useful for anyone running Keycloak as their identity provider who wants to use it in combination with a RADIUS server.

What is it?

keyrad is a standalone RADIUS server written in Go that authenticates users directly against Keycloak. It's not a wrapper around FreeRADIUS. It maps Keycloak roles, groups, and scopes to RADIUS attributes, including MikroTik VSAs (which was one of the original use cases).

Key benefit: OTP/TOTP support

A major advantage of keyrad is that it supports password + OTP/TOTP authentication together with Keycloak. The plugin exposes the OTP challenge message and the relevant authentication toggles in the GUI (PAP, challenge-response, Message-Authenticator). If you're already using Keycloak with TOTP for your users, this lets you bring that same second factor to RADIUS-authenticated services (WPA2-Enterprise WiFi, VPN gateways, hotspot logins) without standing up a separate OTP backend or maintaining a parallel user database.

The OPNsense part:

os-keyrad is an OPNsense MVC plugin that gives you a web GUI to manage keyrad. It handles general settings (Keycloak URLs, realm, client credentials, RADIUS listen address), a clients grid (FreeRADIUS-style shared secrets), an attribute mappings grid (roles/groups to RADIUS attributes), and service control (start/stop/restart/apply). The plugin renders the keyrad.yaml and clients.conf files via the standard template engine.

Important warnings to be upfront about:

  • This is a GUI-only plugin: The keyrad daemon binary is not included. You need to cross-compile it for FreeBSD/OPNsense and place it at /usr/local/bin/keyrad manually. Instructions are in the repo.
  • Testing status: The core keyrad project is at v2.0.x and is already used in production environments. The OPNsense GUI is newer still. I would classify this as "ready for curious testers" rather than production-ready. On my OPNsense(DEC740) it works as intendet.

Links:

OPNsense plugin (os-keyrad): https://github.com/gpappsoft/os-keyrad

Core RADIUS server (keyrad): https://github.com/gpappsoft/keyrad

Original MikroTik forum thread: link

Why post here?

I know the OPNsense community values transparency and proper packaging. I'm posting this to see if there's interest and to get feedback from anyone who might test it – particularly on the plugin's MVC structure, the template generation, or any FreeBSD-specific issues with the binary.


Happy to answer any questions, and thanks for looking.

Cheers,
Marco
#2
Hello,

I am not sure if this is related, since the ACME client (issuing/renewing) and the OPNCentral cert push (distribution) are really separate paths – but in case it helps: after the last two updates I had ACME client trouble too (different DNS provider), which I could clear with a ,,Reset ACME Client" + ,,Renew Certificate". Probably not related to the push mechanism you're describing, though.

Cheers,
Marco
#3
Wrote this up as a feature request: https://github.com/opnsense/core/issues/10435

Short version: an option to require membership in at least one configured group and deny the login otherwise, instead of constraint_groups only doing the mapping.

Sorry if the wording/description of the FR is a bit clumsy – all this ,,who's in which group and why" logic makes my head spin. I'm clearly better with firewalls than with group theory. 😉

Cheers,
Marco
#4
Yes, I agree that OPNsense should have an internal check as well. Something like "require membership in at least one of the configured groups, otherwise deny" would be much easier to set up than per-client flow overrides, especially when multiple teams need access. It looks like constraint_groups only changes how groups are mapped right now, not whether access is granted, so a real "deny if not a member" option would be nice to have.

Meanwhile, if you're using Keycloak, you can solve this server-side on the IdP: duplicate the browser flow, add a conditional sub-flow with a "Condition — user role" (negated) plus a "Deny Access" execution, and bind that flow to the OPNsense client only (Clients -> Advanced -> Authentication flow overrides). When you gate on a role and map your group to it, people who aren't in your group won't get a token at all, so OPNsense doesn't create them automatically. No auth proxy required.

Cheers,
Marco
#5
Quick follow-up: I had a bit of time this morning to dig into this and test it on a BE VM. Turns out the deep-link target is already available on the login page (OPNsense puts it in $_GET['url'], same as the password login uses), so it just needs to be carried through the OIDC flow and applied at the end instead of always landing on the dashboard. I got it working end-to-end on the VM.

I've opened a feature request with the details (validation helper, the open-core changes, and the two Business Edition controller inserts described by file/method): https://github.com/opnsense/core/issues/10433

Cheers,
Marco
#6
Thanks, will do. I'll open a feature request on GitHub. But I'm pretty busy right now, so it might take me a few days to get to it. I'll post the link here once it's filed.

Cheers,
Marco
#7
I suspect this isn't a config error on your side. I'm seeing the same behavior here.

As far as I can tell, the redirect_uri has to be the fixed .../api/oidc/rp/finalize/<appcode> callback, and finalize seems to just create the session and send you to the dashboard. My guess is the originally requested URL simply isn't carried through the round trip (it would need to be stashed in the OAuth state and restored at finalize), so it gets lost—whereas with password login that URL never leaves OPNsense, which would explain why it works there.

I also couldn't find any setting for it: the WebGui/Admin provider only exposes Application code, Service, Extensive log, and Description.

Since it's a Business Edition feature, I assume any fix would have to come from Deciso – probably worth a feature request to preserve the original REQUEST_URI (validated as a local /ui/... path so it can't become an open redirect).