Commit graph

320 commits

Author SHA1 Message Date
CTO (Paperclip Agent)
ec3e9624b3 feat(velero): schedules + PVC exclude annotations (DEV-573)
- velero/schedules/: cluster-all @ 03:00 (30d TTL, manifests only),
  per-namespace <ns>-manifests @ 03:15+n min (30 in-scope namespaces,
  30d TTL, no fs backup), and per-namespace <ns>-data @ 03:45+n min
  for the whitelist (pocket-id, vaultwarden, vikunja, passbolt;
  14d TTL, defaultVolumesToFsBackup=true).
- All schedules exclude velero, kube-system, kube-node-lease,
  kube-public so the Kopia password Secret never lands in S3.
- apps/app-velero-schedules.yaml: sibling Argo app syncing that
  directory into namespace velero with prune+selfHeal.
- opencloud, harbor(registry+trivy): pod-template annotation
  backup.velero.io/backup-volumes-excludes as a safety net so blob-
  heavy PVCs are never fs-backed-up even under a manual defaultVolumes
  run. nextcloud/monitoring workloads are not Argo-tracked yet — they
  were patched live in the same commit but need a follow-up to source-
  control them.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 17:35:49 +00:00
CTO (Paperclip Agent)
9b8cfebbd5 velero: reseal S3 credential with new Hetzner project key
The prior sealed secret's access key lived in the personal `imicros.de`
Hetzner Object Storage project and could not list objects in
`basicstack-velero` (bucket is in the BasicStack project) — BSL was
`Unavailable` with an S3 `AccessDenied` on ListObjectsV2.

CEO created a new S3 credential in the BasicStack project; re-sealed
against the cluster controller and applied. BSL `default` now
`Available`.

Refs DEV-572.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 17:25:15 +00:00
CTO Agent
60cc931d6b fix(velero): pin AWS plugin to v1.14.2 (v1.14.3 unreleased); shrink node-agent memory req to fit workers (DEV-572)
- velero-plugin-for-aws v1.14.3 is not published on docker.io (only rc.1/rc.2). Downgrade to the latest GA tag v1.14.2 — still compatible with Velero 1.18.x.
- Drop node-agent memory request 256Mi -> 64Mi so the daemonset schedules on worker-1/2/3 (they sit at ~95% memory-request already).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 17:08:09 +00:00
CTO Agent
b3d98bf902 feat(velero): Argo app + Helm values for Kopia/Hetzner S3 backups (DEV-572)
- apps/app-velero.yaml: Argo Application, path apps/velero, ns velero,
  CreateNamespace + ServerSideApply (chart uses long CRDs).
- apps/velero/Chart.yaml: pinned vmware-tanzu velero chart 12.1.0
  (appVersion 1.18.1).
- apps/velero/values.yaml: Kopia uploader, no CSI, node-agent enabled,
  AWS S3 provider -> https://hel1.your-objectstorage.com, dedicated
  bucket basicstack-velero (per DEV-570), BSL credential
  velero-s3-credentials/cloud, Kopia repo password auto-picked from
  velero-repo-credentials/repository-password.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 17:05:42 +00:00
CTO Agent
f318719c59 velero(secrets): seal dedicated Hetzner OS S3 credential (DEV-570)
Adds SealedSecret velero-s3-credentials in namespace velero, holding
the dedicated velero-prod Hetzner Object Storage access/secret key
(scoped only to the basicstack-velero bucket).

Sealed on cp-1 via /root/seal-velero-s3-credentials.sh with the
sealed-secrets-controller public cert; plaintext never left the host.
Only the in-cluster controller can decrypt.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 17:00:43 +00:00
CTO Agent
0c82ded5f7 velero(secrets): seal Kopia repo password for velero namespace
Adds the SealedSecret for the Kopia repository password that the Velero
BackupRepository will consume once the Argo app lands. Raw password is
held on cp-1 (root:root, 0600) pending the CEO handoff to Vaultwarden
and the offline safe copy (DEV-571).

