From 3ada3a7e0ee28dbe7cb272c702969bf08e8ce358 Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Fri, 16 May 2025 15:59:10 +0200 Subject: [PATCH] make certificate configurable Signed-off-by: Jens Langhammer --- authentik/providers/radius/api/providers.py | 2 + .../0005_radiusprovider_certificate.py | 25 ++++++++++++ authentik/providers/radius/models.py | 8 ++++ blueprints/schema.json | 5 +++ internal/outpost/radius/api.go | 1 + internal/outpost/radius/eap/tls/buff_conn.go | 5 +-- internal/outpost/radius/eap/tls/payload.go | 20 +++++++++- internal/outpost/radius/eap/tls/settings.go | 2 +- internal/outpost/radius/eap/tls/state.go | 1 + .../outpost/radius/handle_access_request.go | 40 ++++++++++--------- internal/outpost/radius/radius.go | 15 ++++--- schema.yml | 16 ++++++++ .../radius/RadiusProviderFormForm.ts | 9 +++++ 13 files changed, 117 insertions(+), 32 deletions(-) create mode 100644 authentik/providers/radius/migrations/0005_radiusprovider_certificate.py diff --git a/authentik/providers/radius/api/providers.py b/authentik/providers/radius/api/providers.py index 0ab9d04a10..5cdcdd8ddf 100644 --- a/authentik/providers/radius/api/providers.py +++ b/authentik/providers/radius/api/providers.py @@ -44,6 +44,7 @@ class RadiusProviderSerializer(ProviderSerializer): "shared_secret", "outpost_set", "mfa_support", + "certificate", ] extra_kwargs = ProviderSerializer.Meta.extra_kwargs @@ -79,6 +80,7 @@ class RadiusOutpostConfigSerializer(ModelSerializer): "client_networks", "shared_secret", "mfa_support", + "certificate", ] diff --git a/authentik/providers/radius/migrations/0005_radiusprovider_certificate.py b/authentik/providers/radius/migrations/0005_radiusprovider_certificate.py new file mode 100644 index 0000000000..250c344b2b --- /dev/null +++ b/authentik/providers/radius/migrations/0005_radiusprovider_certificate.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.9 on 2025-05-16 13:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_crypto", "0004_alter_certificatekeypair_name"), + ("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"), + ] + + operations = [ + migrations.AddField( + model_name="radiusprovider", + name="certificate", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="authentik_crypto.certificatekeypair", + ), + ), + ] diff --git a/authentik/providers/radius/models.py b/authentik/providers/radius/models.py index 5557a2f732..4f936c3a53 100644 --- a/authentik/providers/radius/models.py +++ b/authentik/providers/radius/models.py @@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import Serializer from authentik.core.models import PropertyMapping, Provider +from authentik.crypto.models import CertificateKeyPair from authentik.lib.generators import generate_id from authentik.outposts.models import OutpostModel @@ -38,6 +39,13 @@ class RadiusProvider(OutpostModel, Provider): ), ) + certificate = models.ForeignKey( + CertificateKeyPair, + on_delete=models.CASCADE, + default=None, + null=True + ) + @property def launch_url(self) -> str | None: """Radius never has a launch URL""" diff --git a/blueprints/schema.json b/blueprints/schema.json index 9134f33f6e..7e96401cc8 100644 --- a/blueprints/schema.json +++ b/blueprints/schema.json @@ -8953,6 +8953,11 @@ "type": "boolean", "title": "MFA Support", "description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon." + }, + "certificate": { + "type": "string", + "format": "uuid", + "title": "Certificate" } }, "required": [] diff --git a/internal/outpost/radius/api.go b/internal/outpost/radius/api.go index 0f5672f486..c00ac4c3a3 100644 --- a/internal/outpost/radius/api.go +++ b/internal/outpost/radius/api.go @@ -51,6 +51,7 @@ func (rs *RadiusServer) Refresh() error { MFASupport: provider.GetMfaSupport(), appSlug: provider.ApplicationSlug, flowSlug: provider.AuthFlowSlug, + certId: provider.GetCertificate(), providerId: provider.Pk, s: rs, log: logger, diff --git a/internal/outpost/radius/eap/tls/buff_conn.go b/internal/outpost/radius/eap/tls/buff_conn.go index fe85f325f5..05d92a7f37 100644 --- a/internal/outpost/radius/eap/tls/buff_conn.go +++ b/internal/outpost/radius/eap/tls/buff_conn.go @@ -42,7 +42,7 @@ func NewBuffConn(initialData []byte, ctx context.Context) *BuffConn { var errStall = errors.New("Stall") func (conn BuffConn) OutboundData() []byte { - d, err := retry.DoWithData( + d, _ := retry.DoWithData( func() ([]byte, error) { b := conn.writer.Bytes() if len(b) < 1 { @@ -52,9 +52,6 @@ func (conn BuffConn) OutboundData() []byte { }, conn.retryOptions..., ) - if err != nil { - return []byte{} - } return d } diff --git a/internal/outpost/radius/eap/tls/payload.go b/internal/outpost/radius/eap/tls/payload.go index 21e145b941..49509969bd 100644 --- a/internal/outpost/radius/eap/tls/payload.go +++ b/internal/outpost/radius/eap/tls/payload.go @@ -8,6 +8,7 @@ import ( "slices" "time" + "github.com/avast/retry-go/v4" log "github.com/sirupsen/logrus" "goauthentik.io/internal/outpost/radius/eap/debug" "goauthentik.io/internal/outpost/radius/eap/protocol" @@ -104,7 +105,21 @@ func (p *Payload) Handle(ctx protocol.Context) protocol.Payload { } if p.st.Conn.writer.Len() == 0 && p.st.HandshakeDone { defer p.st.ContextCancel() - ctx.EndInnerProtocol(protocol.StatusSuccess, func(r *radius.Packet) *radius.Packet { + // If we don't have a final status from the handshake finished function, stall for time + pst, _ := retry.DoWithData( + func() (protocol.Status, error) { + if p.st.FinalStatus == protocol.StatusUnknown { + return p.st.FinalStatus, errStall + } + return p.st.FinalStatus, nil + }, + retry.Context(p.st.Context), + retry.Delay(10*time.Microsecond), + retry.DelayType(retry.BackOffDelay), + retry.MaxDelay(100*time.Millisecond), + retry.Attempts(0), + ) + ctx.EndInnerProtocol(pst, func(r *radius.Packet) *radius.Packet { microsoft.MSMPPERecvKey_Set(r, p.st.MPPEKey[:32]) microsoft.MSMPPESendKey_Set(r, p.st.MPPEKey[64:64+32]) return r @@ -129,6 +144,7 @@ func (p *Payload) tlsInit(ctx protocol.Context) { err := p.st.TLS.HandshakeContext(p.st.Context) if err != nil { ctx.Log().WithError(err).Debug("TLS: Handshake error") + p.st.FinalStatus = protocol.StatusError ctx.EndInnerProtocol(protocol.StatusError, func(p *radius.Packet) *radius.Packet { return p }) @@ -159,7 +175,7 @@ func (p *Payload) tlsHandshakeFinished(ctx protocol.Context) { ctx.Log().Debugf("TLS: ksm % x %v", ksm, err) p.st.MPPEKey = ksm p.st.HandshakeDone = true - ctx.ProtocolSettings().(Settings).HandshakeSuccessful(ctx, cs.PeerCertificates) + p.st.FinalStatus = ctx.ProtocolSettings().(Settings).HandshakeSuccessful(ctx, cs.PeerCertificates) } func (p *Payload) startChunkedTransfer(data []byte) *Payload { diff --git a/internal/outpost/radius/eap/tls/settings.go b/internal/outpost/radius/eap/tls/settings.go index ad108e00bd..a435129084 100644 --- a/internal/outpost/radius/eap/tls/settings.go +++ b/internal/outpost/radius/eap/tls/settings.go @@ -9,5 +9,5 @@ import ( type Settings struct { Config *tls.Config - HandshakeSuccessful func(ctx protocol.Context, certs []*x509.Certificate) + HandshakeSuccessful func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status } diff --git a/internal/outpost/radius/eap/tls/state.go b/internal/outpost/radius/eap/tls/state.go index e8caca93f9..628679cc52 100644 --- a/internal/outpost/radius/eap/tls/state.go +++ b/internal/outpost/radius/eap/tls/state.go @@ -11,6 +11,7 @@ type State struct { HasStarted bool RemainingChunks [][]byte HandshakeDone bool + FinalStatus protocol.Status ClientHello *tls.ClientHelloInfo MPPEKey []byte TotalPayloadSize int diff --git a/internal/outpost/radius/handle_access_request.go b/internal/outpost/radius/handle_access_request.go index 59730b4f6a..c9b2a0927c 100644 --- a/internal/outpost/radius/handle_access_request.go +++ b/internal/outpost/radius/handle_access_request.go @@ -128,13 +128,18 @@ func (pi *ProviderInstance) SetEAPState(key string, state *eap.State) { } func (pi *ProviderInstance) GetEAPSettings() eap.Settings { - // Testing - cert, err := ttls.LoadX509KeyPair( - "../t/ca/out/cert_jens-mbp.lab.beryju.org.pem", - "../t/ca/out/cert_jens-mbp.lab.beryju.org.key", - ) - if err != nil { - panic(err) + certId := pi.certId + if certId == "" { + return eap.Settings{ + ProtocolsToOffer: []protocol.Type{}, + } + } + + cert := pi.s.cryptoStore.Get(certId) + if cert == nil { + return eap.Settings{ + ProtocolsToOffer: []protocol.Type{}, + } } return eap.Settings{ @@ -142,19 +147,18 @@ func (pi *ProviderInstance) GetEAPSettings() eap.Settings { ProtocolSettings: map[protocol.Type]interface{}{ tls.TypeTLS: tls.Settings{ Config: &ttls.Config{ - Certificates: []ttls.Certificate{cert}, + Certificates: []ttls.Certificate{*cert}, ClientAuth: ttls.RequireAnyClientCert, }, - HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) { + HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status { + ctx.Log().Debug("Starting authn flow") pem := pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: certs[0].Raw, }) fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{ - // "username": username, - // "client": r.RemoteAddr(), - // "requestId": r.ID(), + "client": utils.GetIP(ctx.Packet().RemoteAddr), }) fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr)) fe.Params.Add("goauthentik.io/outpost/radius", "true") @@ -162,16 +166,14 @@ func (pi *ProviderInstance) GetEAPSettings() eap.Settings { passed, err := fe.Execute() if err != nil { - panic(err) + ctx.Log().WithError(err).Warning("failed to execute flow") + return protocol.StatusError } + ctx.Log().WithField("passed", passed).Debug("Finished flow") if passed { - ctx.EndInnerProtocol(protocol.StatusSuccess, func(p *radius.Packet) *radius.Packet { - return p - }) + return protocol.StatusSuccess } else { - ctx.EndInnerProtocol(protocol.StatusError, func(p *radius.Packet) *radius.Packet { - return p - }) + return protocol.StatusError } }, }, diff --git a/internal/outpost/radius/radius.go b/internal/outpost/radius/radius.go index 9a753b5886..9eef5696d5 100644 --- a/internal/outpost/radius/radius.go +++ b/internal/outpost/radius/radius.go @@ -23,24 +23,27 @@ type ProviderInstance struct { appSlug string flowSlug string providerId int32 + certId string s *RadiusServer log *log.Entry eapState map[string]*eap.State } type RadiusServer struct { - s radius.PacketServer - log *log.Entry - ac *ak.APIController + s radius.PacketServer + log *log.Entry + ac *ak.APIController + cryptoStore *ak.CryptoStore providers []*ProviderInstance } func NewServer(ac *ak.APIController) ak.Outpost { rs := &RadiusServer{ - log: log.WithField("logger", "authentik.outpost.radius"), - ac: ac, - providers: []*ProviderInstance{}, + log: log.WithField("logger", "authentik.outpost.radius"), + ac: ac, + providers: []*ProviderInstance{}, + cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi), } rs.s = radius.PacketServer{ Handler: rs, diff --git a/schema.yml b/schema.yml index aa49a98751..90fcf81ce0 100644 --- a/schema.yml +++ b/schema.yml @@ -54849,6 +54849,10 @@ components: should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon. + certificate: + type: string + format: uuid + nullable: true PatchedRedirectStageRequest: type: object description: RedirectStage Serializer @@ -57302,6 +57306,10 @@ components: should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon. + certificate: + type: string + format: uuid + nullable: true required: - application_slug - auth_flow_slug @@ -57388,6 +57396,10 @@ components: should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon. + certificate: + type: string + format: uuid + nullable: true required: - assigned_application_name - assigned_application_slug @@ -57512,6 +57524,10 @@ components: should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon. + certificate: + type: string + format: uuid + nullable: true required: - authorization_flow - invalidation_flow diff --git a/web/src/admin/providers/radius/RadiusProviderFormForm.ts b/web/src/admin/providers/radius/RadiusProviderFormForm.ts index a99ae46b96..614cff965d 100644 --- a/web/src/admin/providers/radius/RadiusProviderFormForm.ts +++ b/web/src/admin/providers/radius/RadiusProviderFormForm.ts @@ -1,3 +1,4 @@ +import "@goauthentik/admin/common/ak-crypto-certificate-search"; import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search"; import "@goauthentik/admin/common/ak-flow-search/ak-flow-search"; import { ascii_letters, digits, randomString } from "@goauthentik/common/utils"; @@ -93,6 +94,14 @@ export function renderForm( help=${clientNetworksHelp} input-hint="code" > + + + +

${msg("Certificate used for EAP-TLS.")}

+