diff --git a/internal/checkdefaults/defaults.go b/internal/checkdefaults/defaults.go index ec8bde6..e6f5c0e 100644 --- a/internal/checkdefaults/defaults.go +++ b/internal/checkdefaults/defaults.go @@ -24,13 +24,14 @@ import ( // check_defs.kind values — one semantic kind can expand to several concrete // checks (`resource` becomes four ssh-script rows). const ( - KindPing = "ping" - KindResource = "resource" - KindUpdates = "updates" - KindProcess = "process" - KindHTTP = "http" - KindCapacity = "capacity" - KindBackup = "backup-freshness" + KindPing = "ping" + KindResource = "resource" + KindUpdates = "updates" + KindProcess = "process" + KindHTTP = "http" + KindCapacity = "capacity" + KindBackup = "backup-freshness" + KindCertExpiry = "cert-expiry" ) // defaultBackupMaxAge is how long a backup target may go without a new @@ -246,11 +247,40 @@ func buildKind(kind string, t Target, attrs map[string]any, host, user string, p config: map[string]any{"url": url, "max_status": 500}, interval: 60, }}, "" + + case KindCertExpiry: + // The host to TLS-dial for the cert. Prefer an explicit `hostname` + // attribute, then `cn`, then a dotted name (a certificate's name is + // its CN/SAN). Hourly: expiry changes once a day, but a renewal or a + // mis-issued cert is worth noticing within the hour. + host := certHost(t, attrs) + if host == "" { + return nil, "no hostname / cn / dotted name to dial for the cert" + } + return []checkDef{{ + kind: "cert-expiry", + config: map[string]any{"host": host, "warn_days": 30, "crit_days": 7}, + interval: 3600, + }}, "" } return nil, "no builder for this kind yet" } +// certHost works out the hostname to TLS-dial for a certificate's expiry. +func certHost(t Target, attrs map[string]any) string { + for _, key := range []string{"hostname", "cn", "san"} { + if v, ok := attrs[key].(string); ok && v != "" { + return v + } + } + // A dotted name is a hostname (hubris.network, media.hubris.network). + if strings.Contains(t.Name, ".") && !strings.Contains(t.Name, " ") { + return t.Name + } + return "" +} + // writeCheck upserts one check_def and its backing check entity. // // The entity upsert MUST return the row's id. The previous version generated diff --git a/seeds/inventory.yaml b/seeds/inventory.yaml index 957594e..9853fde 100644 --- a/seeds/inventory.yaml +++ b/seeds/inventory.yaml @@ -339,6 +339,30 @@ entities: - {slug: "ingress:sab.hubris.network", type: ingress-route, name: sab.hubris.network, attributes: {forward_auth: true}} + # ─── TLS certificates (Caddy-managed, *.hubris.network) ─────────── + # One entity per public hostname; the cert-expiry checker dials each on + # :443 and reads NotAfter. The cn/name is the hostname the checker dials. + - {slug: "cert:proxmox.hubris.network", type: certificate, name: proxmox.hubris.network} + - {slug: "cert:git.hubris.network", type: certificate, name: git.hubris.network} + - {slug: "cert:auth.hubris.network", type: certificate, name: auth.hubris.network} + - {slug: "cert:media.hubris.network", type: certificate, name: media.hubris.network} + - {slug: "cert:cloud.hubris.network", type: certificate, name: cloud.hubris.network} + - {slug: "cert:paperless.hubris.network", type: certificate, name: paperless.hubris.network} + - {slug: "cert:matrix.hubris.network", type: certificate, name: matrix.hubris.network} + - {slug: "cert:photos.hubris.network", type: certificate, name: photos.hubris.network} + - {slug: "cert:artifacto.hubris.network", type: certificate, name: artifacto.hubris.network} + - {slug: "cert:trmnl.hubris.network", type: certificate, name: trmnl.hubris.network} + - {slug: "cert:zimaos.hubris.network", type: certificate, name: zimaos.hubris.network} + - {slug: "cert:teddy.hubris.network", type: certificate, name: teddy.hubris.network} + - {slug: "cert:mcp.hubris.network", type: certificate, name: mcp.hubris.network} + - {slug: "cert:house.hubris.network", type: certificate, name: house.hubris.network} + - {slug: "cert:books.hubris.network", type: certificate, name: books.hubris.network} + - {slug: "cert:seanime.hubris.network", type: certificate, name: seanime.hubris.network} + - {slug: "cert:roms.hubris.network", type: certificate, name: roms.hubris.network} + - {slug: "cert:jellyseerr.hubris.network", type: certificate, name: jellyseerr.hubris.network} + - {slug: "cert:qbit.hubris.network", type: certificate, name: qbit.hubris.network} + - {slug: "cert:sab.hubris.network", type: certificate, name: sab.hubris.network} + # ─── Governance ──────────────────────────────────────────────────── - {slug: "person:dtoro", type: person, name: dtoro, attributes: {matrix_id: "@dtoro:avispero"}} @@ -486,6 +510,28 @@ relationships: - {source: "ingress:jellyseerr.hubris.network", target: "service:caddy", type: served-by} - {source: "ingress:qbit.hubris.network", target: "service:caddy", type: served-by} - {source: "ingress:sab.hubris.network", target: "service:caddy", type: served-by} + # Each public route is served with its TLS certificate. + - {source: "ingress:proxmox.hubris.network", target: "cert:proxmox.hubris.network", type: uses-certificate} + - {source: "ingress:git.hubris.network", target: "cert:git.hubris.network", type: uses-certificate} + - {source: "ingress:auth.hubris.network", target: "cert:auth.hubris.network", type: uses-certificate} + - {source: "ingress:media.hubris.network", target: "cert:media.hubris.network", type: uses-certificate} + - {source: "ingress:cloud.hubris.network", target: "cert:cloud.hubris.network", type: uses-certificate} + - {source: "ingress:paperless.hubris.network", target: "cert:paperless.hubris.network", type: uses-certificate} + - {source: "ingress:matrix.hubris.network", target: "cert:matrix.hubris.network", type: uses-certificate} + - {source: "ingress:photos.hubris.network", target: "cert:photos.hubris.network", type: uses-certificate} + - {source: "ingress:artifacto.hubris.network", target: "cert:artifacto.hubris.network", type: uses-certificate} + - {source: "ingress:trmnl.hubris.network", target: "cert:trmnl.hubris.network", type: uses-certificate} + - {source: "ingress:zimaos.hubris.network", target: "cert:zimaos.hubris.network", type: uses-certificate} + - {source: "ingress:teddy.hubris.network", target: "cert:teddy.hubris.network", type: uses-certificate} + - {source: "ingress:mcp.hubris.network", target: "cert:mcp.hubris.network", type: uses-certificate} + - {source: "ingress:house.hubris.network", target: "cert:house.hubris.network", type: uses-certificate} + - {source: "ingress:books.hubris.network", target: "cert:books.hubris.network", type: uses-certificate} + - {source: "ingress:seanime.hubris.network", target: "cert:seanime.hubris.network", type: uses-certificate} + - {source: "ingress:roms.hubris.network", target: "cert:roms.hubris.network", type: uses-certificate} + - {source: "ingress:jellyseerr.hubris.network", target: "cert:jellyseerr.hubris.network", type: uses-certificate} + - {source: "ingress:qbit.hubris.network", target: "cert:qbit.hubris.network", type: uses-certificate} + - {source: "ingress:sab.hubris.network", target: "cert:sab.hubris.network", type: uses-certificate} + - {source: "ingress:secrets.hubris.network", target: "service:secrets-issuance", type: routes-to} - {source: "ingress:house.hubris.network", target: "service:house", type: routes-to} - {source: "ingress:books.hubris.network", target: "service:grimmory", type: routes-to}