Refs: DEV-569, DEV-571

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 16:22:22 +00:00
CTO
b2ee3ffa67 feat(openwebui): deploy Open WebUI v0.11.3 with Pocket ID OIDC (DEV-567)
Adds the Open WebUI stack at openwebui.basicstack.de:

- Namespace `openwebui`, ArgoCD Application with auto-sync + selfHeal
- Own Postgres 16 + pgvector (`pgvector/pgvector:pg16`, 20Gi encrypted PVC,
  fsn1-pinned); VECTOR_DB=pgvector, extension auto-created
- Own Redis 7 (2Gi encrypted PVC, fsn1-pinned) used both as cache and as
  websocket manager (db=1)
- Hetzner Object Storage bucket `openwebui-basicstack` (hel1, path-style)
  for file uploads; shared project S3 credentials
- Traefik ingress + Let's Encrypt TLS
- Pocket ID client `Open WebUI` (PKCE on) restricted to groups
  `openwebui_admins` and `openwebui_users`; `andreas.leinen@basicstack.de`
  is member of both and gets admin via OAUTH_ADMIN_ROLES
- Password login disabled (`ENABLE_LOGIN_FORM=false`, `ENABLE_SIGNUP=false`)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-12 14:06:56 +00:00
CTO
b2b0d27803 fix(vikunja): set pod FQDN so SMTP EHLO passes Stalwart validation (DEV-563)
Stalwart's port-25 relay rejects bare EHLO labels — needs at least one
dot. Vikunja doesn't call go-mail's WithHELO, so it sends
os.Hostname() as EHLO. Setting hostname/subdomain plus
setHostnameAsFQDN=true makes the pod's kernel hostname a valid FQDN
(vikunja.basicstack.vikunja.svc.cluster.local), which Stalwart
accepts.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 10:05:26 +00:00
CTO
2ec560e090 feat(vikunja): enable mailer via port-25 in-cluster relay (DEV-563)
Vikunja now sends reminder/notification emails through
`stalwart-smtp.stalwart.svc.cluster.local:25` using unauthenticated
relay from the pod-network CIDR that Stalwart already trusts. The
`vikunja@basicstack.de` principal exists in Stalwart but its password
is unknown and admin API access to reset it is currently broken
(admin/andreas/paperclip password auth returns `{"type":"failure"}` per
`stalwart-oidc-only-admin` note). Verified end-to-end: message from
`vikunja@basicstack.de` sent via port-25 relay arrived in the passbolt
mailbox.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 10:03:07 +00:00
CTO
bf7e78a573 feat(kaneo): wire SMTP submission via kaneo@basicstack.de (DEV-562)
Adds the kaneo-smtp SealedSecret and points the Kaneo container at
mail.basicstack.de:587 (STARTTLS) for outbound workspace invites and
notifications. DISABLE_EMAIL_OTP_SIGN_IN stays true so sign-in remains
OIDC-only via Pocket ID.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 10:01:52 +00:00
CTO
2c9e90ac2b docs(outline): note PKCE must be disabled on the Pocket ID client
Outline's OIDC plugin does not send `code_challenge`, so the Pocket ID
client must be created with `pkceEnabled: false` (default is `true`).
Login fails with `?notice=auth-error` and Pocket ID rejects
`/authorize` when PKCE is enforced.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:57:39 +00:00
CTO
244ff86714 docs(vikunja): note PKCE must be off on Pocket ID client (DEV-560)
Vikunja 2.x's frontend redirect helper and Go backend do not send
code_challenge. Pocket ID rejects the /authorize call with
'code_challenge parameter is missing' when the client has pkceEnabled=true.
The Vikunja client is confidential (isPublic=false) so client_secret still
authenticates the token exchange — turning PKCE off is safe and required.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:56:07 +00:00
CTO
18eba97fcb feat(vikunja): request vikunja_scope for OIDC group→team mapping (DEV-564)
Add `vikunja_scope` to the OIDC scope list the Vikunja app requests
from Pocket ID, so the `vikunja_groups` custom claim is available for
Vikunja's automatic team provisioning.

