feat(cert): dial the TLS terminator directly so cert-expiry works from the container
checkCertExpiry now accepts a `dial` address and sets ServerName to the hostname — it connects to the terminator's IP while SNI/cert-read use the hostname. The scheduler container has no mesh interface and the host resolver doesn't know the split-horizon zone, so *.hubris.network can't be dialed by name from there; dialing Caddy's lab IP (reachable on the LAN) makes the probe work. The builder passes through a cert entity's `dial` attribute. Re-seed the 20 *.hubris.network certificate entities with dial=192.168.8.175 (Caddy) and uses-certificate edges; cert-expiry monitoring now has real data.
This commit is contained in:
@@ -249,17 +249,26 @@ func buildKind(kind string, t Target, attrs map[string]any, host, user string, p
|
|||||||
}}, ""
|
}}, ""
|
||||||
|
|
||||||
case KindCertExpiry:
|
case KindCertExpiry:
|
||||||
// The host to TLS-dial for the cert. Prefer an explicit `hostname`
|
// The host whose cert to read (SNI / cert CN). Prefer an explicit
|
||||||
// attribute, then `cn`, then a dotted name (a certificate's name is
|
// `hostname` attribute, then `cn`, then a dotted name. Hourly: expiry
|
||||||
// its CN/SAN). Hourly: expiry changes once a day, but a renewal or a
|
// changes once a day, but a renewal or mis-issued cert is worth
|
||||||
// mis-issued cert is worth noticing within the hour.
|
// noticing within the hour.
|
||||||
host := certHost(t, attrs)
|
host := certHost(t, attrs)
|
||||||
if host == "" {
|
if host == "" {
|
||||||
return nil, "no hostname / cn / dotted name to dial for the cert"
|
return nil, "no hostname / cn / dotted name to dial for the cert"
|
||||||
}
|
}
|
||||||
|
// `dial` is the TLS terminator's address to connect to (Caddy's lab
|
||||||
|
// IP), used when the hostname doesn't resolve/reach from the scheduler.
|
||||||
|
// Without it the probe can't reach *.hubris.network from a container
|
||||||
|
// with no mesh / split-horizon DNS.
|
||||||
|
dial, _ := attrs["dial"].(string)
|
||||||
|
config := map[string]any{"host": host, "warn_days": 30, "crit_days": 7}
|
||||||
|
if dial != "" {
|
||||||
|
config["dial"] = dial
|
||||||
|
}
|
||||||
return []checkDef{{
|
return []checkDef{{
|
||||||
kind: "cert-expiry",
|
kind: "cert-expiry",
|
||||||
config: map[string]any{"host": host, "warn_days": 30, "crit_days": 7},
|
config: config,
|
||||||
interval: 3600,
|
interval: 3600,
|
||||||
}}, ""
|
}}, ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -536,6 +536,12 @@ func checkDisk(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) checkRes
|
|||||||
func checkCertExpiry(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) checkResult {
|
func checkCertExpiry(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) checkResult {
|
||||||
cfg := struct {
|
cfg := struct {
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
|
// Dial is an optional explicit dial address (the TLS terminator's IP)
|
||||||
|
// for when the hostname doesn't resolve/reach from the scheduler — the
|
||||||
|
// container has no mesh interface and the host resolver doesn't know
|
||||||
|
// the split-horizon zone, so *.hubris.network dials Caddy's lab IP
|
||||||
|
// directly while SNI/cert-read still uses Host.
|
||||||
|
Dial string `json:"dial"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
WarnDays int `json:"warn_days"`
|
WarnDays int `json:"warn_days"`
|
||||||
CritDays int `json:"crit_days"`
|
CritDays int `json:"crit_days"`
|
||||||
@@ -556,9 +562,13 @@ func checkCertExpiry(ctx context.Context, cd sqlcgen.ListEnabledCheckDefsRow) ch
|
|||||||
timeout = 30 * time.Second
|
timeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
|
|
||||||
addr := net.JoinHostPort(cfg.Host, fmt.Sprintf("%d", cfg.Port))
|
dialHost := cfg.Host
|
||||||
|
if cfg.Dial != "" {
|
||||||
|
dialHost = cfg.Dial
|
||||||
|
}
|
||||||
|
addr := net.JoinHostPort(dialHost, fmt.Sprintf("%d", cfg.Port))
|
||||||
|
|
||||||
d := tls.Dialer{Config: &tls.Config{InsecureSkipVerify: true}}
|
d := tls.Dialer{Config: &tls.Config{InsecureSkipVerify: true, ServerName: cfg.Host}}
|
||||||
conn, err := d.DialContext(ctx, "tcp", addr)
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return checkResult{
|
return checkResult{
|
||||||
|
|||||||
@@ -339,6 +339,32 @@ entities:
|
|||||||
- {slug: "ingress:sab.hubris.network", type: ingress-route, name: sab.hubris.network,
|
- {slug: "ingress:sab.hubris.network", type: ingress-route, name: sab.hubris.network,
|
||||||
attributes: {forward_auth: true}}
|
attributes: {forward_auth: true}}
|
||||||
|
|
||||||
|
# ─── TLS certificates (Caddy-managed, *.hubris.network) ───────────
|
||||||
|
# Each cert's expiry is probed by dialing Caddy's lab IP (`dial`) with SNI
|
||||||
|
# set to the hostname — the scheduler container has no mesh/split-horizon
|
||||||
|
# DNS, so it can't resolve *.hubris.network, but it CAN reach Caddy on the
|
||||||
|
# lab LAN.
|
||||||
|
- {slug: "cert:proxmox.hubris.network", type: certificate, name: proxmox.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:git.hubris.network", type: certificate, name: git.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:auth.hubris.network", type: certificate, name: auth.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:media.hubris.network", type: certificate, name: media.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:cloud.hubris.network", type: certificate, name: cloud.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:paperless.hubris.network", type: certificate, name: paperless.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:matrix.hubris.network", type: certificate, name: matrix.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:photos.hubris.network", type: certificate, name: photos.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:artifacto.hubris.network", type: certificate, name: artifacto.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:trmnl.hubris.network", type: certificate, name: trmnl.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:zimaos.hubris.network", type: certificate, name: zimaos.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:teddy.hubris.network", type: certificate, name: teddy.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:mcp.hubris.network", type: certificate, name: mcp.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:house.hubris.network", type: certificate, name: house.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:books.hubris.network", type: certificate, name: books.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:seanime.hubris.network", type: certificate, name: seanime.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:roms.hubris.network", type: certificate, name: roms.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:jellyseerr.hubris.network", type: certificate, name: jellyseerr.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:qbit.hubris.network", type: certificate, name: qbit.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
- {slug: "cert:sab.hubris.network", type: certificate, name: sab.hubris.network, attributes: {dial: "192.168.8.175"}}
|
||||||
|
|
||||||
# ─── Governance ────────────────────────────────────────────────────
|
# ─── Governance ────────────────────────────────────────────────────
|
||||||
- {slug: "person:dtoro", type: person, name: dtoro,
|
- {slug: "person:dtoro", type: person, name: dtoro,
|
||||||
attributes: {matrix_id: "@dtoro:avispero"}}
|
attributes: {matrix_id: "@dtoro:avispero"}}
|
||||||
@@ -486,6 +512,27 @@ relationships:
|
|||||||
- {source: "ingress:jellyseerr.hubris.network", target: "service:caddy", type: served-by}
|
- {source: "ingress:jellyseerr.hubris.network", target: "service:caddy", type: served-by}
|
||||||
- {source: "ingress:qbit.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}
|
- {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:secrets.hubris.network", target: "service:secrets-issuance", type: routes-to}
|
||||||
- {source: "ingress:house.hubris.network", target: "service:house", type: routes-to}
|
- {source: "ingress:house.hubris.network", target: "service:house", type: routes-to}
|
||||||
|
|||||||
Reference in New Issue
Block a user