enterprise/stages: Add MTLS stage (#14296)

* prepare client auth with inbuilt server

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* introduce better IPC auth

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* init

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* start stage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* only allow trusted proxies to set MTLS headers

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* more stage progress

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* dont fail if ipc_key doesn't exist

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* actually install app

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add some tests

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* update API

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix unquote

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix int serial number not jsonable

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* init ui

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add UI

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* unrelated: fix git pull in makefile

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix parse helper

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add test for outpost

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* more tests and improvements

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* improve labels

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add support for multiple CAs on brand

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* add support for multiple CAs to MTLS stage

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* dont log ipcuser secret views

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

* fix go mod

Signed-off-by: Jens Langhammer <jens@goauthentik.io>

---------

Signed-off-by: Jens Langhammer <jens@goauthentik.io>
This commit is contained in:
Jens L.
2025-05-19 22:48:17 +02:00
committed by GitHub
parent b361dd3b59
commit 65517f3b7f
44 changed files with 1950 additions and 96 deletions

View File

@ -1,9 +1,12 @@
import { certificateProvider, certificateSelector } from "@goauthentik/admin/brands/Certificates";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { DefaultBrand } from "@goauthentik/common/ui/config";
import "@goauthentik/elements/CodeMirror";
import { CodeMirrorMode } from "@goauthentik/elements/CodeMirror";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import { ModelForm } from "@goauthentik/elements/forms/ModelForm";
@ -303,6 +306,17 @@ export class BrandForm extends ModelForm<Brand, string> {
.certificate=${this.instance?.webCertificate}
></ak-crypto-certificate-search>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Client Certificates")}
name="clientCertificates"
>
<ak-dual-select-dynamic-selected
.provider=${certificateProvider}
.selector=${certificateSelector(this.instance?.clientCertificates)}
available-label=${msg("Available Certificates")}
selected-label=${msg("Selected Certificates")}
></ak-dual-select-dynamic-selected>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${msg("Attributes")} name="attributes">
<ak-codemirror
mode=${CodeMirrorMode.YAML}

View File

@ -0,0 +1,39 @@
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import { CertificateKeyPair, CryptoApi } from "@goauthentik/api";
const certToSelect = (s: CertificateKeyPair) => [s.pk, s.name, s.name, s];
export async function certificateProvider(page = 1, search = "") {
const certificates = await new CryptoApi(DEFAULT_CONFIG).cryptoCertificatekeypairsList({
ordering: "name",
pageSize: 20,
search: search.trim(),
page,
hasKey: undefined,
});
return {
pagination: certificates.pagination,
options: certificates.results.map(certToSelect),
};
}
export function certificateSelector(instanceMappings?: string[]) {
if (!instanceMappings) {
return [];
}
return async () => {
const pm = new CryptoApi(DEFAULT_CONFIG);
const mappings = await Promise.allSettled(
instanceMappings.map((instanceId) =>
pm.cryptoCertificatekeypairsRetrieve({ kpUuid: instanceId }),
),
);
return mappings
.filter((s) => s.status === "fulfilled")
.map((s) => s.value)
.map(certToSelect);
};
}

View File

@ -16,6 +16,7 @@ import "@goauthentik/admin/stages/dummy/DummyStageForm";
import "@goauthentik/admin/stages/email/EmailStageForm";
import "@goauthentik/admin/stages/identification/IdentificationStageForm";
import "@goauthentik/admin/stages/invitation/InvitationStageForm";
import "@goauthentik/admin/stages/mtls/MTLSStageForm";
import "@goauthentik/admin/stages/password/PasswordStageForm";
import "@goauthentik/admin/stages/prompt/PromptStageForm";
import "@goauthentik/admin/stages/redirect/RedirectStageForm";

View File

@ -14,6 +14,7 @@ import "@goauthentik/admin/stages/dummy/DummyStageForm";
import "@goauthentik/admin/stages/email/EmailStageForm";
import "@goauthentik/admin/stages/identification/IdentificationStageForm";
import "@goauthentik/admin/stages/invitation/InvitationStageForm";
import "@goauthentik/admin/stages/mtls/MTLSStageForm";
import "@goauthentik/admin/stages/password/PasswordStageForm";
import "@goauthentik/admin/stages/prompt/PromptStageForm";
import "@goauthentik/admin/stages/redirect/RedirectStageForm";

View File

@ -0,0 +1,162 @@
import { certificateProvider, certificateSelector } from "@goauthentik/admin/brands/Certificates";
import "@goauthentik/admin/common/ak-crypto-certificate-search";
import { BaseStageForm } from "@goauthentik/admin/stages/BaseStageForm";
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-dynamic-selected-provider.js";
import "@goauthentik/elements/ak-dual-select/ak-dual-select-provider.js";
import "@goauthentik/elements/forms/FormGroup";
import "@goauthentik/elements/forms/HorizontalFormElement";
import "@goauthentik/elements/forms/Radio";
import { msg } from "@lit/localize";
import { TemplateResult, html } from "lit";
import { customElement } from "lit/decorators.js";
import { ifDefined } from "lit/directives/if-defined.js";
import {
CertAttributeEnum,
MutualTLSStage,
MutualTLSStageModeEnum,
StagesApi,
UserAttributeEnum,
} from "@goauthentik/api";
@customElement("ak-stage-mtls-form")
export class MTLSStageForm extends BaseStageForm<MutualTLSStage> {
loadInstance(pk: string): Promise<MutualTLSStage> {
return new StagesApi(DEFAULT_CONFIG).stagesMtlsRetrieve({
stageUuid: pk,
});
}
async send(data: MutualTLSStage): Promise<MutualTLSStage> {
if (this.instance) {
return new StagesApi(DEFAULT_CONFIG).stagesMtlsUpdate({
stageUuid: this.instance.pk || "",
mutualTLSStageRequest: data,
});
} else {
return new StagesApi(DEFAULT_CONFIG).stagesMtlsCreate({
mutualTLSStageRequest: data,
});
}
}
renderForm(): TemplateResult {
return html`
<span> ${msg("Client-certificate/mTLS authentication/enrollment.")} </span>
<ak-form-element-horizontal label=${msg("Name")} ?required=${true} name="name">
<input
type="text"
value="${ifDefined(this.instance?.name || "")}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-group .expanded=${true}>
<span slot="header"> ${msg("Stage-specific settings")} </span>
<div slot="body" class="pf-c-form">
<ak-form-element-horizontal label=${msg("Mode")} required name="mode">
<ak-radio
.options=${[
{
label: msg("Certificate optional"),
value: MutualTLSStageModeEnum.Optional,
default: true,
description: html`${msg(
"If no certificate was provided, this stage will succeed and continue to the next stage.",
)}`,
},
{
label: msg("Certificate required"),
value: MutualTLSStageModeEnum.Required,
description: html`${msg(
"If no certificate was provided, this stage will stop flow execution.",
)}`,
},
]}
.value=${this.instance?.mode}
>
</ak-radio>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Certificate authorities")}
name="certificateAuthorities"
>
<ak-dual-select-dynamic-selected
.provider=${certificateProvider}
.selector=${certificateSelector(this.instance?.certificateAuthorities)}
available-label=${msg("Available Certificates")}
selected-label=${msg("Selected Certificates")}
></ak-dual-select-dynamic-selected>
<p class="pf-c-form__helper-text">
${msg(
"Configure the certificate authority client certificates are validated against. The certificate authority can also be configured on a brand, which allows for different certificate authorities for different domains.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("Certificate attribute")}
required
name="certAttribute"
>
<ak-radio
.options=${[
{
label: msg("Common Name"),
value: CertAttributeEnum.CommonName,
},
{
label: msg("Email"),
value: CertAttributeEnum.Email,
default: true,
},
{
label: msg("Subject"),
value: CertAttributeEnum.Subject,
},
]}
.value=${this.instance?.certAttribute}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg(
"Configure the attribute of the certificate used to look for a user.",
)}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${msg("User attribute")}
required
name="userAttribute"
>
<ak-radio
.options=${[
{
label: msg("Username"),
value: UserAttributeEnum.Username,
},
{
label: msg("Email"),
value: UserAttributeEnum.Email,
default: true,
},
]}
.value=${this.instance?.userAttribute}
>
</ak-radio>
<p class="pf-c-form__helper-text">
${msg("Configure the attribute of the user used to look for a user.")}
</p>
</ak-form-element-horizontal>
</div>
</ak-form-group>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ak-stage-mtls-form": MTLSStageForm;
}
}