The claim itself is set per Pocket ID user group (`vikunja_admins`,
`vikunja_members`) via `PUT /api/custom-claims/user-group/:id`.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:47:37 +00:00
cto-agent
38c7b8c624 feat(outline): deploy Outline knowledge base at outline.basicstack.de (DEV-559)
- Namespace `outline` with Postgres 16, Redis 7, and Outline 1.10.0
- Stateful pods pinned to Hetzner location `fsn1` (encrypted CSI volumes)
- Pocket ID OIDC integration (group-restricted to outline_admins/outline_members)
- Hetzner Object Storage bucket `outline-basicstack` (region hel1) with CORS
- Traefik Ingress with cert-manager `letsencrypt-prod` (DNS-01)
- Sealed secrets for postgres, app (SECRET_KEY/UTILS_SECRET/URLs), OIDC, S3
- Argo CD Application `outline` in `apps/app-outline.yaml`

SMTP notifications deferred (see apps/outline/README.md).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:42:21 +00:00
CTO
c184eeaff3 chore(vikunja): rename JWT env to non-deprecated service.secret (DEV-560)
Vikunja 2.6.0 deprecates `service.jwtsecret` in favor of `service.secret`;
switch the env var name (secret payload unchanged) and fix the admin-promotion
example in the README to the current CLI (`user set-admin --admin`).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:41:05 +00:00
CTO
1743815703 feat(vikunja): deploy Vikunja 2.6.0 with Pocket ID OIDC (DEV-560)
Namespace-scoped Postgres 16 + unified Vikunja container behind Traefik
ingress at vikunja.basicstack.de. Both pods pinned to Hetzner fsn1 due to
their encrypted PVCs (10 Gi DB, 10 Gi attachments). Local auth disabled;
sign-in only via Pocket ID (client `Vikunja`, groups `vikunja_admins` /
`vikunja_members`). SMTP mailer left disabled pending mailbox provisioning.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:38:25 +00:00
CTO
28a28bf7c0 feat(kaneo): deploy Kaneo project management (DEV-561)
Hand-rolled manifests under apps/kaneo/ + Argo Application:

- Dedicated PostgreSQL 16-alpine on hcloud-volumes-encrypted (20Gi),
  pinned to fsn1 via csi.hetzner.cloud/location node affinity.
- Kaneo v2.22.0 (single ghcr.io/usekaneo/kaneo image; API + web on
  5173). Pinned to fsn1 for consistency with the volume.
- Traefik ingress at kaneo.basicstack.de with letsencrypt-prod cert.
- Better Auth JWT secret and Postgres credentials as SealedSecrets.
- OIDC via Pocket ID using CUSTOM_OAUTH_*, restricted to the two
  new Pocket ID groups kaneo-admins / kaneo-members (client id
  d58ff9bb-dc98-49b4-9535-4af2aba66de2).
- Registration + password + email-OTP sign-in disabled; SSO only.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-05 09:38:04 +00:00
dda91a7471 apps/grist/grist-deployment.yaml aktualisiert 2026-08-30 12:03:03 +00:00
CTO
f29e2a5cb5 chore(grist): enable ArgoCD auto-sync + selfHeal (DEV-556)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-30 11:07:29 +00:00
CTO
8cdfd0e2f9 feat(grist): deploy Grist community edition (DEV-556)
- New namespace grist with in-namespace PostgreSQL 16
- Ingress grist.basicstack.de + letsencrypt-prod (DNS-01 via Hetzner)
- Pocket ID OIDC login (client "Grist", group "grist")
- All PVCs on hcloud-volumes-encrypted
- ArgoCD application via apps/app-grist.yaml

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-30 11:05:20 +00:00
CTO Agent
a61f0cd630 fix(opencloud): shrink backup pod requests to fit worker-3 (DEV-555)
The scheduled backup pod could not start because worker-3 (where the
opencloud data PVC is attached and where podAffinity pins the backup)
had ~40Mi of memory left by requests after opencloud/tika were
co-scheduled. rclone actually uses well under 100Mi during the sync.

