Add mTLS RBAC with path-based access control, role/cert trust model, and comprehensive tests

Implements full role-based access control for mTLS client certificates:
- Database: mtls_roles, mtls_certificate_roles, mtls_access_rules tables with migration
- Models: CRUD for roles, cert-role assignments, path-based access rules
- Caddy config: HTTP-layer RBAC enforcement via CEL fingerprint matching in subroutes
- New trust model: select individual certs or entire roles instead of CAs (derives CAs automatically)
- REST API: /api/v1/mtls-roles, cert assignments, proxy-host access rules endpoints
- UI: Roles management tab (card-based), cert/role trust picker, inline RBAC rule editor
- Fix: dialog autoclose bug after creating proxy host (key-based remount)
- Tests: 85 new tests (785 total) covering models, schema, RBAC route generation, leaf override, edge cases

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
fuomag9
2026-04-05 18:40:21 +02:00
parent a2b8d69aa6
commit 277ae6e79c
28 changed files with 3484 additions and 86 deletions

View File

@@ -28,6 +28,8 @@ import { RedirectsFields } from "./RedirectsFields";
import { LocationRulesFields } from "./LocationRulesFields";
import { RewriteFields } from "./RewriteFields";
import type { CaCertificate } from "@/lib/models/ca-certificates";
import type { MtlsRole } from "@/lib/models/mtls-roles";
import type { IssuedClientCertificate } from "@/lib/models/issued-client-certificates";
export function CreateHostDialog({
open,
@@ -36,7 +38,9 @@ export function CreateHostDialog({
accessLists,
authentikDefaults,
initialData,
caCertificates = []
caCertificates = [],
mtlsRoles = [],
issuedClientCerts = [],
}: {
open: boolean;
onClose: () => void;
@@ -45,6 +49,8 @@ export function CreateHostDialog({
authentikDefaults: AuthentikSettings | null;
initialData?: ProxyHost | null;
caCertificates?: CaCertificate[];
mtlsRoles?: MtlsRole[];
issuedClientCerts?: IssuedClientCertificate[];
}) {
const [state, formAction] = useFormState(createProxyHostAction, INITIAL_ACTION_STATE);
@@ -164,7 +170,12 @@ export function CreateHostDialog({
<UpstreamDnsResolutionFields upstreamDnsResolution={initialData?.upstream_dns_resolution} />
<GeoBlockFields />
<WafFields value={initialData?.waf} />
<MtlsFields value={initialData?.mtls} caCertificates={caCertificates} />
<MtlsFields
value={initialData?.mtls}
caCertificates={caCertificates}
mtlsRoles={mtlsRoles}
issuedClientCerts={issuedClientCerts}
/>
</form>
</AppDialog>
);
@@ -176,7 +187,9 @@ export function EditHostDialog({
onClose,
certificates,
accessLists,
caCertificates = []
caCertificates = [],
mtlsRoles = [],
issuedClientCerts = [],
}: {
open: boolean;
host: ProxyHost;
@@ -184,6 +197,8 @@ export function EditHostDialog({
certificates: Certificate[];
accessLists: AccessList[];
caCertificates?: CaCertificate[];
mtlsRoles?: MtlsRole[];
issuedClientCerts?: IssuedClientCertificate[];
}) {
const [state, formAction] = useFormState(updateProxyHostAction.bind(null, host.id), INITIAL_ACTION_STATE);
@@ -298,7 +313,13 @@ export function EditHostDialog({
}}
/>
<WafFields value={host.waf} />
<MtlsFields value={host.mtls} caCertificates={caCertificates} />
<MtlsFields
value={host.mtls}
caCertificates={caCertificates}
proxyHostId={host.id}
mtlsRoles={mtlsRoles}
issuedClientCerts={issuedClientCerts}
/>
</form>
</AppDialog>
);