Issue real certificates via ACME DNS-01
Tunnels now get a per-tunnel certificate from the configured ACME CA (Let's Encrypt by default) with DNS-01 through Porkbun, keeping the client's CSR and key. Issuance is asynchronous (issuing/ready/failed); the account persists in the store, renewals run in a 30-day window with retries, and a failed renewal keeps the old certificate serving. Switching from the local CA reissues existing tunnels automatically, and the admin dashboard shows the issuer and certificate state. Verified against Pebble and Let's Encrypt staging.
This commit is contained in:
+2
-1
@@ -4,6 +4,7 @@
|
||||
import SummaryCards from "./lib/SummaryCards.svelte";
|
||||
import TunnelsTable from "./lib/TunnelsTable.svelte";
|
||||
import BridgesTable from "./lib/BridgesTable.svelte";
|
||||
import { fmtIssuer } from "./lib/format";
|
||||
|
||||
const KEY = "tunneld-admin-token";
|
||||
const REFRESH_MS = 5000;
|
||||
@@ -56,7 +57,7 @@
|
||||
<h1>tunneld admin</h1>
|
||||
<span class="meta">
|
||||
{#if overview}
|
||||
{overview.server.domain} · v{overview.server.version} · {overview.server.go}
|
||||
{overview.server.domain} · v{overview.server.version} · {overview.server.go} · {fmtIssuer(overview.server.issuer)}
|
||||
{/if}
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<div class="card"><div class="label">Online</div><div class="value">{totals.online}</div></div>
|
||||
<div class="card"><div class="label">Bridges</div><div class="value">{totals.bridges}</div></div>
|
||||
<div class="card"><div class="label">Visitors</div><div class="value">{totals.visitors}</div></div>
|
||||
<div class="card"><div class="label">CA expires</div><div class="value small">{fmtDate(ca.expires)}</div></div>
|
||||
<div class="card"><div class="label">Local CA expires</div><div class="value small">{fmtDate(ca.expires)}</div></div>
|
||||
<div class="card"><div class="label">Uptime</div><div class="value small">{fmtUptime(server.uptime_seconds)}</div></div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -37,7 +37,16 @@
|
||||
{/if}
|
||||
</td>
|
||||
<td>{tunnel.visitors}</td>
|
||||
<td>{fmtDate(tunnel.certificate_expiry)}</td>
|
||||
<td title={tunnel.certificate_error ?? ""}>
|
||||
{fmtDate(tunnel.certificate_expiry)}
|
||||
{#if tunnel.certificate_state === "issuing"}
|
||||
<span class="dim"> issuing…</span>
|
||||
{:else if tunnel.certificate_state === "failed"}
|
||||
<span class="pill offline">failed</span>
|
||||
{:else if tunnel.certificate_error}
|
||||
<span class="dim"> retrying</span>
|
||||
{/if}
|
||||
</td>
|
||||
<td>{fmtDate(tunnel.created_at)}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
|
||||
@@ -4,6 +4,7 @@ export interface ServerInfo {
|
||||
domain: string;
|
||||
version: string;
|
||||
go: string;
|
||||
issuer: string;
|
||||
uptime_seconds: number;
|
||||
}
|
||||
|
||||
@@ -25,6 +26,9 @@ export interface Tunnel {
|
||||
routes: string[] | null;
|
||||
visitors: number;
|
||||
certificate_expiry?: string;
|
||||
certificate_state?: "issuing" | "ready" | "failed";
|
||||
certificate_issuer?: string;
|
||||
certificate_error?: string;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,11 @@ export function fmtRoutes(routes: string[] | null): string {
|
||||
return routes && routes.length > 0 ? routes.join(", ") : "–";
|
||||
}
|
||||
|
||||
export function fmtIssuer(issuer: string): string {
|
||||
if (!issuer || issuer === "local") return "local CA";
|
||||
return issuer.replace(/^https?:\/\//, "").replace(/\/directory$/, "");
|
||||
}
|
||||
|
||||
// routeUrl is the public URL a route is served at: "@" is the tunnel
|
||||
// hostname itself, any other route is a subdomain of it.
|
||||
export function routeUrl(hostname: string, route: string): string {
|
||||
|
||||
Reference in New Issue
Block a user