Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1901,8 +1901,8 @@ jobs:
shellcheck scripts/tests/test-production-authorization-rules.sh
bash scripts/tests/test-production-authorization-rules.sh

- name: 🚦 Validate Actual Budget user-namespace pilot
# Exercise the active component and its rollback on both the PR and the
- name: 🚦 Validate Actual Budget user namespaces
# Exercise the permanent setting and its rollback on both the PR and the
# speculative merge-group revision before deploy-prod can publish it.
timeout-minutes: 5
run: |
Expand Down
17 changes: 10 additions & 7 deletions k8s/bases/apps/actual-budget/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ app with an optional self-hosted sync server.

## User namespaces

Production runs a [stateful user-namespace pilot](../../../providers/hetzner/apps/actual-budget/components/user-namespaces/README.md)
whose runtime verdict is recorded on
[#3604](https://github.com/devantler-tech/platform/issues/3604). Its component
pairs the pod setting with the existing admission policy and preserves the
database volume, both containers' identities and mounts, and the single-writer
deployment strategy. The guide covers offline validation, the runtime verdict,
and rollback. Local app opt-in keeps the shared defaults.
In production the pod runs in its own user namespace (`hostUsers: false`). The
stateful pilot passed on
[#3604](https://github.com/devantler-tech/platform/issues/3604) and is permanent
since [#3605](https://github.com/devantler-tech/platform/issues/3605). Two
production patches pair the pod setting with the existing admission policy. They
preserve the database volume, both containers' identities and mounts, and the
single-writer deployment strategy. The
[production guide](../../../providers/hetzner/apps/actual-budget/README.md)
covers offline validation and rollback. Local app opt-in keeps the shared
defaults.

## Bank sync (Enable Banking)

Expand Down
61 changes: 61 additions & 0 deletions k8s/providers/hetzner/apps/actual-budget/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Actual Budget: user namespaces in production

In production the Actual Budget pod runs in its own user namespace
(`hostUsers: false`): container IDs map to unprivileged host IDs. Actual Budget
is the one stateful workload that does this. The setting started as a pilot.
Its runtime verdict passed on
[#3604](https://github.com/devantler-tech/platform/issues/3604), and it was made
permanent in [#3605](https://github.com/devantler-tech/platform/issues/3605).

Two patches in [`../kustomization.yaml`](../kustomization.yaml) make up the
setting, and they always change together:

- [`patches/enable-user-namespaces.yaml`](patches/enable-user-namespaces.yaml)
labels the namespace for the existing Kyverno `add-user-namespaces` rule.
- [`patches/disable-host-users.yaml`](patches/disable-host-users.yaml) appends a
Helm post-renderer that sets `hostUsers: false` on the chart's Deployment. It
goes last, so the base strategy, startup-probe and OIDC post-renderers keep
their order. The chart's Deployment has no `metadata.namespace`, so the inner
target names it exactly. Helm scopes it to the release namespace.

The sync server and the `enablebanking-seed` sidecar keep their user and group
IDs, their mounts and their shared database volume. The Deployment keeps its
single-replica `Recreate` strategy, so each rollout of this pod brings a brief
outage. The local Docker provider keeps the shared defaults.

## Validate without deploying

From the repository root, run:

```bash
bash scripts/tests/test-actual-budget-user-namespaces.sh
ksail workload validate
ksail --config ksail.prod.yaml workload validate
```

The regression needs `kubectl`, Helm, `jq` and `yq` v4. It needs no cluster and
no secrets. It checks the following:

- Production references each patch exactly once, and the pod patch targets only
the Actual Budget HelmRelease.
- It downloads the chart version the HelmRelease pins, then renders it with
every post-renderer, with and without the two patches. Only `hostUsers` may
differ in the chart's workload, and the PVC and both containers must stay
identical.
- The local overlay is unaffected, and removing both patches and adding them
back reproduces the committed render exactly.

[`scripts/read-actual-budget-userns-maps.sh`](../../../../scripts/read-actual-budget-userns-maps.sh)
reads the live id maps of both containers. It is read-only.

## Roll back without touching storage

Remove both patch entries from `../kustomization.yaml` in one pull request.
That removes the namespace label and the appended post-renderer together. After
deployment, verify that both are gone, the replacement pod is Ready, the same
claim and volume are still bound, and budget access and the sidecar's
reconciliation work again.

Do not delete or recreate the PVC, reinstall the chart, change file ownership
recursively, or remove the namespace label on its own. None of those reverses
this setting.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Run the Actual Budget pod in its own user namespace (`hostUsers: false`).
# The stateful pilot passed (#3604) and is permanent since #3605. Appended as
# the last Helm post-renderer so the base strategy, probe and OIDC renderers
# survive in order. The chart's Deployment has no metadata.namespace, so the
# inner target uses its exact name; Helm scopes it to the release namespace.
- op: add
path: /spec/postRenderers/-
value:
kustomize:
patches:
- target:
group: apps
version: v1
kind: Deployment
name: actual-budget-actualbudget
patch: |-
- op: add
path: /spec/template/spec/hostUsers
value: false
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Opt Actual Budget into the label-gated `add-user-namespaces` Kyverno rule,
# paired with the authored `hostUsers: false` in disable-host-users.yaml. The
# stateful pilot passed (#3604) and is permanent since #3605. The label lives
# only in the Hetzner overlay, so the local Docker provider keeps the defaults.
---
apiVersion: v1
kind: Namespace
metadata:
name: actual-budget
labels:
pod-security.devantler.tech/user-namespaces: enabled
18 changes: 13 additions & 5 deletions k8s/providers/hetzner/apps/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,6 @@ components:
# bases/components/helmrelease-drift-detection.
- ../../../bases/components/helmrelease-drift-detection
- ../../../bases/components/helmrelease-flux-defaults
# Stateful user-namespace pilot, active for verification (#3604). The runtime
# verdict is recorded on #3604; rollback removes this one reference, retaining
# data. Making the setting permanent is a separate change (#3605).
- actual-budget/components/user-namespaces
# Strategic-merge patch files self-identify (apiVersion/kind/name/namespace), so
# they need only a path — no redundant `target:` selector. JSON6902 patches have
# no embedded identity and keep an explicit target.
Expand Down Expand Up @@ -95,8 +91,20 @@ patches:
# User namespaces (hostUsers: false) — enabled for the proven prod apps. These
# namespace-label patches opt proven-stateless workloads into the
# add-security-context Kyverno rule. Local/CI stays out, and so do stateful
# workloads apart from the Actual Budget pilot component above; in mixed
# workloads apart from Actual Budget below; in mixed
# namespaces (umami) the rule itself excludes the
# CNPG database pods via their cnpg.io/cluster label.
- path: whoami/patches/enable-user-namespaces.yaml
- path: homepage/patches/enable-user-namespaces.yaml
# Actual Budget is the one stateful workload in its own user namespace. Its
# pilot passed (#3604) and is permanent since #3605: the namespace label pairs
# with the authored hostUsers: false on its pod. Rolling back removes both
# entries together and leaves the volume and its data untouched.
- path: actual-budget/patches/enable-user-namespaces.yaml
- target:
group: helm.toolkit.fluxcd.io
version: v2
kind: HelmRelease
name: actual-budget
namespace: actual-budget
path: actual-budget/patches/disable-host-users.yaml
Loading
Loading