Files
felhom.eu/manifests/hub.yaml
T
admin 8b188bea68
gates / gates (push) Successful in 37s
hub v0.103.0 — a host can read the packages we kept for it (R-311)
ListSupersededEscrow had zero production callers for nineteen days. It is the only
reader of a retained identity_blob, so the retention shipped in v0.93.0 was
material the product could not reach - proven on the fixture 2026-08-12, where a
code that opens a retained package was answered as a code that opened nothing.

New GET /api/v1/hosts/<id>/escrow/retained: self-scoped exactly as the current-row
GET, same recovery-mode gate, same audit event written BEFORE the bytes leave,
capped at 16. Rows with a NULL identity_blob are WITHHELD and returned as
unopenable_count - they retain the PBS key, not the repository password, so they
can never open what the caller is asking about, and serving them would let the
screen claim an earlier package is openable on exactly the boxes the original
defect hurt. The count is returned because their existence is load-bearing and
underivable by the caller.

The trade, stated rather than waved through: the hub still cannot read any of it -
sealed bytes in, sealed bytes out, no decrypt path, no recovery code ever held.
What widens is volume, bounded by self-scope, the recovery-mode gate and the cap.

The response is a NAMED TYPE, not a map, so the wire-contract gate can resolve it;
the wire is declared as a fourth ROOT and the gate now checks 182 tags rather than
174. A positive control shows that check is name-presence, not decodability -
filed as R-315 rather than reported as coverage.

Also: golden 0.214.0 baked, published and round-trip verified; the countdown on
demo-felhom cancelled on the operator's ruling (R-307); the spike that halted
Part 3 recorded as R-312; the set-aside store found unrecoverable as R-313.
Six hub tests through the real endpoint; four red-proofs asserted applied.
2026-08-12 18:56:44 +02:00

338 lines
12 KiB
YAML

