feat: add mTLS support for proxy hosts
- New `ca_certificates` table for reusable CA certs (migration 0011) - CA cert CRUD model, server actions, and UI dialogs - Proxy host create/edit dialogs include mTLS toggle + CA cert selection - Caddy config generates `client_authentication` TLS policy blocks with `require_and_verify` mode for hosts with mTLS enabled - CA certs sync to slave instances via instance-sync payload - Certificates page shows CA Certificates section Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,8 @@ import { UpstreamDnsResolutionFields } from "./UpstreamDnsResolutionFields";
|
||||
import { UpstreamInput } from "./UpstreamInput";
|
||||
import { GeoBlockFields } from "./GeoBlockFields";
|
||||
import { WafFields } from "./WafFields";
|
||||
import { MtlsFields } from "./MtlsConfig";
|
||||
import type { CaCertificate } from "@/src/lib/models/ca-certificates";
|
||||
|
||||
export function CreateHostDialog({
|
||||
open,
|
||||
@@ -28,7 +30,8 @@ export function CreateHostDialog({
|
||||
certificates,
|
||||
accessLists,
|
||||
authentikDefaults,
|
||||
initialData
|
||||
initialData,
|
||||
caCertificates = []
|
||||
}: {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -36,6 +39,7 @@ export function CreateHostDialog({
|
||||
accessLists: AccessList[];
|
||||
authentikDefaults: AuthentikSettings | null;
|
||||
initialData?: ProxyHost | null;
|
||||
caCertificates?: CaCertificate[];
|
||||
}) {
|
||||
const [state, formAction] = useFormState(createProxyHostAction, INITIAL_ACTION_STATE);
|
||||
|
||||
@@ -130,6 +134,7 @@ export function CreateHostDialog({
|
||||
<UpstreamDnsResolutionFields upstreamDnsResolution={initialData?.upstream_dns_resolution} />
|
||||
<GeoBlockFields />
|
||||
<WafFields value={initialData?.waf} />
|
||||
<MtlsFields value={initialData?.mtls} caCertificates={caCertificates} />
|
||||
</Stack>
|
||||
</AppDialog>
|
||||
);
|
||||
@@ -140,13 +145,15 @@ export function EditHostDialog({
|
||||
host,
|
||||
onClose,
|
||||
certificates,
|
||||
accessLists
|
||||
accessLists,
|
||||
caCertificates = []
|
||||
}: {
|
||||
open: boolean;
|
||||
host: ProxyHost;
|
||||
onClose: () => void;
|
||||
certificates: Certificate[];
|
||||
accessLists: AccessList[];
|
||||
caCertificates?: CaCertificate[];
|
||||
}) {
|
||||
const [state, formAction] = useFormState(updateProxyHostAction.bind(null, host.id), INITIAL_ACTION_STATE);
|
||||
|
||||
@@ -234,6 +241,7 @@ export function EditHostDialog({
|
||||
}}
|
||||
/>
|
||||
<WafFields value={host.waf} />
|
||||
<MtlsFields value={host.mtls} caCertificates={caCertificates} />
|
||||
</Stack>
|
||||
</AppDialog>
|
||||
);
|
||||
|
||||
126
src/components/proxy-hosts/MtlsConfig.tsx
Normal file
126
src/components/proxy-hosts/MtlsConfig.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Alert,
|
||||
Box,
|
||||
Checkbox,
|
||||
Collapse,
|
||||
FormControlLabel,
|
||||
Stack,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import LockPersonIcon from "@mui/icons-material/LockPerson";
|
||||
import { useState } from "react";
|
||||
import type { CaCertificate } from "@/src/lib/models/ca-certificates";
|
||||
import type { MtlsConfig } from "@/src/lib/models/proxy-hosts";
|
||||
|
||||
type Props = {
|
||||
value?: MtlsConfig | null;
|
||||
caCertificates: CaCertificate[];
|
||||
};
|
||||
|
||||
export function MtlsFields({ value, caCertificates }: Props) {
|
||||
const [enabled, setEnabled] = useState(value?.enabled ?? false);
|
||||
const [selectedIds, setSelectedIds] = useState<number[]>(value?.ca_certificate_ids ?? []);
|
||||
|
||||
function toggleId(id: number) {
|
||||
setSelectedIds(prev =>
|
||||
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
border: "1px solid",
|
||||
borderColor: "info.main",
|
||||
bgcolor: (theme) =>
|
||||
theme.palette.mode === "dark" ? "rgba(2,136,209,0.06)" : "rgba(2,136,209,0.04)",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<input type="hidden" name="mtls_present" value="1" />
|
||||
<input type="hidden" name="mtls_enabled" value={enabled ? "true" : "false"} />
|
||||
{enabled && selectedIds.map(id => (
|
||||
<input key={id} type="hidden" name="mtls_ca_cert_id" value={String(id)} />
|
||||
))}
|
||||
|
||||
{/* Header */}
|
||||
<Stack direction="row" alignItems="flex-start" justifyContent="space-between" spacing={1}>
|
||||
<Stack direction="row" alignItems="flex-start" spacing={1.5} flex={1} minWidth={0}>
|
||||
<Box
|
||||
sx={{
|
||||
mt: 0.25,
|
||||
width: 32,
|
||||
height: 32,
|
||||
borderRadius: 1.5,
|
||||
bgcolor: "info.main",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<LockPersonIcon sx={{ fontSize: 18, color: "#fff" }} />
|
||||
</Box>
|
||||
<Box minWidth={0}>
|
||||
<Typography variant="subtitle1" fontWeight={700} lineHeight={1.3}>
|
||||
Mutual TLS (mTLS)
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" mt={0.25}>
|
||||
Require clients to present a certificate signed by a trusted CA
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onChange={(_, checked) => setEnabled(checked)}
|
||||
sx={{ flexShrink: 0 }}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
<Collapse in={enabled} timeout="auto" unmountOnExit>
|
||||
<Box mt={2}>
|
||||
<Alert severity="info" sx={{ mb: 2 }}>
|
||||
mTLS requires TLS to be configured on this host (certificate must be set).
|
||||
</Alert>
|
||||
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
fontWeight={600}
|
||||
sx={{ textTransform: "uppercase", letterSpacing: 0.5 }}
|
||||
>
|
||||
Trusted Client CA Certificates
|
||||
</Typography>
|
||||
|
||||
{caCertificates.length === 0 ? (
|
||||
<Typography variant="body2" color="text.secondary" mt={1}>
|
||||
No CA certificates configured. Add them on the Certificates page.
|
||||
</Typography>
|
||||
) : (
|
||||
<Stack mt={0.5}>
|
||||
{caCertificates.map(ca => (
|
||||
<FormControlLabel
|
||||
key={ca.id}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={selectedIds.includes(ca.id)}
|
||||
onChange={() => toggleId(ca.id)}
|
||||
size="small"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Typography variant="body2">{ca.name}</Typography>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user