Drop cpu request 100m -> 50m and memory request 256Mi -> 64Mi. Limits
unchanged (500m / 512Mi) so it can still burst.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-30 10:26:13 +00:00
CTO
c97a2d218e chore(stalwart): bump v0.16.11 -> v0.16.18 (DEV-542)
Transport-only patch bump. No auth/OIDC/config changes.
Snapshots (config.json, DB dump, live YAML) attached to DEV-542.
Rollback: git revert; ArgoCD resyncs; PVCs retained.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 16:46:31 +00:00
CTO
e69e3d8eb4 chore(restic): bump image 0.17.3 -> 0.19.1 (DEV-541)
Mirrored docker.io/restic/restic:0.19.1 to
harbor.basicstack.de/library/restic:0.19.1 (crane in-cluster Job).
Updated all CronJob pins and the restore-drill/tag-bump docs.

Digest: sha256:136600b6ff6843d61d355f7f71f460a166429f35de6fd11b568fece3c9a4d510

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:52:15 +00:00
CTO Agent
96c3846202 chore(stalwart): bump alpine 3.19 -> 3.24 in backup cronjob (DEV-539)
Only occurrence of plain alpine:3.19 across apps/**. Touches:
- apps/stalwart/stalwart-fresh-deployment.yaml (stalwart-backup CronJob container)

Scope excludes alpine/k8s (see DEV-520 majors phase).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:42:36 +00:00
CTO Agent
76e250c334 fix(opencloud): use Recreate rollout strategy for RWO PVC (DEV-538)
The 7.2.0 -> 7.4.0 rollout stalled because the default RollingUpdate
tried to start the new pod before terminating the old one, but they
both mount opencloud-data (RWO), causing Multi-Attach errors.

Switch to strategy: Recreate so the old pod is terminated first and
the volume detaches cleanly before the new pod starts. Same fix
pattern as bookstack (DEV-528) and other RWO-backed apps.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:39:17 +00:00
CTO Agent
0887f87c82 chore(opencloud): bump 7.2.0 -> 7.4.0 (DEV-538)
Skimmed 7.3.0 (2026-07-14) and 7.4.0 (2026-08-03) release notes: no
breaking changes, no config or index migrations required.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:35:03 +00:00
CTO
a7e81db4e1 chore(directus): bump 12.1.1 -> 12.3.0 (DEV-537)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:27:14 +00:00
CTO
777a4d0404 chore(pocket-id): bump v2.11.0 -> v2.14.0 (DEV-536)
Minor version bump of the cluster OIDC provider. No breaking changes;
multi-client-secret support in 2.14 is additive; SameSite=Lax cookie
default in 2.13 is standard. DB snapshot captured pre-rollout.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:18:23 +00:00
CTO
3317401eba chore(headlamp): bump v0.43.0 -> v0.45.0 (DEV-535)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:14:38 +00:00
CTO Agent
71496fc5ac chore(dozzle): bump v10.6.10 -> v10.7.3 (DEV-534)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:11:47 +00:00
CTO Agent
6313b85999 chore(pangolin): bump postgres 17.5 -> 17.11 (DEV-533)
Patch bump per DEV-520 plan. Only pangolin uses postgres:17.5*;
other apps (paperclip, directus, forgejo, stalwart) are on
postgres:16-alpine and are out of scope for this ticket.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:07:38 +00:00
CTO Agent
7cbb603ec1 chore(pangolin): bump busybox 1.37 -> 1.38.0 (DEV-532)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 15:02:26 +00:00
CTO Agent
a226958b16 chore(paperclip): bump busybox 1.36 -> 1.38.0 (DEV-531)
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 14:59:34 +00:00
CTO Agent
e6a94ae26a chore(stalwart): remove no-op stalwart-allow-cluster-ips job (DEV-530)
The Job self-deletes after ttlSecondsAfterFinished=3600, causing ArgoCD
to report the app permanently OutOfSync. The job body only hits
/healthz/live and prints a manual TODO; it never modified Stalwart
config. The real IP allowlist is already declared in
apps/stalwart/stalwart-config.yaml under
[server.listener.*.security] allowed-ips.

Also removes the orphan ServiceAccount/Role/RoleBinding
stalwart-config-access bundled in the same file.

Board approval: DEV-529 interaction 9472bc9b-93d7-44e2-b32b-cf5598480f9a.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 14:18:26 +00:00
cto-agent
e155ab6a17 fix(bookstack): use Recreate rollout strategy (DEV-528)
BookStack is a single-replica Deployment backed by a ReadWriteOnce
PVC (`bookstack-config`). The default RollingUpdate strategy creates
the new pod before terminating the old one, and the two pods cannot
share the RWO volume — so every rollout deadlocks on
`Multi-Attach error for volume`.

Switching to `strategy: Recreate` terminates the old pod first,
releases the PVC, and then starts the new pod. Brief downtime is
expected on rollout, but rollouts actually complete.

Discovered while rolling out the `/status` probe fix (previous commit).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 12:01:35 +00:00
cto-agent
8a80068b95 fix(bookstack): point probes at /status instead of / (DEV-528)
BookStack's `/` returns 302 → /login, which caused Kubernetes to
raise ProbeWarning events on the readiness (and liveness) probes.
`/status` is BookStack's built-in monitoring endpoint: it returns
200 without a redirect and is designed for health checks.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 11:56:37 +00:00
CTO Agent
f5a0982222 fix(argocd): label argocd-oidc-secret so Argo CD reads its keys (DEV-523)
Argo CD only substitutes $secret:key references from Secrets carrying
the label app.kubernetes.io/part-of: argocd. Without it, the server
logs "secret key does not exist in secret" and renders placeholders
verbatim into the OIDC redirect URL.

Adding the label to the SealedSecret template ensures sealed-secrets-
controller re-produces the Secret with the label on every restore, so
OIDC keeps working after DR / re-seal.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 10:37:01 +00:00
CTO Agent
d1641c9047 fix(argocd): reference argocd-oidc-secret directly for OIDC (DEV-523)
Post-migration verify uncovered that Pocket ID OIDC login was broken:
the argocd-server was rendering $oidc.pocketid.clientId literally into
the authorize URL instead of substituting the client id.

The Helm chart's default $key syntax looks in argocd-secret, but our
OIDC keys are held only in the SealedSecret-backed argocd-oidc-secret
Opaque secret. Pre-migration argocd-secret happened to contain byte-for-
byte copies of those keys (legacy install), which is why it worked.

Switch to Argo CD's $secret:key form so the values point at the correct
secret without duplicating sealed material into argocd-secret.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 10:33:50 +00:00
CTO Agent
e8ab9d4674 fix(argocd): server.insecure=true for TLS-terminating ingress (DEV-522)
Argo CD server was returning HTTP 307 redirect loop behind Traefik
because it was serving HTTPS on port 8080 while the ingress terminates
TLS. Setting server.insecure so the server speaks plain HTTP internally.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 10:20:55 +00:00
CTO Agent
447ff93ccf fix(argocd): add forgejo internal SSH host key to values (DEV-522)
Argo CD repo-server needs the forgejo.forgejo.svc.cluster.local host key
in its known_hosts to clone the git repos over SSH. Adding it to
configs.ssh.extraHosts so the helm-managed ssh-known-hosts-cm renders
the entry from git and does not drift when the argocd Application syncs.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 10:18:04 +00:00
CTO Agent
43610fb16d feat(argocd): switch to community Helm chart (DEV-519)
Replace the vendored ~33k-line apps/argocd/argocd-install.yaml with the
argoproj/argo-helm chart argo-cd 10.4.0 (app v3.5.1). Values live in
apps/argocd/values.yaml; the local kustomize wrapper now only carries the
Traefik ingress and the sealed secrets. The root apps/app-argocd.yaml
Application becomes multi-source (chart + this repo as $values), enables
ServerSideApply + ApplyOutOfSyncOnly, and pins the resources-finalizer
explicitly.

Behavior-equivalent to the previous install: same URL, OIDC (Pocket ID),
argo_admins RBAC mapping, resource.exclusions list, and per-component
memory limits (DEV-281). Ingress is disabled in the chart; ours stays in
kustomize with cert-manager letsencrypt-prod annotations.

README.md updated with the Helm bump procedure. argocd-install.yaml
removed.

Verified locally:
  helm template argocd argo/argo-cd --version 10.4.0 \
    -f apps/argocd/values.yaml -n argocd
  # renders 34k lines, image: quay.io/argoproj/argocd:v3.5.1
  kustomize build apps/argocd/
  # renders 1 Ingress + 3 SealedSecrets, no errors

Refs: DEV-521, plan DEV-519 §3, §8.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 08:32:23 +00:00
CTO Agent
89cf3094d0 chore(monitoring): add restic alerts for forgejo repo (DEV-514)
New freshness (BackupForgejoStale, 28h) and integrity
(BackupForgejoCheckFailed) rules matching the sibling
loki/grafana/k8s-resources/prometheus alerts.

Metrics come from backup_forgejo_{timestamp_seconds,check_status,success}
emitted by apps/forgejo/forgejo-backup-cronjob.yaml.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 01:16:38 +00:00
CTO Agent
585d6ee114 feat(forgejo-backup): migrate to pg_dump -> restic -> Hetzner S3 (DEV-514)
Replaces the legacy volume-based backup that wrote pg_dump files to
`platform-backup-data` (RWO PVC on hcloud volume 106575948). That
volume was deleted externally on 2026-08-17; instead of re-provisioning
the legacy pattern, this migrates forgejo to the restic->S3 pipeline
already in use for loki/grafana/prometheus (DEV-485..DEV-492).

- initContainer runs pg_dump -F c into emptyDir (5 GiB cap)
- main container: restic backup /source, tag forgejo, repo
  s3:${S3_ENDPOINT}/${S3_BUCKET}/restic/forgejo
- forget/prune (7d/4w/6m), check --read-data-subset=5%
- textfile-collector metrics -> backup_forgejo.prom (DEV-494 wiring)
- SealedSecret forgejo-s3-backup mirrors monitoring-s3-backup fields
- retire platform-backup-data PVC manifest

Verified manually 2026-08-23 01:11 UTC:
  forgejo-backup-manual-1787447497 -> snapshot a961a473, repo 192 KiB.
Orphan PV/PVC (`platform-backup-data`, `pvc-de59ae9c-...`) deleted.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-23 01:16:38 +00:00
CTO
8dffba2c52 fix(forgejo-runner): pin to nodes with docker installed (DEV-498)
Rolling OS updates removed docker.io from all workers, breaking the
runner's hostPath mount of /var/run/docker.sock (type=Socket check).

- Install docker.io on all 5 worker nodes and enable the systemd unit.
- Label the nodes basicstack.de/docker=true so the dependency is explicit.
- Add nodeSelector to the Deployment so scheduling fails fast on
  non-labeled nodes instead of hanging in ContainerCreating.
- Document the node prerequisite in the README.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 20:15:16 +00:00
CTO Agent
810d22f0a6 feat(monitoring): prometheus-backup → restic client-side encrypted (DEV-492)
Migrates the last plaintext-at-rest monitoring backup off rclone-sync
onto the same restic pipeline as loki/grafana/k8s-resources. Closes
the Hetzner Object Storage gap flagged in DEV-482 — no SSE-S3/SSE-KMS
means every object we push must be encrypted client-side.

- apps/monitoring/prometheus-backup-cronjob.yaml: pin the image to the
  Harbor mirror `harbor.basicstack.de/library/restic:0.17.3` (DEV-493)
  so the pull path matches the sibling CronJobs.
- docs/monitoring/restic-restore.md: append the DEV-492 drill entry —
  cold restic init, 8.017 GiB → 2.441 GiB stored, `restic check`
  clean, restore of `latest` into a scratch namespace, then
  `promtool tsdb list` + `promtool tsdb analyze` against all 19
  blocks (0 failed). Confirms the compaction-race mitigation
  (`--exclude wal/*`/`chunks_head/*` + accept exit 3) does not leave
  a corrupt snapshot. Also documents the DEV-492 Prometheus repo in
  the layout table + per-repo restore commands.
- apps/monitoring/README.md: point the restore section at all four
  repos and cite DEV-488 (loki/k8s) + DEV-492 (prometheus) drill
  entries.

Definition of done (DEV-492):
- backup ships client-side-encrypted to Hetzner: ✅ (restic init
  succeeded on the manual run; snapshot 71420465 written)
- restore drill promtool tsdb analyze clean: ✅ (19/19 blocks OK)
- OBSERVABILITY_BASELINE.md + apps/monitoring/README.md updated: ✅
- restic-password ownership documented: ✅ (README shared-SealedSecret
  table already covered all four repos after this change)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 16:29:22 +00:00
CTO Agent
ef62dde67c feat(observability): wire node-exporter textfile collector for backup metrics (DEV-494)
Turns the four monitoring backup CronJobs' .prom output into scrapeable
Prometheus series so the DEV-490 alerts finally evaluate against live data.

- apps/observability/patches/node-exporter-textfile-collector.yaml:
  strategic-merge patch on the kube-prometheus-stack node-exporter DS
  that adds `--collector.textfile.directory=/host/textfile_collector`
  and mounts `/var/lib/node_exporter/textfile_collector` read-only.
  Chart isn't tracked in ArgoCD, so we keep the patch under version
  control and re-apply after any helm upgrade (see patches/README.md).
- apps/monitoring/backup-{loki,grafana,k8s-resources,prometheus}-*-cronjob.yaml:
  swap `emptyDir` /metrics for a `hostPath` on the same directory
  (`DirectoryOrCreate`). Write via `.prom.tmp` + `mv` so node-exporter
  never reads a truncated sample.
- apps/monitoring/backup-restic-alerts.yaml: `time() - max(...) > 28h`
  for all four freshness alerts so a stale `.prom` left on a node the
  job has since left does not fire the freshness pager.
- apps/monitoring/README.md: drop the "once wired" caveat; document
  the on-node directory, atomic write, cross-node staleness rationale.

Verified end-to-end with a synthetic `kubectl create job
--from=cronjob/backup-k8s-resources`: pod ran on k3s-worker-4,
.prom file materialized in /var/lib/node_exporter/textfile_collector,
and Prometheus returned all four metric families
(`backup_k8s_resources_success=1`, `..._timestamp_seconds`,
`..._check_status=0`, `restic_repo_size_bytes{repo="k8s-resources"}=13692516`).
`time() - max(backup_k8s_resources_timestamp_seconds)` returned ~84s
against a fresh run. `promtool check rules` + `promtool test rules`
still pass (9 rules, 5 scenarios).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 16:21:56 +00:00
CTO Agent
b1680431c6 feat(monitoring): pull restic image from Harbor mirror (DEV-493)
Point the three monitoring restic CronJobs at the Harbor-hosted
mirror at harbor.basicstack.de/library/restic:0.17.3 so the backup
pipeline is not gated by Docker Hub throttling or upstream retag.

- Mirrored docker.io/restic/restic:0.17.3 into the public
  `library` project via a one-shot crane copy Job. Digest in
  Harbor matches the upstream index (sha256:8f5a62b4…).
- Updated the three CronJobs
  (backup-loki-restic, backup-grafana-restic,
  backup-k8s-resources) to pull from Harbor.
- Documented the tag-bump procedure in
  docs/monitoring/restic-restore.md (§ Tag-bump procedure) and
  pointed the restore-drill pod at the same Harbor image so the
  restore round-trip stays image-consistent with the writers.

Refs: [DEV-493](/DEV/issues/DEV-493), parent [DEV-482](/DEV/issues/DEV-482).

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 16:20:06 +00:00
CTO Agent
550d1276f7 feat(monitoring): Prometheus backup-freshness + restic alerts (DEV-490)
- New PrometheusRule apps/monitoring/backup-restic-alerts.yaml with
  seven warning-level rules:
    * BackupLokiStale / BackupGrafanaStale / BackupK8sResourcesStale
      -> time() - backup_<kind>_timestamp_seconds > 28h
    * BackupLokiCheckFailed / BackupGrafanaCheckFailed /
      BackupK8sResourcesCheckFailed -> backup_<kind>_check_status != 0
    * ResticRepoOversize -> restic_repo_size_bytes > 20 GiB (per-repo)
- Emit restic_repo_size_bytes{repo="<kind>"} from all three restic
  CronJobs (loki, grafana, k8s-resources) via `restic stats --json
  --mode raw-data` so ResticRepoOversize has data to match once the
  textfile-collector scrape path is wired.
- Companion promtool unit test backup-restic-alerts.test.yaml with
  five scenarios (fresh/stale, check pass/fail, oversize) -- verified
  locally with promtool 2.53.1: SUCCESS.
- README.md: document the three restic CronJobs, the shared
  SealedSecret keys (access-key/secret-key/endpoint/bucket/
  restic-password), the emitted textfile metrics, and the alert list.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 16:07:55 +00:00
CTO Agent
0c1c05fc50 feat(monitoring): retire legacy backup-volumes CronJob + backup-storage PVC (DEV-489)
The restic pipeline (loki/grafana/k8s-resources) is proven end-to-end
after the DEV-488 restore drill, so the legacy rsync/tar pipeline into
the 100 Gi local-path `backup-storage` PVC is removed.

- Delete `apps/monitoring/backup-volumes-cronjob.yaml`.
- Remove the DEV-483 bridge `nodeSelector: k3s-worker-2` from
  `apps/monitoring/loki-deployment.yaml`. Loki's data protection now
  runs via `backup-loki-restic`, which follows the pod via podAffinity
  regardless of which node the RWO CSI volume attaches on. The
  `Recreate` rollout strategy stays — it is unrelated (avoids the
  attach-deadlock during a rollout). Resolves the RWO/nodeSelector
  attach race that was blocking DEV-478 weekly OS updates.
- Update `apps/monitoring/README.md` to drop the `backup-volumes`
  section, link the restic restore runbook, and record the pin
  removal.
- Clean stale coexistence comments in the restic/prometheus CronJob
  manifests now that the legacy job is gone.

Cluster-side (already applied out-of-band, since these manifests are
`kubectl apply`-based, not Argo-managed):
- `kubectl -n monitoring delete cronjob backup-volumes` -> NotFound.
- `kubectl -n monitoring delete pvc backup-storage` -> gone; local-path
  PV `pvc-d0db0ba9-8f89-4f66-9e65-d573ebe1085a` reclaimed automatically
  (Delete policy). Two stale pre-DEV-487 `backup-k8s-resources` job
  pods that still referenced the PVC were deleted to release the
  `pvc-protection` finalizer.
- `kubectl -n monitoring apply -f loki-deployment.yaml` -> Recreate
  rollout, new pod Ready in ~60s, no nodeSelector on the new spec.
- No `VolumeAttachment` for the retired PV.
- Restic CronJobs (`backup-loki-restic`, `backup-grafana-restic`,
  `backup-k8s-resources`, `prometheus-backup`) intact.

Pre-delete snapshots retained on k3s-cp-1 under
`/root/dev489-snapshots-20260816T155411Z/` for post-mortem.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 15:58:43 +00:00
CTO Agent
e434fa751b feat(monitoring): backup-k8s-resources → restic --stdin to Hetzner S3 (DEV-487)
Rewrites the backup-k8s-resources CronJob to drop the local-path tarball
path and stream a YAML dump through `restic backup --stdin` into
`hetzner-s3:${BUCKET}/restic/k8s-resources`. Uses a two-container pattern:
init `alpine/k8s:1.29.4` dumps into an emptyDir, main `restic/restic:0.17.3`
reads that file on stdin. Retains `serviceAccountName: backup-sa`, drops
the `k3s-worker-2` nodeSelector (no more local-path pin), matches sibling
loki/grafana jobs on retention, `restic check --read-data-subset=5%`, and
textfile metrics (`backup_k8s_resources_success` / `_timestamp_seconds` /
`_check_status`).

Deployed in parallel with the legacy `backup-volumes` CronJob — the
`backup-storage` PVC keeps serving `backup-volumes` until DEV-482 step 6
(restore drill).

Server-side dry-run validated on the k3s control plane.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-08-16 15:39:06 +00:00