# Felhom Hub — Multi-customer dashboard
# Dashboard: https://hub.felhom.eu
# API: POST /api/v1/report (Bearer token auth)
#
# Receives health reports from customer controllers and displays
# a centralized overview dashboard for the operator (Viktor).
#
# Namespace: felhom-system (shared with healthchecks and other felhom infra)
#
# PREREQUISITES:
# 1. Build and push the hub image:
# cd ~/build/felhom-hub && ./build.sh v0.2.0 --push
#
# 2. Generate a bcrypt password hash for dashboard login:
# htpasswd -nbBC 10 "" "your-password" | cut -d: -f2
# Update the ConfigMap password_hash field below.
#
# 3. Create the operator/global bearer key Secret (out-of-band, NEVER committed):
# openssl rand -hex 32 # mint
# kubectl -n felhom-system create secret generic report-api \
# --from-literal=REPORT_API_KEY=<minted-key>
# (Customer boxes use per-customer/per-host keys generated by the hub — the global
# key is the operator's own, e.g. felhom-ops -hub-key.)
#
# 4. Apply this manifest:
# kubectl apply -f manifests/hub.yaml
#
# 5. Configure DNS:
# Add hub.felhom.eu → k3s cluster IP in Cloudflare
#
# DEBUGGING:
# kubectl logs -n felhom-system deploy/hub -f
# kubectl exec -it -n felhom-system deploy/hub -- ls /data/
# kubectl describe ingress -n felhom-system hub
# =============================================================================
# PERSISTENT STORAGE
# =============================================================================
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: hub-data
namespace: felhom-system
labels:
app: hub
recurring-job-group.longhorn.io/default: disabled
spec:
accessModes:
- ReadWriteOnce
storageClassName: longhorn
resources:
requests:
storage: 1Gi
# =============================================================================
# CONFIGURATION
# =============================================================================
---
apiVersion: v1
kind: ConfigMap
metadata:
name: hub-config
namespace: felhom-system
data:
hub.yaml: |
auth:
# Bcrypt hash for dashboard login (Viktor only)
# Generate: htpasswd -nbBC 10 "" "your-password" | cut -d: -f2
password_hash: "$2y$10$N5.O9jBnc.1tIlJT/irx3OlVjJQemlCHRnfqIJg/EyZofnzXSCpeG"
api:
# Operator/global bearer key. NOT stored here since v0.53.0 — injected at runtime from
# Secret/report-api via the REPORT_API_KEY env var (see Deployment below). The Secret is
# created out-of-band and NOT committed (documentation/runbooks/secrets.md); the previously
# committed literal is retired by ROTATION (see the publish-runbook notes). Leave empty.
report_api_key: ""
retention:
max_days: 90
prune_schedule: "04:30"
alerting:
stale_threshold: "30m"
notifications:
# Resend API key is NOT stored here. It is injected at runtime from Secret/resend-api
# via the RESEND_API_KEY env var (see Deployment below). The Secret is created out-of-band
# and is NOT committed — see documentation/runbooks/secrets.md. Leave this empty.
resend_api_key: ""
# Operator alert recipient + enable. WITHOUT both, Dispatcher.processOperator returns early and
# NO operator email is ever sent — the self-health pipeline (probe→report→checker→dispatch) stops
# one hop short of the inbox (TESTRUN finding: the unproven hop). The address is the operator's own
# and is not a secret. from_email defaults to monitoring@felhom.eu.
operator_email: "admin@felhom.eu"
operator_enabled: true
registry:
image: "gitea.dooplex.hu/admin/felhom-controller"
# username + token injected via REGISTRY_USERNAME / REGISTRY_TOKEN env vars
# from Secret/gitea-creds (see Deployment below)
check_interval: "6h"
template_interval: "1h"
server:
listen: ":8080"
data_dir: "/data"
# =============================================================================
# DEPLOYMENT
# =============================================================================
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: hub
namespace: felhom-system
labels:
app: hub
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
app: hub
template:
metadata:
labels:
app: hub
spec:
containers:
- name: hub
image: gitea.dooplex.hu/admin/felhom-hub:0.103.0
ports:
- containerPort: 8080
name: http
env:
- name: TZ
value: "Europe/Budapest"
# Phase 2 managed updates: global controller-version FLOOR fallback. Any reporting box below
# this auto-updates to it (unless a per-customer override is set via the operator UI).
- name: DEFAULT_MIN_CONTROLLER_VERSION
value: "0.120.0"
# Resend API key — injected from the out-of-band Secret/resend-api (NOT committed).
# See documentation/runbooks/secrets.md. Overrides the empty ConfigMap placeholder.
- name: RESEND_API_KEY
valueFrom:
secretKeyRef:
name: resend-api
key: RESEND_API_KEY
# Operator/global bearer key — injected from the out-of-band Secret/report-api
# (NOT committed; documentation/runbooks/secrets.md). Deliberately NOT optional:
# a missing Secret must fail the pod Ready rather than boot an unauthenticatable
# hub with an empty bearer key. Create the Secret BEFORE syncing this manifest.
- name: REPORT_API_KEY
valueFrom:
secretKeyRef:
name: report-api
key: REPORT_API_KEY
- name: REGISTRY_USERNAME
valueFrom:
secretKeyRef:
name: gitea-creds
key: username
- name: REGISTRY_TOKEN
valueFrom:
secretKeyRef:
name: gitea-creds
key: password
# S1 offsite connectivity: the WG peer-sync push channel (doc 06 §5 + runbook
# offsite-endpoint.md). Addr is dev-phase literal (the throwaway endpoint); the SSH
# private key + (non-secret) pinned host key come from Secret/wg-endpoint-ssh,
# created out-of-band in runbook step 6 — optional so the pod starts before it
# exists (the hub logs peer-sync disabled until then).
- name: WG_ENDPOINT_SSH_ADDR
value: "167.233.158.164:22"
- name: WG_ENDPOINT_SSH_USER
value: "felhom-peersync"
- name: WG_ENDPOINT_SSH_KEY_FILE
value: "/etc/hub-secrets/wg-endpoint-ssh/key"
- name: WG_ENDPOINT_SSH_HOSTKEY
valueFrom:
secretKeyRef:
name: wg-endpoint-ssh
key: hostkey
optional: true
# PBS DR tier (SLICE 1): the tenantsync channel — same endpoint + pinned host key as
# peersync (env above), its OWN private key from Secret/tenantsync (out-of-band,
# runbook offsite-endpoint.md §10). Optional: absent → the hub logs tenantsync disabled.
- name: TENANTSYNC_SSH_KEY_FILE
value: "/etc/hub-secrets/tenantsync/key"
# Agent-plane immediate-sync (Direction-2a, v0.59.0): the poke sender — same endpoint +
# pinned host key + peersync user as above, its OWN forced-command key from Secret/agent-poke
# (out-of-band; the ep0 authorized_keys line carries the PUBLIC half, command="felhom-poke").
# Optional: absent → the hub logs the poke disabled; agent-plane saves still reconcile in
# ≤15 min. See documentation/runbooks/offsite-endpoint.md (poke section).
- name: POKE_SSH_KEY_FILE
value: "/etc/hub-secrets/agent-poke/key"
# Offsite provisioning (SLICE 1+2): Hetzner Storage Box API token + the NUMERIC id of the
# pool box, from the out-of-band Secret/storagebox (NOT committed). The token MUST be scoped
# to the dedicated storage project — NEVER the shared-project token (it can touch ep0).
# HETZNER_POOL_BOX_ID is the numeric box id (console #id), not the box name. Optional so the
# pod starts before the secret exists (hub then logs offsite provisioning disabled).
- name: HETZNER_POOL_BOX_ID
valueFrom:
secretKeyRef:
name: storagebox
key: HETZNER_POOL_BOX_ID
optional: true
- name: HETZNER_TOKEN
valueFrom:
secretKeyRef:
name: storagebox
key: HETZNER_TOKEN
optional: true
# Non-secret; the hub defaults to fsn1 anyway — explicit for clarity.
- name: HETZNER_LOCATION
value: "fsn1"
resources:
requests:
memory: "64Mi"
cpu: "50m"
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/felhom-hub
- name: wg-endpoint-ssh
mountPath: /etc/hub-secrets/wg-endpoint-ssh
readOnly: true
- name: tenantsync
mountPath: /etc/hub-secrets/tenantsync
readOnly: true
- name: agent-poke
mountPath: /etc/hub-secrets/agent-poke
readOnly: true
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 3
periodSeconds: 10
timeoutSeconds: 3
volumes:
- name: data
persistentVolumeClaim:
claimName: hub-data
- name: config
configMap:
name: hub-config
- name: wg-endpoint-ssh
secret:
secretName: wg-endpoint-ssh
optional: true
items:
- key: key
path: key
mode: 0400
- name: tenantsync
secret:
secretName: tenantsync
optional: true
items:
- key: key
path: key
mode: 0400
- name: agent-poke
secret:
secretName: agent-poke
optional: true
items:
- key: key
path: key
mode: 0400
# =============================================================================
# SERVICE
# =============================================================================
---
apiVersion: v1
kind: Service
metadata:
name: hub
namespace: felhom-system
labels:
app: hub
spec:
selector:
app: hub
ports:
- port: 8080
targetPort: 8080
name: http
# =============================================================================
# INGRESS — hub.felhom.eu
# =============================================================================
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: hub
namespace: felhom-system
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "2m"
# Geo-restrict to Hungary (operator-only dashboard)
# NOTE: /api/v1/report must also be reachable — all customers are in HU
nginx.ingress.kubernetes.io/configuration-snippet: |
set $geo_allowed 0;
if ($remote_addr ~ "^192\.168\.") { set $geo_allowed 1; }
if ($remote_addr ~ "^10\.") { set $geo_allowed 1; }
if ($geoip2_country_code = "HU") { set $geo_allowed 1; }
if ($geo_allowed = 0) {
return 403 "Access restricted to Hungary";
}
spec:
ingressClassName: nginx-internal
tls:
- hosts:
- hub.felhom.eu
secretName: hub-felhom-eu-tls
rules:
- host: hub.felhom.eu
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: hub
port:
number: 8080