make certificate configurable
Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
@ -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",
|
||||
]
|
||||
|
||||
|
||||
|
@ -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",
|
||||
),
|
||||
),
|
||||
]
|
@ -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"""
|
||||
|
@ -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": []
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ type State struct {
|
||||
HasStarted bool
|
||||
RemainingChunks [][]byte
|
||||
HandshakeDone bool
|
||||
FinalStatus protocol.Status
|
||||
ClientHello *tls.ClientHelloInfo
|
||||
MPPEKey []byte
|
||||
TotalPayloadSize int
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -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,
|
||||
|
16
schema.yml
16
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
|
||||
|
@ -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"
|
||||
></ak-text-input>
|
||||
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||
<ak-crypto-certificate-search
|
||||
certificate=${ifDefined(provider?.certificate ?? undefined)}
|
||||
singleton
|
||||
></ak-crypto-certificate-search>
|
||||
<p class="pf-c-form__helper-text">${msg("Certificate used for EAP-TLS.")}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${msg("Property mappings")}
|
||||
name="propertyMappings"
|
||||
|
Reference in New Issue
Block a user