From 4dd1d7fa82c427b2ee9b43e53767949d148a3ee8 Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Wed, 12 Aug 2026 21:14:18 +0200 Subject: [PATCH 1/9] Release terraform-backend-mongodb 1.3.0 Chart 0.3.0 packages application 1.3.0, which adds a failed-attempt lockout, a short-lived credential cache, a dummy-hash verify that closes a username enumeration oracle, and an authentication failure log carrying the caller's address. Expose the settings those need under authentication and network. network.trustAllProxies defaults to true because it is the only setting that works behind an ingress whose address is not known in advance, and because the alternative, every request looking like the ingress, makes every caller share one lockout bucket and turns the lockout into the denial of service it exists to prevent. Document the choice in the chart README, along with the ingress annotation for rate limiting, which belongs at the edge rather than in the application, and the fact that TLS termination is not optional when the credential is replayed on every request. Add AGENTS.md and CLAUDE.md, recording the layout, the release rule that a chart change needs a version bump in the same commit, and the writing style used across these repositories. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_011th2BLVHbkVe4NvmUZ2rCb --- AGENTS.md | 83 +++++++++++++++++++ CLAUDE.md | 1 + charts/terraform-backend-mongodb/Chart.yaml | 4 +- charts/terraform-backend-mongodb/README.md | 37 +++++++++ .../templates/deployment.yaml | 16 ++++ charts/terraform-backend-mongodb/values.yaml | 28 ++++++- 6 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..b5b4b92 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,83 @@ +# AGENTS.md + +Guidance for coding agents working in this repository. + +## What this is + +A collection of Helm charts published as a chart repository at `https://devpro.github.io/helm-charts`, alongside a VitePress documentation site. +Charts cover both custom applications maintained by devpro and third-party applications packaged for convenience. + +## Repository layout + +- **`charts/`**: the source of every chart, one directory per chart. + [Helm Chart Releaser](https://github.com/helm/chart-releaser) supports neither multiple chart directories nor nested levels, so every chart lives directly under `charts/` and nowhere else. +- **`docs/`**: the VitePress site, with application guides in `docs/application-guides` and custom chart documentation in `docs/custom-charts`. +- **`samples/`**: example manifests and values. +- **`scripts/`**: helper scripts, including `add_helm_repo.sh` which adds the dependency repositories the charts need. + +## Common commands + +```bash +helm lint charts/ # lint one chart +helm template myapp charts/ -f values.yaml --namespace myns # render the manifests +helm upgrade --install myapp charts/ -f values.yaml --namespace myns --create-namespace +kube-linter lint charts --config .kube-linter.yaml # the check CI runs +npm run docs:dev # serve the documentation site +``` + +## Releasing a chart + +A chart is published when its `version` in `Chart.yaml` changes on `main`, so a change to any chart file needs a version bump in the same commit or nothing is released. +`version` is the chart's own version and `appVersion` is the version of the packaged application: they move independently. +Where a chart pins an image tag in `values.yaml`, that tag and `appVersion` must be updated together. + +## Conventions + +- Chart values are documented with `# --` comments above the key, which is the helm-docs annotation form. +- A chart that packages a devpro application keeps its values structure close to the application's configuration keys, so that a setting can be traced from `values.yaml` to the environment variable it becomes. +- CI runs `kube-linter` over `charts/` with `.kube-linter.yaml`, and `ct lint` over the charts changed against `main`. + Exclusions belong in `.kube-linter.yaml` with a comment saying why. +- Markdown and YAML are linted in CI through `.markdownlint-cli2.yaml` and `.yamllint.yaml`. +- Do not run markdownlint: linting is run manually by the maintainer and in CI. +- Files carrying real values for a local deployment are named `values.mine.yaml` and are gitignored, so they are never edited or committed. + +## Writing style + +These rules apply to Markdown, YAML comments, chart templates, code comments, commit messages, and any prose in scripts. + +**One sentence per line.** +A line break only ever happens at the end of a sentence, and a sentence is never wrapped across two lines. +There is no maximum line length: screens are wide, and the 80 character convention is not used here. +This applies to comments as much as to prose, so a long comment sentence stays on a single line rather than continuing onto a second comment line. +Wrapping is handled by the editor, not by hard newlines. + +**Never use the em dash (`—`) or the en dash (`–`).** +Use a colon when introducing an explanation, a comma when joining clauses, or a full stop and a new sentence. +This applies to prose, code comments, table cells, and error message strings. + +**Never use the second person.** +No "you", no "your", not even in placeholders such as ``, which should read ``. +The documentation describes the repository, it does not address a reader. +Write "the working tree", not "your working tree". + +**Other conventions.** +Use `ini` as the fence language for `.properties` blocks, never `properties`. +Prefer `>` over `→` when describing UI navigation, for example **Project Settings > Quality Gate**. + +Existing files predate these rules and break them in places. +That is not a reason to add more, and not a reason to reformat prose that a change does not otherwise touch. + +### Scripts + +Shell scripts are named in `snake_case`, which is the standard for bash: `add_helm_repo.sh`, not `add-helm-repo.sh`. + +Scripts must be committed with the executable bit set. +A script committed as `100644` fails on a fresh clone even though it works locally: + +```bash +git update-index --chmod=+x path/to/script.sh +``` + +### Target platform + +Target platform is Linux with Docker, including WSL2, and `bash`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/charts/terraform-backend-mongodb/Chart.yaml b/charts/terraform-backend-mongodb/Chart.yaml index 0514096..e54f42d 100644 --- a/charts/terraform-backend-mongodb/Chart.yaml +++ b/charts/terraform-backend-mongodb/Chart.yaml @@ -2,8 +2,8 @@ name: terraform-backend-mongodb description: Helm chart for Terraform Backend MongoDB type: application -version: 0.2.0 -appVersion: "1.2.3" +version: 0.3.0 +appVersion: "1.3.0" home: https://github.com/devpro/terraform-backend-mongodb sources: - https://github.com/devpro/terraform-backend-mongodb diff --git a/charts/terraform-backend-mongodb/README.md b/charts/terraform-backend-mongodb/README.md index fabc2e7..3c9914d 100644 --- a/charts/terraform-backend-mongodb/README.md +++ b/charts/terraform-backend-mongodb/README.md @@ -86,6 +86,43 @@ If none of the three is configured, or `mongodb.enabled=true` is set without `mo the chart fails at render/install time with a clear error instead of deploying a Pod that can't reach a database. +## Authentication hardening + +Application version 1.3.0 adds a failed-attempt lockout, a short-lived credential cache and an authentication failure log that carries the caller's address. +The defaults under `authentication` in [values.yaml](values.yaml) suit most deployments and need no change. + +The setting that does need a decision is `network.trustAllProxies`, because the other two depend on it. + +Terraform's `http` backend sends a Basic credential on every single request, so the server verifies the same password over and over and every failed guess costs it a BCrypt verify. +The lockout puts a ceiling on that, and it is scoped to the username and the caller's source address together rather than to the username alone: a lockout on the username alone would let anybody on the internet lock the real operator out of the backend. + +Behind an ingress, an application that does not trust the proxy sees the proxy's address on every request. +Every caller then shares one lockout bucket, which turns the protection into the denial of service it was designed to avoid. +`network.trustAllProxies` therefore defaults to `true`, which is safe where the pod is reachable through the ingress alone. + +Where other workloads can reach the service directly and are not trusted, name the ingress instead: + +```yaml +network: + trustAllProxies: false + knownNetworks: + - 10.42.0.0/16 +``` + +Rate limiting by source address belongs at the ingress rather than in the application, since the edge rejects a request before any CPU is spent on it and holds one counter across every replica. +The annotation depends on the ingress controller, for example with ingress-nginx: + +```yaml +ingress: + enabled: true + annotations: + nginx.ingress.kubernetes.io/limit-rps: "10" +``` + +TLS termination is not optional for this application. +The password is replayed on every request, so a single plaintext hop exposes the state of every workspace in the tenant. +Note that `skip_cert_verification` in a Terraform backend block defeats the protection it appears to configure. + ## Going further Check the [contribution guide](CONTRIBUTING.md). diff --git a/charts/terraform-backend-mongodb/templates/deployment.yaml b/charts/terraform-backend-mongodb/templates/deployment.yaml index cb136cb..33d1bcd 100644 --- a/charts/terraform-backend-mongodb/templates/deployment.yaml +++ b/charts/terraform-backend-mongodb/templates/deployment.yaml @@ -105,6 +105,22 @@ spec: value: {{ $.Values.dotnet.httpsRedirectionEnabled | quote }} - name: Features__IsScalarEnabled value: {{ $.Values.dotnet.enableScalar | quote }} + - name: Authentication__CredentialCacheSeconds + value: {{ $.Values.authentication.credentialCacheSeconds | quote }} + - name: Authentication__MaxFailedAttempts + value: {{ $.Values.authentication.maxFailedAttempts | quote }} + - name: Authentication__LockoutSeconds + value: {{ $.Values.authentication.lockoutSeconds | quote }} + - name: Network__TrustAllProxies + value: {{ $.Values.network.trustAllProxies | quote }} + {{- range $index, $proxy := $.Values.network.knownProxies }} + - name: Network__KnownProxies__{{ $index }} + value: {{ $proxy | quote }} + {{- end }} + {{- range $index, $cidr := $.Values.network.knownNetworks }} + - name: Network__KnownNetworks__{{ $index }} + value: {{ $cidr | quote }} + {{- end }} {{- if $.Values.dotnet.openTelemetry.enabled }} - name: OTEL_SERVICE_NAME value: {{ $.Values.dotnet.openTelemetry.serviceName | default (include "terraform-backend-mongodb.fullname" (dict "app" . "root" $)) | quote }} diff --git a/charts/terraform-backend-mongodb/values.yaml b/charts/terraform-backend-mongodb/values.yaml index 1baf622..4c2d969 100644 --- a/charts/terraform-backend-mongodb/values.yaml +++ b/charts/terraform-backend-mongodb/values.yaml @@ -4,7 +4,7 @@ webapi: name: tfbackend image: docker.io/devprofr/terraform-backend-mongodb # images are hosted on DockerHub: https://hub.docker.com/r/devprofr/terraform-backend-mongodb/tags - tag: "1.2.3" # pins to Chart.yaml appVersion - avoid floating tags (e.g. latest) in Production + tag: "1.3.0" # pins to Chart.yaml appVersion - avoid floating tags (e.g. latest) in Production replicaCount: 1 port: 80 containerPort: 8080 @@ -68,6 +68,32 @@ dotnet: endpoint: "" protocol: "http/protobuf" +# Authentication hardening, available from application version 1.3.0. +# Terraform's http backend sends a Basic credential on every single request and supports no token, so the same password is verified over and over and every failed guess costs the server a BCrypt verify. +authentication: + # -- How long a verified credential is reused without running BCrypt again, which is what stops a busy terraform apply paying around 139 ms of CPU on every request. + # Set to 0 to disable the cache. + credentialCacheSeconds: 60 + # -- Consecutive failures before that username is refused from that source address. + maxFailedAttempts: 10 + # -- How long the refusal lasts. + lockoutSeconds: 300 + +# Which reverse proxies the application believes when it reads the caller's address. +# This is a prerequisite for the lockout above and for the authentication failure log, not a detail: behind an ingress an unconfigured application sees the proxy's address on every request, so every caller shares one lockout bucket and an attacker on the internet can lock the real operator out of the backend. +network: + # -- Accept X-Forwarded-For from any caller. + # It defaults to true because it is the only setting that works behind an ingress whose address is not known in advance, and because the alternative, every request looking like the ingress, is worse than the risk it avoids. + # It is safe where the pod is reachable through the ingress alone. + # Where other workloads can reach the service directly and are not trusted, set this to false and list the ingress below instead. + trustAllProxies: true + # -- Specific proxy addresses to trust, read only when trustAllProxies is false. + knownProxies: [] + # - 10.0.0.7 + # -- Networks to trust in CIDR notation, read only when trustAllProxies is false. + knownNetworks: [] + # - 10.42.0.0/16 + # optional: image registry credentials, e.g. for a private mirror of the image above imagePullSecrets: [] # - name: my-registry-secret From c4362e88d73dada894dd156e01ff91ee959b333e Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Sun, 16 Aug 2026 18:07:19 +0200 Subject: [PATCH 2/9] Update docs --- .markdownlint-cli2.yaml | 1 + charts/sidelab/README.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.markdownlint-cli2.yaml b/.markdownlint-cli2.yaml index bc94ac6..690956f 100644 --- a/.markdownlint-cli2.yaml +++ b/.markdownlint-cli2.yaml @@ -1,6 +1,7 @@ gitignore: true ignores: - "**/node_modules/**" + - "CLAUDE.md" config: default: true MD013: diff --git a/charts/sidelab/README.md b/charts/sidelab/README.md index 7008e73..d282887 100644 --- a/charts/sidelab/README.md +++ b/charts/sidelab/README.md @@ -31,7 +31,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for ready-to-use `values.mine.yaml` snipp ## Running several launcher replicas -The launcher is stateless — sessions, lab-token replay protection and expiry claims are coordinated through the database, and running lab Pods are reconciled from it on startup — so it scales horizontally once nothing is stored on the Pod itself: +The launcher is stateless: sessions, lab-token replay protection and expiry claims are coordinated through the database, and running lab Pods are reconciled from it on startup. +So it scales horizontally once nothing is stored on the Pod itself: ```yaml replicaCount: 3 From 568942447ab3e99d7f6af68549f7892e8d1a8cd5 Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Fri, 21 Aug 2026 23:13:36 +0200 Subject: [PATCH 3/9] Add param --- charts/keeptrack/templates/deployment.yaml | 4 ++++ charts/keeptrack/values.yaml | 2 ++ 2 files changed, 6 insertions(+) diff --git a/charts/keeptrack/templates/deployment.yaml b/charts/keeptrack/templates/deployment.yaml index c4892b8..1229eb9 100644 --- a/charts/keeptrack/templates/deployment.yaml +++ b/charts/keeptrack/templates/deployment.yaml @@ -215,6 +215,10 @@ spec: - name: ReferenceData__BookProvider value: {{ .referenceData.bookProvider | quote }} {{- end }} + {{- if .referenceData.videoGameProvider }} + - name: ReferenceData__VideoGameProvider + value: {{ .referenceData.videoGameProvider | quote }} + {{- end }} {{- if .referenceData.tmdb.apiKeySecretKeyRef }} - name: Tmdb__ApiKey valueFrom: diff --git a/charts/keeptrack/values.yaml b/charts/keeptrack/values.yaml index e258494..32b7a06 100644 --- a/charts/keeptrack/values.yaml +++ b/charts/keeptrack/values.yaml @@ -74,6 +74,8 @@ webapi: referenceData: # overrides the app's own default ("OpenLibrary") - see WebApi's AppConfiguration.BookReferenceProvider bookProvider: "" + # overrides the app's own default ("IGDB") - see WebApi's AppConfiguration.VideoGameProvider + videoGameProvider: "" # periodic background re-sync of stale reference data (every 24h); matches the app's own default isSyncEnabled: true tmdb: From be3b8feb96dc6f2e884fa532d25cfd2545cffc98 Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Thu, 3 Sep 2026 22:01:38 +0200 Subject: [PATCH 4/9] Update keeptrack to remove Features__IsWebSocketsOnlyEnabled --- charts/keeptrack/templates/deployment.yaml | 2 -- 1 file changed, 2 deletions(-) diff --git a/charts/keeptrack/templates/deployment.yaml b/charts/keeptrack/templates/deployment.yaml index 1229eb9..2f368fb 100644 --- a/charts/keeptrack/templates/deployment.yaml +++ b/charts/keeptrack/templates/deployment.yaml @@ -104,8 +104,6 @@ spec: {{- if eq .role "frontend" }} - name: WebApi__BaseUrl value: "http://{{ $.Values.webapi.name }}" - - name: Features__IsWebSocketsOnlyEnabled - value: {{ .webSocketsOnly | quote }} {{- if .dataProtection.enabled }} {{- if .dataProtection.connectionStringSecretKeyRef }} - name: DataProtection__MongoDb__ConnectionString From fef3867f85dda70369fe12f57fa3f18da1103c5b Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Thu, 3 Sep 2026 22:01:56 +0200 Subject: [PATCH 5/9] Update tfbackend doc --- docs/custom-charts/terraform-backend-mongodb.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/custom-charts/terraform-backend-mongodb.md b/docs/custom-charts/terraform-backend-mongodb.md index 8605032..e760889 100644 --- a/docs/custom-charts/terraform-backend-mongodb.md +++ b/docs/custom-charts/terraform-backend-mongodb.md @@ -1,4 +1,4 @@ -# Terraform Backend MongoDB +# Terraform Backend MongoDB Let's see how to deploy [Terraform Backend MongoDB](https://github.com/devpro/terraform-backend-mongodb) on a Kubernetes cluster. @@ -20,8 +20,9 @@ Create the `values.yaml` file to override [default parameters](https://github.co ```yaml [Application] dotnet: environment: Development - enableSwagger: true - enableOpenTelemetry: false + enableScalar: true + openTelemetry: + enabled: false ``` ```yaml [Ingress] @@ -57,7 +58,7 @@ helm upgrade --install tfbackend devpro/terraform-backend-mongodb -f values.yaml ## Optional checks -If enabled, open the Swagger page from the browser (`/swagger`). +If enabled, open the Scalar page from the browser (`/scalar`). Forward MongoDB port for local access: From 0f2715f8aef3c4dc1312bbbcba500d14533360fd Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Mon, 7 Sep 2026 23:56:49 +0200 Subject: [PATCH 6/9] Update setup-go action version --- .github/workflows/pkg.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pkg.yml b/.github/workflows/pkg.yml index 47810b4..e432f7e 100644 --- a/.github/workflows/pkg.yml +++ b/.github/workflows/pkg.yml @@ -73,7 +73,7 @@ jobs: # name: docs # path: docs - name: Setup Go environment - uses: actions/setup-go@v6 + uses: actions/setup-go@v7 - name: Generate index.html run: | go install github.com/halkeye/helm-repo-html@v0.2.1 From d8b88ca72933579bb281994e6287251f8ff6d708 Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Fri, 11 Sep 2026 18:01:25 +0200 Subject: [PATCH 7/9] Update sidelab --- charts/sidelab/CONTRIBUTING.md | 63 +++++++++++++++----- charts/sidelab/README.md | 76 +++++++++---------------- charts/sidelab/templates/_helpers.tpl | 42 +++++++------- charts/sidelab/templates/configmap.yaml | 7 ++- 4 files changed, 102 insertions(+), 86 deletions(-) diff --git a/charts/sidelab/CONTRIBUTING.md b/charts/sidelab/CONTRIBUTING.md index 53098ea..ef7e99c 100644 --- a/charts/sidelab/CONTRIBUTING.md +++ b/charts/sidelab/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contribution guide -## Update chart dependencies +## Updating chart dependencies Add the Bitnami Helm repository: @@ -23,7 +23,7 @@ Update `Chart.lock` and fetch the archive into `charts/` (gitignored, rebuilt fr helm dependency update . ``` -## Review the generated manifest +## Reviewing the generated manifest ```bash helm template sidelab . -f values.yaml -f values.mine.yaml --namespace sidelab --debug > temp.yaml @@ -31,10 +31,40 @@ helm template sidelab . -f values.yaml -f values.mine.yaml --namespace sidelab - Useful sanity checks on the output: -- the same value should never appear twice from two different sources (that's what the top-level `domain` value and reusing `ingress.className`/`ingress.tls`/the cert-manager annotation for lab sessions are for), -- and `--set database.backend=mongo` with nothing else set, or `--set ingress.enabled=true` with no `domain`/`ingress.host`, should fail fast with a clear `fail` message rather than render something broken. +- the same value should never appear twice from two different sources + (that's what the top-level `domain` value and reusing `ingress.className`/`ingress.tls`/the cert-manager annotation for lab sessions are for) +- having `--set database.backend=mongo` with nothing else set, or `--set ingress.enabled=true` with no `domain`/`ingress.host`, + should fail fast with a clear `fail` message rather than render something broken -## Validate on MicroK8s +## Running several launcher replicas + +The launcher is stateless: sessions, lab-token replay protection and expiry claims are coordinated through the database, and running lab Pods are reconciled from it on startup. +So it scales horizontally once nothing is stored on the Pod itself: + +```yaml +replicaCount: 3 + +database: + backend: mongo + mongo: + url: mongodb://:@mongo:27017/sidelab + +persistence: + enabled: false # nothing left to persist locally; a ReadWriteOnce PVC would pin the launcher to one Pod + +launcher: + labAccess: ingress # nodeport lab URLs are built from the answering node's IP + +extraEnv: + - name: TRUST_PROXY + value: "1" # the login throttle is per-replica; without this, per-IP throttling sees only the proxy +``` + +That combination also switches the Deployment from `Recreate` to `RollingUpdate`, so upgrades no longer drop the dashboard, and makes `autoscaling.enabled` usable. +Asking for more than one replica while `database.backend=sqlite` or a non-`ReadWriteMany` PVC is enabled fails at render time with the fix in the message, +rather than silently corrupting a database. + +## Validating on MicroK8s This is the fastest local loop: no cloud cluster, no DNS, no cert-manager required for the base cases below. @@ -63,7 +93,7 @@ From the `sidelab` repo: ```bash docker compose --profile build-only build -docker tag sidelab-app:latest sidelab-app:v1 # avoid :latest (see docs/wsl-microk8s.md) +docker tag sidelab-app:latest sidelab-app:v1 docker save sidelab-launcher:latest | microk8s images import - docker save sidelab-app:v1 | microk8s images import - ``` @@ -84,7 +114,8 @@ A `microk8s stop`/`start` cycle (or one triggered by a snap refresh) can clear t ### 2. Known-good `values.mine.yaml` combinations -Point `image.repository`/`launcher.labImage` at the locally-imported tags in every scenario below (MicroK8s doesn't need the registry path prefix, plain `sidelab-launcher`/`sidelab-app` resolve to what was imported). +Point `image.repository`/`launcher.labImage` at the locally-imported tags in every scenario below +(MicroK8s doesn't need the registry path prefix, plain `sidelab-launcher`/`sidelab-app` resolve to what was imported). #### a. Zero-config default: SQLite + NodePort @@ -229,8 +260,10 @@ Two settings in there are easy to get wrong, and both fail quietly: - **`ingress.tls.enabled: true` despite no cert-manager and no certificate.** The launcher picks the scheme of the URL it hands learners by reading back whether the lab `Ingress` it just created has a `tls:` block. - Leave this false and every session opens at `http://.labs.example.com`: the single-use `labToken` crosses the public internet in a plaintext query string, and the lab session cookie isn't marked `Secure`. - The per-session secret it names never materializes, and that's harmless here: the controller falls back to its default certificate, which nothing ever sees, because the upstream hop reaches it over HTTP. + Leave this false and every session opens at `http://.labs.example.com`: + the single-use `labToken` crosses the public internet in a plaintext query string, and the lab session cookie isn't marked `Secure`. + The per-session secret it names never materializes, and that's harmless here: the controller falls back to its default certificate, which nothing ever sees, + because the upstream hop reaches it over HTTP. - **`ingress.enabled: false` does not disable `ingress.className`/`ingress.tls`.** Both are deliberately reused for lab sessions, so you configure the ingress controller once rather than twice. One thing to confirm on the certificate side, since `launcher.labDomain` decides it: @@ -264,8 +297,10 @@ extraEnv: ``` The two required lines are `database.backend: mongo` and `persistence.enabled: false`. -Both are enforced: asking for `replicaCount > 1` (or `autoscaling.enabled` with a `maxReplicas > 1`) on SQLite, or with a PVC that isn't `ReadWriteMany`, fails the render with the fix in the message. -SQLite is a single-writer local file, and a `ReadWriteOnce` PVC can't be mounted by two Pods to begin with — a chart that let either through would hand you a silently split or corrupted database. +Both are enforced: asking for `replicaCount > 1` (or `autoscaling.enabled` with a `maxReplicas > 1`) on SQLite, or with a PVC that isn't `ReadWriteMany`, +fails the render with the fix in the message. +SQLite is a single-writer local file, and a `ReadWriteOnce` PVC can't be mounted by two Pods to begin with. +A chart that let either through would hand you a silently split or corrupted database. Meeting both conditions is also what flips the `Deployment` strategy from `Recreate` to `RollingUpdate`, so upgrades stop dropping the dashboard. Verify with: @@ -275,8 +310,10 @@ helm template sidelab . -f values.mine.yaml | grep -A2 'strategy:' Two more, not enforced because neither is wrong enough to block on: -- **`launcher.labAccess: ingress`.** NodePort lab URLs are built from `K8S_NODE_IP`, the IP of the node running the replica that served the request, so learners get URLs pointing at different nodes depending on which replica answered. -- **`TRUST_PROXY`.** The failed-login throttle is in-process, so `replicaCount: 3` already gives an attacker three times the budget (per-username throttling still applies on each replica). Without `TRUST_PROXY` the per-IP half stops working entirely, every request appearing to come from the ingress controller. +- **`launcher.labAccess: ingress`.** NodePort lab URLs are built from `K8S_NODE_IP`, the IP of the node running the replica that served the request, + so learners get URLs pointing at different nodes depending on which replica answered. +- **`TRUST_PROXY`.** The failed-login throttle is in-process, so `replicaCount: 3` already gives an attacker three times the budget (per-username throttling still applies on each replica). + Without `TRUST_PROXY` the per-IP half stops working entirely, every request appearing to come from the ingress controller. ### 3. Check everything came up diff --git a/charts/sidelab/README.md b/charts/sidelab/README.md index d282887..aeffadd 100644 --- a/charts/sidelab/README.md +++ b/charts/sidelab/README.md @@ -1,60 +1,37 @@ # Sidelab Helm Chart -Helm chart for [Sidelab](https://github.com/devpro-training/sidelab) — a self-hosted interactive lab platform. -Deploys the Sidelab launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces. +Helm chart for Sidelab, a self-hosted interactive lab platform. -Supports SQLite (zero-dependency, quick look/demo) or MongoDB as the database backend, and NodePort or Ingress for exposing lab sessions. -`values.yaml` is the source of truth for every option. -It's fully commented, including Traefik/cert-manager/Let's Encrypt examples. +In a nutshell: -## Quick Start - -Add the chart repository: - -```bash -helm repo add devpro https://devpro.github.io/helm-charts -helm repo update -``` - -Create the `values.yaml` file to override [default values](values.yaml). - -Install the application: +- Deploys the launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces +- Supports SQLite (zero-dependency, quick look/demo) or MongoDB as the database backend, and NodePort or Ingress for exposing lab sessions -```bash -helm upgrade --install sidelab devpro/sidelab -f values.yaml --namespace sidelab --create-namespace -``` - -One value is always required: `image.repository`. -The chart is public but the sidelab images are not published to a public registry. -Everything else has working defaults: SQLite + NodePort + auto-generated secrets, no external dependencies. -See [CONTRIBUTING.md](CONTRIBUTING.md) for ready-to-use `values.mine.yaml` snippets covering the other use cases (MongoDB, Ingress + cert-manager, the bundled MongoDB demo chart). - -## Running several launcher replicas - -The launcher is stateless: sessions, lab-token replay protection and expiry claims are coordinated through the database, and running lab Pods are reconciled from it on startup. -So it scales horizontally once nothing is stored on the Pod itself: +## Quick Start -```yaml -replicaCount: 3 +1. Add the chart repository -database: - backend: mongo - mongo: - url: mongodb://user:pass@mongo:27017/sidelab + ```bash + helm repo add devpro https://devpro.github.io/helm-charts + helm repo update + ``` -persistence: - enabled: false # nothing left to persist locally; a ReadWriteOnce PVC would pin the launcher to one Pod +2. Create the `values.yaml` file to override [default values](values.yaml) -launcher: - labAccess: ingress # nodeport lab URLs are built from the answering node's IP + > [!IMPORTANT] + > One value is always required: `image.repository`. + > + > `values.yaml` is the source of truth for every option. + > It's fully commented, including Traefik/cert-manager/Let's Encrypt examples. + > + > `CONTRIBUTING` contains known-good `values.yaml` combinations for every use case. -extraEnv: - - name: TRUST_PROXY - value: "1" # the login throttle is per-replica; without this, per-IP throttling sees only the proxy -``` +3. Install the application: -That combination also switches the Deployment from `Recreate` to `RollingUpdate`, so upgrades no longer drop the dashboard, and makes `autoscaling.enabled` usable. -Asking for more than one replica while `database.backend=sqlite` or a non-`ReadWriteMany` PVC is enabled fails at render time with the fix in the message, rather than silently corrupting a database. + ```bash + helm upgrade --install sidelab devpro/sidelab -f values.yaml \ + --namespace sidelab --create-namespace + ``` ## Uninstall @@ -63,9 +40,10 @@ helm uninstall sidelab -n sidelab kubectl delete namespace sidelab ``` -The auto-generated `-auth` Secret (admin password) and the data PVC are deleted along with the namespace. -Back up `database.mongo.url`'s target or the PVC first if data must be kept. +> [!NOTE] +> The auto-generated `-auth` Secret (admin password) and the data PVC are deleted along with the namespace. +> Back up `database.mongo.url`'s target or the PVC first if data must be kept. ## Going further -Check the [contribution guide](CONTRIBUTING.md) for MicroK8s validation steps and known-good `values.yaml` combinations for every use case. +Check the [contribution guide](CONTRIBUTING.md) for additional information. diff --git a/charts/sidelab/templates/_helpers.tpl b/charts/sidelab/templates/_helpers.tpl index adeb3ef..709ae18 100644 --- a/charts/sidelab/templates/_helpers.tpl +++ b/charts/sidelab/templates/_helpers.tpl @@ -94,14 +94,16 @@ Ingress template ConfigMap name (for lab session Ingress resources). {{- end }} {{/* -Image tag — falls back to .Chart.AppVersion. +Image tag. +Falls back to .Chart.AppVersion. */}} {{- define "sidelab.imageTag" -}} {{- .Values.image.tag | default .Chart.AppVersion }} {{- end }} {{/* -Dashboard hostname. ingress.host wins; otherwise derived from the shared top-level `domain` value as "sidelab.". +Dashboard hostname. +ingress.host wins; otherwise derived from the shared top-level `domain` value as "sidelab.". Empty if neither is set. */}} {{- define "sidelab.ingressHost" -}} @@ -113,7 +115,8 @@ Empty if neither is set. {{- end }} {{/* -Lab session wildcard domain. launcher.labDomain wins; otherwise derived from the shared top-level `domain` value as "labs.". +Lab session wildcard domain. +launcher.labDomain wins; otherwise derived from the shared top-level `domain` value as "labs.". Empty if neither is set. */}} {{- define "sidelab.labDomain" -}} @@ -126,9 +129,8 @@ Empty if neither is set. {{/* MongoDB connection string. -database.mongo.url wins; -otherwise, if the bundled mongodb subchart is enabled, derived from its default standalone Service name -("-mongodb", per the alias in Chart.yaml and the Bitnami chart's own naming convention) and mongodb.auth.rootPassword. +database.mongo.url wins; otherwise, if the bundled mongodb subchart is enabled, +derived from its default standalone Service name ("-mongodb", per the alias in Chart.yaml and the Bitnami chart's own naming convention) and mongodb.auth.rootPassword. Only called from secret.yaml when database.mongo.existingSecret is not set. */}} {{- define "sidelab.mongoUrl" -}} @@ -140,9 +142,9 @@ Only called from secret.yaml when database.mongo.existingSecret is not set. {{- end }} {{/* -JWT secret — auth.jwtSecret wins; otherwise reused from the existing Secret on upgrade (via lookup), or freshly generated on first install. -`lookup` returns nothing outside a real cluster (e.g. `helm template`), so offline rendering always generates a fresh value — -expected, and harmless for a dry render. +JWT secret. +auth.jwtSecret wins; otherwise reused from the existing Secret on upgrade (via lookup), or freshly generated on first install. +`lookup` returns nothing outside a real cluster (e.g. `helm template`), so offline rendering always generates a fresh value (expected, and harmless for a dry render). Only called from secret.yaml when auth.existingSecret is not set. */}} {{- define "sidelab.jwtSecret" -}} @@ -159,7 +161,8 @@ Only called from secret.yaml when auth.existingSecret is not set. {{- end }} {{/* -Admin password — same reuse-then-generate pattern as sidelab.jwtSecret above. +Admin password. +Same reuse-then-generate pattern as sidelab.jwtSecret above. Only called from secret.yaml when auth.existingSecret is not set. */}} {{- define "sidelab.adminPassword" -}} @@ -176,19 +179,16 @@ Only called from secret.yaml when auth.existingSecret is not set. {{- end }} {{/* -Whether this configuration owns local state that two launcher Pods must never -share. Returns "true" or "" (empty is false in an `if`). +Whether this configuration owns local state that two launcher Pods must never share. +Returns "true" or "" (empty is false in an `if`). Two independent reasons, and only these two: - - the sqlite backend is a single-writer file — unsafe between processes even on - a shared volume, and worse without one (each Pod would silently get its own - empty database on its own emptyDir); - - a PVC that isn't ReadWriteMany simply cannot be mounted by two Pods. - -Nothing else in the launcher is per-Pod state: sessions, lab-token replay -protection and expiry claims all live in the database, and lab Pods are -reconciled from it on startup. So with database.backend=mongo and -persistence.enabled=false this is empty, and the chart rolls and scales normally. + - The sqlite backend is a single-writer file. + unsafe between processes even on a shared volume, and worse without one (each Pod would silently get its own empty database on its own emptyDir); + - A PVC that isn't ReadWriteMany simply cannot be mounted by two Pods. + +Nothing else in the launcher is per-Pod state: sessions, lab-token replay protection and expiry claims all live in the database, +and lab Pods are reconciled from it on startup. So with database.backend=mongo and persistence.enabled=false this is empty, and the chart rolls and scales normally. Drives both the Deployment update strategy and the replica validation below. */}} diff --git a/charts/sidelab/templates/configmap.yaml b/charts/sidelab/templates/configmap.yaml index 30f8557..a9f20d5 100644 --- a/charts/sidelab/templates/configmap.yaml +++ b/charts/sidelab/templates/configmap.yaml @@ -38,9 +38,10 @@ data: --- {{- if and (eq .Values.launcher.labAccess "ingress") (not .Values.launcher.ingressTemplateConfigMap) }} # Default Ingress template for lab session Pods, modeled on samples/manifests/ingress-template-microk8s.yaml in the sidelab repo. -# It includes the /websockify path needed by VNC-enabled labs, and the sidelab.io/session + sidelab.io/tenant labels the launcher relies on -# to reconcile orphaned lab resources after a restart (deleteByLabel fallback). -# Reuses the dashboard's own ingress.className / ingress.tls / cert-manager annotation from above, so you don't configure the same ingress controller and issuer twice. +# It includes the /websockify path needed by VNC-enabled labs, +# and the sidelab.io/session + sidelab.io/tenant labels the launcher relies on to reconcile orphaned lab resources after a restart (deleteByLabel fallback). +# Reuses the dashboard's own ingress.className / ingress.tls / cert-manager annotation from above, +# so you don't configure the same ingress controller and issuer twice. # Override launcher.ingressTemplateConfigMap for a different shape entirely. apiVersion: v1 kind: ConfigMap From 15cb430b954f1076a5558e3467d0596a32f489ed Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Fri, 11 Sep 2026 18:20:16 +0200 Subject: [PATCH 8/9] Bump version for charts --- charts/keeptrack/Chart.yaml | 2 +- charts/sidelab/Chart.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/keeptrack/Chart.yaml b/charts/keeptrack/Chart.yaml index 323d470..83a044d 100644 --- a/charts/keeptrack/Chart.yaml +++ b/charts/keeptrack/Chart.yaml @@ -2,7 +2,7 @@ name: keeptrack description: Helm chart for Keeptrack type: application -version: 0.2.5 +version: 0.2.6 appVersion: "2.4.1" home: https://github.com/devpro/keeptrack sources: diff --git a/charts/sidelab/Chart.yaml b/charts/sidelab/Chart.yaml index 2c4fd83..9409a2a 100644 --- a/charts/sidelab/Chart.yaml +++ b/charts/sidelab/Chart.yaml @@ -4,7 +4,7 @@ description: > Self-hosted interactive lab platform. Deploys the Sidelab launcher, which manages lab sessions as ephemeral Kubernetes Pods inside tenant-scoped namespaces. type: application -version: 0.1.5 +version: 0.1.6 appVersion: "0.1.0" keywords: - sidelab From ed343b793786ca541eecd7c3a934eccf500c96de Mon Sep 17 00:00:00 2001 From: Bertrand THOMAS Date: Mon, 21 Sep 2026 09:58:12 +0200 Subject: [PATCH 9/9] Remove comments --- charts/keeptrack/templates/_helpers.tpl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/charts/keeptrack/templates/_helpers.tpl b/charts/keeptrack/templates/_helpers.tpl index 4c7acf5..fa9494a 100644 --- a/charts/keeptrack/templates/_helpers.tpl +++ b/charts/keeptrack/templates/_helpers.tpl @@ -1,11 +1,4 @@ -{{/* -Recommended Kubernetes labels (https://helm.sh/docs/chart_best_practices/labels/), added on top of -the app-specific `app` / `app.kubernetes.io/name` labels every template already sets directly. -These are informational only - never add them to a Deployment's spec.selector.matchLabels, which -is immutable and would break `helm upgrade` for existing releases. -Call with a dict: (dict "app" . "root" $) where "." is the current blazorapp/webapi values entry. -*/}} -{{- define "keeptrack.commonLabels" -}} +{{- define "keeptrack.commonLabels" -}} helm.sh/chart: {{ printf "%s-%s" .root.Chart.Name .root.Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} app.kubernetes.io/version: {{ .root.Chart.AppVersion | quote }} app.kubernetes.io/managed-by: {{ .root.Release.Service }}