Add CA download to dashboard: GET /gateway/ca.crt + Gateway-panel button

When the gateway serves internal-CA HTTPS (gateway.tls=internal), other devices
must trust Caddy's root CA to accept *.lan HTTPS. Surface that from the dashboard
instead of hand-copying root.crt around.

- API: GET /gateway/ca.crt returns the public root cert (PEM) as a download,
  sourced from Caddy's admin API (/pki/ca/local — matches the running gateway)
  with an on-disk root.crt fallback. 404 unless tls=internal. GatewayInfo gains
  `tls` and `ca_fingerprint` (SHA-256, for out-of-band verification). The CA
  private key is never exposed — only the public root.
- UI: a "CA cert" button in the Gateway panel, shown only when tls=internal,
  linking to the download with the fingerprint in its tooltip.
- Tests for the tls=off defaults and the 404-without-internal-tls path; docs note
  the button + endpoint.
This commit is contained in:
2026-06-29 08:26:15 -07:00
parent 0d9c1ae6c2
commit fc5802d823
6 changed files with 116 additions and 5 deletions

View File

@@ -1,8 +1,9 @@
import { useMemo, useState } from "react"
import { Link } from "react-router-dom"
import { Globe, RefreshCw, FileText } from "lucide-react"
import { Globe, RefreshCw, FileText, ShieldCheck } from "lucide-react"
import type { GatewayInfo, HealthStatus } from "@/types"
import { useGatewayReload, useCaddyfile } from "@/services/api/hooks"
import { apiClient } from "@/services/api/client"
import { HealthBadge } from "./HealthBadge"
interface GatewayPanelProps {
@@ -33,6 +34,20 @@ export function GatewayPanel({ gateway, statuses }: GatewayPanelProps) {
</span>
</div>
<div className="flex items-center gap-1">
{gateway.tls === "internal" && (
<a
href={apiClient.streamUrl("/gateway/ca.crt")}
download="castle-root.crt"
className="flex items-center gap-1 text-xs px-2.5 py-1 rounded bg-[var(--border)] hover:bg-[var(--border)]/80 text-[var(--muted)] hover:text-[var(--foreground)] transition-colors"
title={
"Download the gateway's root CA — install on other devices to trust *.lan HTTPS" +
(gateway.ca_fingerprint ? `\nSHA-256: ${gateway.ca_fingerprint}` : "")
}
>
<ShieldCheck size={12} />
CA cert
</a>
)}
<button
onClick={() => reload()}
disabled={reloading}