Commit Graph
5873 Commits
Author SHA1 Message Date
Myasnikov Daniil fc5dc66c61 fix(networking): isolate webhook policies from namespace churn
Move pod denials to source-side Cilium policies so namespace churn
cannot regenerate fail-closed webhook endpoints. Keep the external
world deny as a static target rule.

Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-09-02 19:15:44 +05:00
myasnikovdaniil 67b4e23ca2 test(tests): cut the noise in unit and e2e job logs (#3973)
## What this PR does

Unit test job log is 238,794 lines and 31MB, and 233,444 of those are
`set -x` trace from `hack/cozytest.sh`. The one failing assertion sits
at line 238,573. Nothing in that stream is needed there, `run_one`
already tees raw output to `$log` and the fail handler dumps all of it
for whichever test fails, so dropping the live stream loses output of
passing tests only.

`COZYTEST_TRACE` defaults to 1, so e2e call sites in
`packages/core/testing/Makefile` keep the live stream. For a long e2e
suite that stream is the only progress signal, and the only record at
all when a step timeout or a dead runner kills the job before the fail
handler runs. `bats-unit-tests` sets 0. Running the runner by hand stays
verbose, that is the case where a human is actually watching.

Second thing, previous-instance log capture now orders by restart
recency. Container cap (`COZY_PREVLOG_MAX=12`) is spent in namespace
order and "unrelated" meant only "in another namespace", time was never
a key. So a container that restarted once during bring-up competed on
equal footing with one that crash-looped during the test, and won
whenever the pod list reached it first. Two measured e2e jobs spent 1034
and 1545 lines on `virt-api` and `cdi-apiserver`, both restarted once at
install time by `Unable to create certwatcher`, hours before any app
suite ran. On a live stand 178 containers carry a restart and their
timestamps span 24 days, so the cap binds hard enough for order to
decide everything.

This orders, it does not filter. Install-time restart is the explanation
when install is what broke, and the capture has no reference point for
"when the test began" that would make a cutoff honest. Namespace stays
primary key, because a stale restart in the namespace under test beats a
fresh one three namespaces away. So it fixes cap displacement, not raw
volume of a single run.

`lastState.terminated.finishedAt` joins the row. The guard on it is
three deep and the innermost one is load bearing, which I got wrong
first time round and review caught: `finishedAt` is a metav1.Time behind
`json:",omitempty"`, omitempty cannot omit a struct, and the zero value
marshals to `null`. The kubelet does synthesize a terminated state with
no time, ContainerStatusUnknown on a pod deleted mid-run, which also
bumps restartCount and so clears the restart filter. kubectl renders
that leaf as the literal `<no value>`, and `<` sorts above every digit,
so under the reverse sort that row landed FIRST and displaced the most
recent real restart. Exactly the inversion the ordering exists to
prevent, plus a header reading `previous instance ended <no value>`.
Fixed at both layers because they fail independently: the template
guards the leaf so the string cannot be produced, and the sort
normalises any field that is not shaped like a timestamp to empty so the
ordering holds even if some other producer emits one.

Worth naming how that slipped through. I probed the template against a
live cluster, saw no `<no value>`, and read it as the guard being
correct. It only meant no container on that cluster was in that state.

Reading the field also fixes a latent bug it would otherwise have
caused: `read -r ... restarts` folds a trailing field into the last
variable, so the dump header would print
`restarts=1|2026-08-22T09:06:00Z`. Now it names the time, which is the
first thing telling a reader whether the dump predates the test.

### What quiet mode keeps and what it drops

Two things are dropped, and the second one came out of review rather
than out of me.

First, xtrace and stdout of tests that pass. You cannot diff a passing
test's trace against the failing one inside the same job log, and a
warning printed by a test that passed is gone.

Second, a test that is KILLED rather than failed loses its trace as
well. The fail handler runs only after the pipeline returns, so a hang
reaped by the job timeout never dumps anything, and quiet mode has by
then dropped the live stream. Verbose mode never noticed, because the
trace was already printed. Measured: verbose leaves the line the test
blocked on, quiet leaves the `╭` and nothing else. So this one is a real
regression on the unit lane, not a pre-existing gap.

An INT/TERM trap is the obvious repair and it does not work. POSIX sh
defers a trap until the current foreground command returns, so
installing one stops the runner dying on the signal at all: a suite hung
on `sleep 30` took 30s to exit with the trap against 2s under the
default disposition. GitHub escalates INT, then TERM, then KILL, so the
deferred handler gets SIGKILLed before it ever runs, and the grace
period goes on waiting for the hang. Built it, measured it, reverted it,
and the measurement is in the comment now so the next reader does not
repeat it. Making it work needs the test pipeline moved into the
background with `wait`, which is the interruptible one, and that is
surgery on the path every suite in this repo takes for a partial trace
in a rare case.

What is left for that case is the escape hatch below, which for this
lane costs 48s and no cluster. The same argument is why e2e keeps the
stream on: re-running there is an hour and a live cluster, so the record
has to be written as it goes.

Everything else stays. The `╭`/`╰` pair per test with elapsed time, so
which tests ran, in what order and how long each took is all still
there. Full trace, stdout and stderr of the failing test, same bytes as
before, because `run_one` tees to `$log` and the fail handler dumps it,
and that path is untouched. Anything printed outside `run_one` as well,
file level code, `.bats` parse errors, and the `--- running <file> ---`
markers. A hang still shows up as `╭` with no matching `╰`. And `make
unit-tests` stops at the first failing test anyway, so there was never
more than one dump to read.

Measured on a suite that is actually red (`hack/ghcr-mirror_test.bats`
fails on my machine because it reads a live cluster): verbose 1472
lines, quiet 63 lines, and the post-failure dump is 23 lines and
byte-identical in both modes.

Getting the trace back, both paths checked:

```
COZYTEST_TRACE=1 make unit-tests     # env prefix, 29200 trace lines
make unit-tests COZYTEST_TRACE=1     # make cmdline, same
hack/cozytest.sh hack/foo.bats       # by hand, verbose by default
```

This lane needs no cluster and runs locally in 48s, so anyone who wants
the trace just reruns it. That is exactly why the same argument does not
extend to e2e, and why the default stays 1 there.

Nothing else consumed that stream. Unit lane stdout goes only to the job
log, `cozyreport.sh` folds in crust-gather snapshots and the trap writes
those to disk independently, and the unit lane produces none of them.

### Verification

Measured on this PR's own run: unit job went from 238,794 lines and 31MB
with 233,444 trace lines, to 5,720 lines and 580KB with 0 trace lines
and 1548 tests OK. Those two totals are not strictly like for like, the
old run was red and this one is green, so the honest comparator is
233,444 trace lines down to 0.

Quiet output is byte-for-byte the non-trace subset of verbose, and the
fail handler dump is unaffected (checked with a deliberately failing
suite). Verbose output unchanged against main, byte-identical sorted on
`hack/bats-no-exit-trap.bats`. Other suites are not self-stable run to
run (temp names, epochs, stdout/stderr interleaving) so that is the
ceiling on that check, not a finding.

Eight tests on the capture and seven on the switch. Every fix is
mutation proven: reverting the template leaf guard, removing the sort
key normalisation, dropping the read variable, dropping `-s`, silencing
an e2e recipe, removing the fail handler dump, flipping the default, and
making the switch a no-op are each caught. Three of those guards did not
bite when first written, which review also found: both stability
fixtures were pre-sorted ascending, which is what sort's last-resort
whole-line compare reproduces with no `-s` at all, and no test fed a
six-field row through the whole script, so deleting the read variable
left the suite green at 39/39. Whole unit lane, 83 files, 1503 passed
and 1 failed. The failure is `hack/ghcr-mirror_test.bats`, it reads a
live cluster and fails identically under main's own runner.

### Screenshots

Not a UI change.

### Downstream repositories

Walked the trigger map file by file against the diff. Nothing under
`hack/` is moved or renamed, no make target changes what it does
(`bats-unit-tests` runs the same files to the same verdict, only
quieter), and `hack/package.mk`, `hack/common-envs.mk`,
`hack/update-crd.sh` and `hack/e2e-prepare-cluster.bats` are untouched,
which is what ccp, external-apps-example, talm and ansible-cozystack
couple to. No package `values.yaml`, `values.schema.json`, CRD,
namespace, label, annotation or metric name is involved.

- [x] No downstream repository is affected by this change

### Release note

```release-note
test(tests): bats unit test trace is now opt-in via COZYTEST_TRACE and quiet by default in `make unit-tests`, and the e2e previous-instance log capture now picks the most recently restarted containers first
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Improvements**
- Test runs can suppress live trace output for passing tests while
preserving full diagnostics for failures.
  - Trace visibility can be customized through the test configuration.
- Previous-instance logs are prioritized by the most recent container
termination within the preferred namespace.
  - Captured log headers now show when the previous instance ended.
  - Undated logs are retained and placed after dated entries.

- **Tests**
- Added coverage for trace controls, failure handling, and recent-log
ordering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
v1.7.0-alpha.1
2026-09-02 10:44:23 +05:00
myasnikovdaniil 5688b3c66a fix(ingress-nginx): re-pin to the segfault-free v1.11.5 rebuild (#4012)
Re-pins the ingress controller to the rebuilt `v1.11.5`. The digest
pinned before this segfaults.

`pb.c` in lua-protobuf violates strict aliasing and gcc 14 at `-O2` acts
on it, so `pb.so` in the previously pinned build makes nginx `cache
manager` and `cache loader` children segfault continuously, tens per
second and it never stops. Workers are untouched, so the pod stays
`Ready` with `restarts=0` and nothing surfaces until master itself dies,
which is how it got here unnoticed. Fixed in
cozystack/ingress-nginx-with-protobuf-exporter#7, tag `v1.11.5`
re-pointed at that merge and both images rebuilt.

Verified against the published image, not a local build:

* `luajit -e 'require "protoc"'` exits 0, was 139.
* 25 second nginx run on a real generated config: 0 `signal 11`, was
105. 0 emerg, all four lua modules load.
* `luarocks` and `gcc` are gone from the runtime image now, so `apk` no
longer upgrades musl underneath the nginx the base image already built.

The exporter is re-pinned too, because one tag builds both images and
the rebuild published a new exporter digest. Its source did not change.

`release-1.5` and `release-1.6` pin v1.11.2 and were never affected, so
nothing to backport.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated container image versions for the ingress controller and
protobuf exporter to newer verified builds.
  * Existing image tags and configuration remain unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-02 10:30:11 +05:00
Myasnikov Daniil 127cc2ce9a fix(ingress-nginx): re-pin to the segfault-free v1.11.5 rebuild
The v1.11.5 tag in cozystack/ingress-nginx-with-protobuf-exporter now points
at the merge of its #7, which builds lua-protobuf with -fno-strict-aliasing.
The digest pinned here was the build before that, whose pb.so makes nginx's
cache manager and cache loader children segfault continuously: tens per
second, indefinitely. The workers are unaffected, so the pod stays Ready with
no restarts and nothing surfaces until the master itself segfaults, which is
how this reached main unnoticed.

Verified against the published image rather than a local build. The one-line
repro, luajit -e 'require "protoc"', exits 0 where it exited 139, and a 25
second nginx run on a generated ingress-nginx config produces no signal 11
where the pinned build produced 105.

The exporter is re-pinned alongside it because one tag builds both images, so
the rebuild published a new exporter digest too. Its source did not change.

release-1.5 and release-1.6 pin v1.11.2 and were never affected.

Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-09-01 18:28:04 +05:00
scooby87 dc2e427262 docs(etcd): note that affinity is not applied to existing members (#3964)
## What this PR does

Documentation follow-up to #3962. Both reviewers (CodeRabbit and
@androndo/@IvanHunters) noted that the new `affinity` value is not
applied retroactively: the etcd-operator sets `spec.affinity` only when
it creates or replaces a member Pod and never reschedules running
members. On a cluster that already has co-located members (the exact
failure #3962 targets) the default soft rule — or a newly set hard rule
— takes effect only as each member is replaced.

This adds that caveat to the `affinity` parameter documentation in
`values.yaml` and regenerates `values.schema.json`, `README.md`, and the
embedded `openAPISchema` in `etcd-rd`. No template or behaviour change;
`make test` passes (20/20).

The other inline note (the soft podAntiAffinity default overlaps the
soft `topologySpreadConstraints`) was explicitly flagged as "fine to
keep as-is", so no code change is made for it.

> Note: this PR is stacked on `fix/etcd-soft-antiaffinity` (#3962) and
will retarget to `main` once that merges.

### Screenshots

n/a — no UI changes.

### Downstream repositories

Doc-only change to an app chart (regenerated artifacts). No package
added/renamed/removed, `kind`/`plural` unchanged, no installer
values/assets/variants touched. No downstream follow-up required.

- [x] No downstream repository is affected by this change

### Release note

```release-note
NONE
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Documentation**
* Clarified that etcd affinity rules apply when member Pods are created
or replaced.
* Noted that running members are not rescheduled retroactively; the rule
takes effect as each member is replaced.
* Updated the guidance consistently across configuration documentation
and schema descriptions.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 13:56:18 +03:00
Aleksei Sviridkin c8d073fff7 fix(api): drop caller-supplied internal.cozystack.io labels in tenantsecret writes (#3599)
## What this PR does

Labels and annotations under `internal.cozystack.io/` belong to the
platform. The lineage webhook stamps
`internal.cozystack.io/tenantresource` to decide whether a Secret is
visible through this API at all, and the cacert controller stamps
`internal.cozystack.io/tenant-ca`, which an `ApplicationDefinition`
selects on in `spec.secrets` to hand a trust anchor to the tenant. The
registry copied every caller-supplied key onto the backing Secret as is,
so anyone with write access to `tenantsecrets` could plant either marker
on a Secret of their choice, or strip one to take an object out of the
platform's view.

`tenantToSecret` is the only place the write verbs go through, PATCH
included, because `rest.Patcher` is `Getter`+`Updater` and the endpoint
installer serves PATCH out of `Update`. It now skips keys with that
prefix. The update target starts as a copy of the stored Secret, so a
platform key cannot be removed either. The guard is on the whole prefix,
so a platform key added later is covered without anyone touching this
code again.

Reads are unchanged. A tenant can see the platform's verdicts, it just
cannot write them. Keys are dropped silently instead of rejected,
otherwise a GET, edit and PUT round trip would break `kubectl edit` for
no gain.

Nothing writes through this API today. Every `tenantsecrets` grant in
`cozystack-basics` is `get`/`list`/`watch`, and the console only lists
and watches. That read-only RBAC is what keeps the hole inert, and it
lives in a different package from the registry that implements the write
verbs, which is why I would rather close it here than keep relying on
it.

Two things I found next to the diff and did not fix. The `Patch` method
on this type is never reached: its signature is the `client-go` shape
and matches no interface the endpoint installer asserts, so no request
lands there. I gave it the same guard instead of leaving the weaker of
two doors, but deleting it is probably the better answer and I would
rather that call was yours. A repair after the write is weaker than the
conversion anyway: the forged key is live between the patch and the
repair, and forcing the object back to a pre-patch snapshot drops a key
that a controller stamped inside that window. Second one, and the reason
none of this is easy to spot: `Update` passes `nil` where the handler
hands the storage the current object, so `hasUID` reports false and the
patch mechanism takes its create-new-object branch. For JSON Patch,
merge patch and strategic merge patch that branch returns `NotFound`, so
all three get a 404 against an existing TenantSecret. Apply is the
exception. `applyPatcher.createNewObject` builds a fresh object of the
kind and applies onto that, so an apply does reach `Update`, carrying
only the fields the caller sent, and the merge in `tenantToSecret` is
what keeps the stored labels. Passing `secretToTenant(previous)` instead
of `nil` is the fix and it belongs in its own change, since it makes the
other three patch types live. Tell me if you want issues for either.

Closes #3464

### Screenshots

Not a UI change.

### Downstream repositories

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

Walked the trigger map against the diff: two Go files under
`pkg/registry/core/tenantsecret/`, no package added, no chart, values
schema, CRD, `ApplicationDefinition` field, namespace, `hack/` target or
release asset touched. Nothing in the map fires.

### Release note

```release-note
fix(api): TenantSecret writes no longer accept caller-supplied `internal.cozystack.io/*` labels and annotations, so a principal with write access to the API can neither plant nor strip the platform markers that decide tenant visibility on the backing Secret.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Strengthened protection of platform-managed metadata during
tenant-secret creation and updates.
* Prevented tenant-provided platform-owned labels and annotations from
overwriting protected values.
  * Preserved standard metadata while hiding sensitive resource markers.
* Improved patch-based update handling and protected metadata
consistency across changes.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 14:53:10 +04:00
Aleksei Sviridkin df20f69e5b chore(ci): re-run with diagnostics enabled
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 13:40:46 +03:00
Aleksei Sviridkin ef4f5b45ab fix(api): drop caller-supplied internal.cozystack.io keys in tenantsecret writes
Labels and annotations under internal.cozystack.io/ are platform
verdicts, not tenant metadata. The lineage admission webhook stamps
internal.cozystack.io/tenantresource to decide whether a Secret is
visible through this API at all, and the cacert controller stamps
internal.cozystack.io/tenant-ca, which an ApplicationDefinition
selects on in spec.secrets to promote a trust anchor to the tenant.
The registry copied every caller-supplied key onto the backing Secret
verbatim, so a principal holding write access to tenantsecrets could
plant either one on a Secret of its choosing, or strip one to take an
object out of the platform's view. Only read-only tenant RBAC, which
lives in a different package, stood between a caller and that path.

tenantToSecret is the choke point every write verb goes through,
PATCH included: rest.Patcher is Getter+Updater, so the endpoint
installer serves PATCH out of Update. It now skips the platform keys,
and since the update target starts as a copy of the stored Secret,
ignoring the caller there also means a platform key cannot be
removed. Guard on the whole prefix rather than on a list of keys, so
a platform key added later is protected without anyone remembering to
come back here.

Delete the Patch method on this type. Its signature matches no
interface the endpoint installer asserts, so no request has ever
reached it, and as a write path it was strictly weaker than the
conversion: the raw patch hit the backing Secret before any guard
ran, leaving a forged key live in etcd -- visible to every controller
and to List -- until a second write put the platform keys back, or
for good when that second write failed. One door is better than two,
and the surviving one commits once.

Keys stay visible on read. A tenant may see the platform's verdicts,
and failing a write that echoes them back would break a
read-modify-write round trip for no gain, so they are dropped
silently instead of rejected.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 13:40:37 +03:00
Aleksei Sviridkin c94a1da5bb feat(mongodb): add TLS support via PSMDB operator-managed certificates (#2692)
## What this PR does

Enables operator-managed TLS for MongoDB and stops handing tenants any
Secret that carries private key material.

- Percona Server for MongoDB serves TLS unconditionally (`mode:
preferTLS`), so this PR drops the misleading `tls.enabled` toggle rather
than pretending the chart controls it.
- The tenant no longer receives the operator's CA Secret
(`<release>-ca-cert`, which holds `ca.key`) or the leaf
(`<release>-ssl`). Both were previously reachable, and either one is a
private key in tenant hands, the CA one is a full trust-chain
compromise.
- The chart declares a `TenantProjection` sentinel naming its CA source
Secret (`<release>-ssl`, the operator-managed leaf, which carries
`ca.crt`), and the ResourceDefinition selects the resulting key-free
`<release>.tenant-ca` projection by the engine-agnostic
`internal.cozystack.io/tenant-ca` label. The CA-extraction controller
reads the sentinel and publishes the projection holding only `ca.crt`.
The tenant gets what it needs to verify the server and nothing else.

### Why the include also carries an exclude list

The trust-anchor selector matches by label with no name constraint, so
any Secret in the namespace that acquired that label would be promoted
with its data copied verbatim. The enumerable dangerous set is therefore
excluded explicitly: the CA, both leaves, the rotation snapshots
(`-ssl-old`, `-ssl-internal-old`, same server keys as the leaves, and
unlike them they carry no owner reference, so an interrupted rotation
can leave them behind after the release is gone), the system-user
passwords, the at-rest encryption key, the keyfile, the object-storage
credentials, and both forms of the operator's internal Secret.

That backstop is bounded, and the comment says so: matching is by exact
name with no globs, so a Secret a future operator version introduces, or
a transient object an issuer creates mid-renewal, stays in scope of the
label-only include. The per-user Secrets cannot be enumerated at all,
since their names carry the username. The primary defence remains that
the controller stamps only its own projection; this closes the
enumerable set, not the whole surface.

### Release note

```release-note
feat(mongodb): stop exposing TLS private key material to tenants and publish the CA as a key-free trust anchor
```
2026-09-01 14:16:24 +04:00
Alexey Artamonov 6fdb5e0840 docs(etcd): note that affinity is not applied to existing members
The operator applies spec.affinity only when it creates or replaces a member
Pod; it does not reschedule running members. Document that on the `affinity`
value so operators do not expect the default (or a newly set rule) to
redistribute already co-located members until each is replaced.

Addresses the non-blocking review notes on #3962.

Signed-off-by: Alexey Artamonov <aleksei.artamonov@aenix.io>
2026-09-01 12:47:46 +03:00
Aleksei Sviridkin edd767ea0f feat(nats): publish ca.crt-only trust anchor to tenants (#3340)
## What this PR does

Converges nats onto the key-free CA trust-anchor contract. A TLS-enabled
nats release renders a `TenantProjection` sentinel naming the chart's
cert-manager CA Secret as its source, and the nats ResourceDefinition
selects the resulting `<release>.tenant-ca` object by label. The
projection is built by the CA extraction controller in
`internal/controller/cacert`, which parses the source certificate and
re-encodes it into a single `ca.crt` key, so a tenant that enables TLS
gets something to verify the server against without ever getting a
private key. No key-bearing Secret is granted to a tenant, by name or by
label.

`packages/apps/nats/templates/certmanager.yaml` is not touched. An
earlier draft of this change stamped publish labels onto the CA
`Certificate`'s `secretTemplate`; the controller that shipped consumes a
sentinel object instead, so the chart now declares its source rather
than marking it. The functional change is one new template and one
`matchLabels` entry in the ResourceDefinition.

The sentinel is gated on the chart's effective TLS state, which is
tri-state: left unset it follows `external`. That gate is the single
divergence from the postgres implementation of the same contract, and it
exists because the nats CA is only issued while TLS resolves true. An
ungated sentinel would sit `Ready=False/SourceNotFound` forever on every
plaintext release.

The CA extraction controller is already on main, so this is live on
merge rather than dormant until a follow-up lands. The
`do-not-merge/hold` label predates that and no longer guards anything.

## Verified against a running cluster

The premise this rests on is that cert-manager populates `ca.crt` on the
Secret of a `selfSigned`, `isCA: true` Certificate. Nothing in this
repository witnesses it: `hack/e2e-chainsaw/` has no nats suite, and
nats appears in no e2e workflow, so every unit test here encodes the
same assumption the chart encodes and the two cannot catch each other
being wrong. I closed it against a live cluster instead, running
cert-manager v1.20.2, the version this repo ships, on two independent
nats releases:

- `nats-<name>-ca`, the chart's CA Secret, holds `ca.crt`, `tls.crt` and
`tls.key`. The declared source key exists.
- `nats-<name>.tenant-ca`, the projection, holds exactly one key,
`ca.crt`.
- The projection's SHA-256 certificate fingerprint equals the source's,
so it carries that CA and not some other certificate.
- The projection carries `internal.cozystack.io/tenant-ca: "true"` and
`internal.cozystack.io/tenantresource: "true"`. The second is stamped by
the lineage webhook, computed from the ResourceDefinition selector this
PR adds, so its presence means the whole chain held.
- `kubectl get tenantsecret nats-<name>-ca` returns NotFound. The
key-bearing Secret is not served through the tenant API.

What is still missing is an automated regression witness for that
premise. Adding one means a new chainsaw suite for nats, which has no
e2e coverage of any kind today, and that is a larger change than this
one.

## What the tests pin

61 helm-unittest cases and 3 bats cases. The sentinel's absence of
`ownerReferences` (an owner would short-circuit the lineage walk before
it reaches the Flux label the attribution depends on), the length of
`spec.projections` as well as its single entry (the controller refuses a
multi-entry sentinel, so a second entry is a silent loss of the anchor
rather than a leak), both implied legs of the tri-state TLS gate, and
`notExists` on either visibility label for both key-bearing
Certificates.

The bats guard covers the two grant surfaces nothing else asserts on.
The ResourceDefinition is rendered, by the `nats-rd` chart inlining
`cozyrds/*`, but that chart has neither a tests directory nor the `test`
target `hack/helm-unit-tests.sh` looks for, and no
`packages/system/*-rd` package does. The dashboard Role is rendered by a
chart that has both, and no suite under `packages/apps/nats/tests/`
asserts on it.

Both are compared whole rather than filtered down to the entries that
mention a Secret, because every such filter admits the spelling it did
not anticipate. A name whitelist passes an added label selector, and
`controller.cert-manager.io/fao` is stamped on both key-bearing Secrets,
so a selector on it is a one-line grant of the CA private key. On the
Role side, a rule with no `resourceNames` names no Secret, a rule with
`resources: ["*"]` names no resource, and a ClusterRole is not a Role,
while all three grant read access to the CA key. Comparing the rendered
output whole needs no filter and leaves no spelling to find.

## Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, file by file. No trigger fires, and no follow-up PR is owed.

`packages/apps/nats/templates/tenant-projection.yaml` is a new template
and `packages/apps/nats/tests/` holds chart tests. No package is added,
renamed or removed; `values.yaml` and `values.schema.json` are
untouched, so neither the hand-written Terraform schema, its version
enums, nor its restated defaults have anything to track. `kind`,
`plural` and `release.prefix` are unchanged, and no output Secret or
Service is renamed, so the provider's concatenated name lookups still
resolve.

`packages/system/nats-rd/cozyrds/nats.yaml` is edited in place rather
than renamed, so the ccp external-app skill's dependency-contract anchor
still resolves. The provider offers no typed access to the new
projection, which is the position it already holds for every Secret it
does not model, and not a break.

`packages/apps/nats/README.md` is covered by the one automated path that
does exist: `nats` is present in the website `Makefile` `APPS` list, so
the docs bot regenerates the managed-apps reference page from this
`README.md` on the next stable tag. This is the silent-skip failure mode
the trigger map warns about, so I checked the list rather than assuming;
an app missing from it gets no page at all, which is what happened to
`opensearch`.

`hack/check-nats-rd-secrets.bats` is a new file, not a move or a rename,
and it changes no make target's contract: `Makefile` globs `hack/*.bats`
into `BATS_UNIT_FILES` automatically. The ccp skills anchor on
`hack/package.mk` and `hack/common-envs.mk`, neither of which is
touched.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

Part of #2814.

### Release note

```release-note
feat(nats): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret
```
2026-09-01 13:11:22 +04:00
Aleksei Sviridkin 65e700486b feat(qdrant): publish ca.crt-only trust anchor to tenants (#3341)
## What this PR does

Converges qdrant onto the key-free CA trust-anchor contract (#2814), the
same way #3340 does it for nats. The chart declares a `TenantProjection`
sentinel naming its cert-manager CA Secret. The CA-extraction controller
reads that declaration and publishes a key-free copy, `ca.crt` and
nothing else, as `<release>.tenant-ca`. The qdrant ResourceDefinition
selects that projection by label so it shows up in the tenant's secret
list. No key-bearing Secret gains a tenant-visible label.

The sentinel is gated on the chart's TLS condition. Engines whose
operator mints a CA unconditionally do not need that, but qdrant only
has a CA while TLS is on, and an ungated sentinel would sit
`Ready=False`/`SourceNotFound` on every plaintext release.

This was held while the CA-extraction controller was still open. That
controller landed in #3407, so the sentinel is honoured as soon as this
merges and the hold is gone.

Tests pin the invariants that otherwise fail silently. The sentinel
carries no `ownerReferences`, so the lineage walk falls through to the
Flux release label. Neither key-bearing Certificate stamps a
tenant-visible label on its Secret. The two objects that decide which
Secrets a tenant may actually read, the dashboard Role with its binding
and the ResourceDefinition, are pinned whole rather than checked for
known-bad names: a rule granting secrets with no `resourceNames` is
unrestricted rather than narrow, and a label selector can reach a
key-bearing Secret while every name in the file stays correct, so
neither adds a name for a probe to notice.

Part of #2814.

### Follow-ups

What this PR does not do, and where each belongs instead.

- nats converges the same contract in #3340 and has no
`dashboard-resourcemap_test.yaml`, so the dashboard Role pin belongs
there too. Tracked in #3701 along with postgres, which already carries
the label selector with no guard behind it.
- The qdrant e2e fixture runs plaintext
(`hack/e2e-chainsaw/qdrant/qdrant.yaml` sets `external: false`), so
nothing in CI walks the path from the sentinel to `tenantsecrets`.
postgres has a `verify-tenant-ca-projection` step to copy.
- `hack/check-qdrant-rd-secrets.bats` is engine-agnostic apart from two
literals, the chart path and the expected Secret set. The third copy of
it is the point to parameterise over `packages/system/*-rd/cozyrds/`
instead of copying again.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, file by file. Nothing needs a manual follow-up, but not for the
reason a values-only walk would give: the website reference page for an
app is regenerated from that package's `README.md`, and this PR does
change it. `qdrant` is in the app list hardcoded in the website
`Makefile`, so the docs bot picks the new section up on the next stable
tag and there is nothing to open by hand. The Terraform provider's
schema is hand-written against `values.schema.json`, which is untouched,
and the provider offers no typed access to the new projection, the same
position it holds for every other Secret it does not model. No package
is added, renamed or removed, and the `cozyrds` file is edited in place
rather than renamed, so the ccp dependency-contract anchor still
resolves.

User-facing documentation for consuming the trust anchor ships with the
chart README, matching #3340.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
feat(qdrant): expose the CA certificate to tenants as a key-free `<release>.tenant-ca` Secret
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Tenant environments can securely access the Qdrant TLS CA certificate
without receiving the private key.
* TLS CA access is automatically enabled when TLS or external access is
configured.

* **Documentation**
* Expanded TLS guidance explains certificate validation and provides
commands for retrieving the tenant CA certificate.

* **Tests**
* Added coverage for TLS projections, secret visibility, dashboard
resources, and secure fail-closed secret exposure.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-01 13:10:37 +04:00
Aleksei Sviridkin 8d386c2cbc docs(mongodb): name the trust anchor as the tenant sees it
Every object this chart creates is named after the Helm release, not
after the MongoDB resource, so a MongoDB named catest yields
mongodb-catest.tenant-ca. The retrieval commands used a bare <release>
placeholder, which reads as the resource name and produces a command
that returns nothing. Spell the prefix out in the two commands meant to
be copied, and define the placeholder once for the prose.

Split the sentinel's shape test in two. The absence of ownerReferences
is the invariant — it breaks the walk from the projection back to the
release, and the tenant stops receiving the anchor. An explicit
namespace or a chart-added label breaks nothing by itself; grouping all
three under one name presented them as equally load-bearing.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:34 +03:00
Aleksei Sviridkin 04112dacdf test(mongodb): stop the tenant-Role guards passing on the wrong document
The assertions withholding the key-bearing TLS Secrets from the tenant
Role addressed it as document 0 and, in two cases, asserted only with
notContains. Neither survives a rearrangement.

notContains passes when its path does not resolve, so a test built from
it alone goes green against the wrong document. Reversing the order of
the two documents in the template moved the RoleBinding into position 0,
and "does not grant the key-bearing TLS secrets when external is
enabled" and "does not grant the tenant-ca projection by name" both
stayed green while the Role granted <release>-ca-cert by name — the
Secret holding the CA private key.

Select the Role by kind, and pair every notContains with a positive
assertion on the same path so a rule reordering that moves rules[1]
reddens too. Indexing survives neither rearrangement; selecting survives
the first; the positive anchor catches the second.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin 8b6867dc47 fix(mongodb): correct the trust-anchor prose and two e2e assertions
The name-clash note claimed the shared TLS helper documents
<release>-ca-cert as the conventional key-free anchor. It documents the
opposite: the canonical name is <release>.tenant-ca, and it names this
engine as the reason, because MongoDB already claims <release>-ca-cert
with a key-bearing Secret. There is no clash to warn about and the chart
follows the convention rather than deviating from it. The paragraph
above already says <release>-ca-cert holds the CA private key, so drop
the note instead of inverting it.

Guard the ownerReference assertions against an absent list. length() of
null is a type error rather than a failed comparison, and the harness
returns it as an error, which aborts the poll instead of retrying — so
the timeout never applies while the operator is still patching the
reference on.

Restore the two-variable form of the fingerprint comparison. Inlining
both command substitutions into the test swallows a non-zero exit: with
no openssl on PATH both sides are empty, they compare equal, and the
check passes having compared nothing.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin 01cfb5fda0 fix(mongodb): correct what a missing extraction controller does
The README said the sentinel would sit inert with nothing else
affected. The CRD is a template of the controller's own chart, so a
cluster without the controller has no TenantProjection kind and the
release fails to render at all. The caveat also guarded a state that
cannot occur, since the controller is a mandatory platform component —
drop it rather than restate it, matching the postgres chart, and keep
the paragraph explaining the choice of source Secret.

Enumerate all three key-bearing Secrets in the registry negative. Two of
the three were listed, which leaves the third free to acquire the label
and leak a key, the exact reasoning the step above it already spells
out.

Collect TenantProjection status on failure. The controller reports
SourceNotFound and its siblings there, and without it a failed
projection surfaces only as an absent Secret with no reason given.

Rename the source-shape step, which no longer looks at the CA Secret,
and cross-reference the two halves of the leaf Secret's name.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin fc57e928e1 test(mongodb): drop a document count the sentinel invalidates
The cert-manager exclusion suite justified its `any: true` requirement
with a specific number of rendered documents. The argument only needs
"more than one"; the number was already tied to one value set and the
sentinel adds another document.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin c0b2ddcdf7 test(mongodb): assert the published trust anchor end-to-end
The existing steps only assert that key-bearing Secrets are NOT promoted
to the tenant, and every one of those is satisfied by an absent label —
they hold just as well if nothing was ever published. Add the positive:
the projection exists, carries exactly ca.crt, matches the source
certificate by fingerprint, and is reachable through the tenantsecrets
registry rather than only as a raw Secret.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:33 +03:00
Aleksei Sviridkin 29d2955241 fix(mongodb): declare the trust anchor with a TenantProjection
A tenant verifying a MongoDB endpoint needs the CA certificate, and
every Secret that holds one also holds a private key: the operator's
<release>-ca-cert carries the CA key, the leaf <release>-ssl the server
key. Granting either would hand over the key along with the anchor.

Render a TenantProjection naming the source, so the CA-extraction
controller can publish the key-free <release>.tenant-ca, and select that
projection by label from the managed-service definition. The sentinel
carries no ownerReferences: it is resolved back to its release through
the label Flux stamps, and an owner reference short-circuits that walk
and silently costs the tenant its anchor.

The sentinel is ungated. The operator issues the certificate chain
whatever the chart asks for, so the source exists for every release and
there is no plaintext leg to skip.

The source is <release>-ssl rather than <release>-ca-cert. It is the
name this chart itself sets in spec.secrets.ssl, so the reference cannot
drift from the CR; the operator writes it on both its cert-manager path
and the self-signed fallback it uses when cert-manager is absent, where
no <release>-ca-cert exists at all; and it holds the merged old and new
CA through a rotation, where <release>-ca-cert holds only the new one
and would stop verifying pods still serving the previous leaf.

Withhold the key-bearing Secrets from the tenant explicitly as well. The
label selector matches on no name, so a single mis-stamp would promote a
private key; the exclude list is evaluated first and closes the
enumerable set.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:20:32 +03:00
Aleksei Sviridkin 4092f59528 docs(mongodb): scope the upgrade-availability claim to quorum-capable sizes
The upgrade-restart note said the replica set stays available through the
rollout. That holds at three members and fails below them, and the chart
permits both smaller sizes: replicas 1 and 2 render unsafeFlags.replsetSize
and are not excluded from the SAN change, so they gain horizons and roll on
upgrade exactly as a three-member release does. At one replica the rollout
takes the only mongod down; at two the write majority is still two, so
restarting either member stalls writes until it returns. An operator running
a single-replica external release would have read the old text and declined
to book a window.

Cover the two replicas case, which was untested, and assert horizons and the
sub-quorum marker together at both sizes, so gating horizons to sizes that
can absorb a restart turns the tests red and sends someone back to this
paragraph. The default-size case carries the pair as a positive control: the
marker proves nothing unless it is absent where a restart is survivable.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:43 +03:00
Aleksei Sviridkin 7ef09cc963 docs(mongodb): show how to verify a connection against the trust anchor
The CA retrieval command was documented without the other half: what to do
with the bundle once it is in hand. A tenant reading only the retrieval step
has the certificate and no worked example of a verified connection.

Pass the two TLS inputs as mongosh flags rather than folding tls=true into
the connection string, because the correct separator differs by topology --
the replica-set URI already carries a ?replicaSet=rs0 query and the sharded
one carries none, so a single copyable form would be wrong for one of them.

Call out that omitting the flags is silent. Under preferTLS the server
accepts the plaintext connection and nothing reports the downgrade, so a
working connection is not evidence that TLS was negotiated.

Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 8af39eea22 docs(mongodb): correct the external SAN version gate and the leaf lifetime
The external-hostname note claimed the operator and the CR version were "both
shipped by this chart". Only crVersion is: the PSMDB operator is a separate
platform component with its own lifecycle, and the SAN fold is gated on both.
On a platform carrying an older operator nothing reports the unmet gate — the
chart still renders splitHorizons, the CR is still accepted and the cluster
still comes up healthy, while the external hostnames never enter the SAN list
and a verifying client fails the handshake. State which half the chart owns and
point at the operator version as the thing to check.

The leaf certificate lifetime was stated as a flat 90 days. It is an operator
default this chart deliberately does not pin: nothing in the tenant contract
keys off it, the operator reissues without client action, and pinning would
freeze the chart against upstream shortening it. Attribute the number to the
operator version and pin the field's absence, so "inherited rather than chosen"
stays falsifiable — the same treatment allowInvalidCertificates already gets.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 90eb5c6815 test(mongodb): make the cert-manager exclusion suite discriminate
Every assertion in the suite passed while checking nothing. containsDocument
without `any: true` asserts that EVERY rendered document matches, so negating
it asks whether "not every document is a Certificate" — true of any chart
rendering more than one object, and this one renders eight. A cert-manager
Certificate appended to templates/mongodb.yaml, the most plausible place for
one to appear, left the suite fully green.

Add `any: true` to each containsDocument so the negation means "no document
anywhere is of this kind". Unscoped assertions are ANDed across template
files, which is what makes the negatives correct: they go red if any file
renders the forbidden kind.

Add a positive control asserting the chart does render the PerconaServerMongoDB
CR through the same matcher shape, scoped to the one template that emits it.
The four negatives are all satisfied by a matcher that never matches, which is
exactly how this suite failed; the control fails if that returns.

Verified by injecting a Certificate into the chart: the suite now fails and
previously passed.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin ea7bc00016 docs(mongodb): correct the TLS secret ownership and external SAN claims
The uninstall section promised that no manual cleanup of the TLS Secrets is
needed, reversing the instruction it replaced. That promise rests on an
ownerReference the operator patches on at reconcile time rather than sets at
creation, and two paths skip the patch: a dry-run reconcile returns before it,
and a Secret already present under the same name with a mismatched
cert-manager.io/certificate-name annotation is counted toward success and
returned on unpatched. The deletion finalizer does not cover the gap — it
enumerates only the users, internal-<release>, internal-<release>-users and
encryption-key Secrets. So a Secret predating the release at one of these names
can stay unowned and survive uninstall, and for <release>-ca-cert that orphans
the CA private key.

State the reconcile-time mechanism and tell operators to confirm the CA Secret
is gone. Pin the outcome with a chainsaw step asserting the CR owns all three
TLS Secrets on a normal install, so the claim goes red if the operator stops
patching instead of rotting as prose.

The external-hostname note described a permanently empty _namespace.host as a
bootstrap window that self-heals. A tenant with no host of its own and no
ancestor supplying one resolves to an empty host for good, leaving external
TLS hostname verification silently unavailable. Name that case alongside the
transient one, and say why the render is not failed closed: it would break
existing hostless tenants already running external access over IP.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 5be4e417b3 docs(mongodb): correct the operator-behaviour claims and the upgrade impact
The TLS notes made several claims the operator's own behaviour contradicts.
The CA is issued for a year and renewed about a month early, not for a decade
with rotation disabled — so a bundle copied once stops verifying, and the
retrieval steps now say to re-read it. Sharded mongos SANs are complete,
because the operator emits them for every topology; the real gap is that no SAN
covers the single external LoadBalancer fronting mongos, so external hostname
verification is unsupported there. preferTLS is the operator's own default
rather than a posture adopted to accommodate backups, which run under the same
certificates. External access in replica set mode is one LoadBalancer per
member with driver-side primary discovery, not a single load-balanced
endpoint — the old description contradicted the external SAN note. And the
posture section named only one of its two inherited weaknesses: unverified
clients are accepted as well as plaintext ones.

The lifecycle section was wrong in both directions. The operator owns its TLS
Secrets and its cert-manager objects through the CR, so they are collected on
uninstall rather than left behind. The rotation snapshots are the opposite of
what was written: the operator deletes them itself once the merged CA has
landed, and reads the previous CA back out of them while a rotation is in
flight — so the advice to delete them by hand could destroy the only copy of
the old CA. Only an interrupted rotation strands them.

Document what an upgrade actually does to a release already running with
external access: the new hostnames become certificate SANs, the leaf is
reissued, and the replica set rolls. Gate the retrieval steps on the component
that publishes the trust anchor and on cert-manager, without which the declared
source does not exist and the command returns nothing.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 2670f7efc1 build(mongodb): run the chart's unit tests
The unit-test runner discovers charts by probing for a test target, and this
chart had none — so its suites had never run in CI.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 612ae08e5f fix(mongodb): omit splitHorizons where it would render empty
With no external hostname to advertise — zero replicas, or a tenant host the
controller has not populated yet — the horizons range produced nothing and the
key was emitted with a null value. The CRD types it as an object without
nullable, so the API server rejected the whole PerconaServerMongoDB and the
HelmRelease with it. The values schema permits zero replicas, so this was
reachable. Guard on both and omit the key entirely.

Pin the surrounding invariants, which were asserted in comments and enforced
nowhere. The chart must render no cert-manager objects of its own, since the
operator owns the chain and anything rendered here would be orphaned while
looking like it participated. It must set neither of the two fields that
switch TLS off at defaulting time while the rendered mode still reads
preferTLS. And the external-access case now supplies a tenant host, so it
isolates the flag it names rather than passing on the host guard.

Correct the header comment, which fused two false claims: that setting
spec.secrets.ssl is what keeps TLS on, and that it is what engages the
cert-manager integration. The operator defaults that name unconditionally and
runs the cert-manager path regardless.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:42 +03:00
Aleksei Sviridkin 27f71bfbe9 fix(mongodb): deliver the TLS trust anchor without private key material
The tenant-facing paths handed over both Secrets the PSMDB operator creates
for TLS. <release>-ca-cert holds the CA private key, so read access is a full
trust-chain compromise; <release>-ssl holds the server private key, so read
access allows impersonation. A client verifying the endpoint needs ca.crt and
nothing else, so neither object belongs on a tenant-facing path.

Declare the trust anchor by name instead. The operator creates its CA Secret
itself and offers no way to label it, so the ApplicationDefinition names it and
the CA-extraction controller lifts ca.crt alone into the key-free projection.
The include selector matches that projection by label, and the dashboard Role
drops both key-bearing names without gaining the projection by name: a name
grant conveys whatever occupies the name, while the label path conveys only
what the platform vouched for.

Witness both delivery paths against a running cluster, since the two that
matter are separate. The Role is asserted as an exact grant with bounded rule
counts, because a rule with no resourceNames — or one granting every resource
— would otherwise pass an absence check while conveying every Secret in the
namespace. The label path is asserted as a negative on all three key-bearing
Secrets. A bats check pins the declaration's names and keys, which nothing else
covers: the field is pruned at admission until the controller ships, so drift
there would surface as an empty trust anchor with nothing red.

Drop the mongodb-rd unit test. That package has no test target, so the file
never ran, and what it asserted was the leak.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-09-01 11:18:41 +03:00
Arsolitt 7f8f32b1ba fix(mongodb): always enable operator-managed TLS, drop tls.enabled field
Address review feedback from lexfrei (B3): the tri-state tls.enabled field
could not actually disable TLS. The PSMDB operator defaults spec.tls to
preferTLS whenever it is unset, so tls.enabled: false rendered no spec.tls
while the operator still served TLS — a misleading no-op.

Remove the field and unconditionally render spec.secrets.ssl and
spec.tls.mode: preferTLS to match operator behaviour. splitHorizons SAN
injection now keys off external alone. Also drop the Makefile jq patch that
re-injected the tls schema property.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Arsolitt <arsolitt@gmail.com>
2026-09-01 11:11:27 +03:00
Arsolitt e9f276195f test(mongodb): cover TLS modes, splitHorizons, and RBAC
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
2026-09-01 11:10:59 +03:00
Arsolitt f6816e1f44 feat(mongodb): add operator-managed TLS with external hostname SANs
Signed-off-by: Arsolitt <arsolitt@gmail.com>
Assisted-By: Claude <noreply@anthropic.com>
2026-09-01 11:10:55 +03:00
myasnikovdaniil e84a446893 chore(keycloak): bump KMS proxy to 0.2.2 (#3995)
## What this PR does

This PR updates keycloak-kms-proxy to v0.2.2 and pins image to published
digest. New version fixes decryption for reads whose result set spans a
join.

### Screenshots

Not applicable.

### Downstream repositories

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
chore(keycloak): update keycloak-kms-proxy to v0.2.2 with a fix for decrypting reads whose result set spans a join
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Updated the Keycloak encryption component to a newer verified image
version.
* Improved deployment reliability by pinning the image to a specific
integrity digest.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-31 12:07:39 +05:00
Myasnikov Daniil 6d9444056d chore(keycloak): bump KMS proxy to 0.2.2
Pin the released image by digest so package rendering is reproducible.

Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-28 17:11:41 +05:00
Andrey Kolkov b9a95a0c4a fix(postgres): let in-place restore re-bootstrap instead of wedging (#3959)
## What this PR does

An in-place `RestoreJob` (one whose `targetApplicationRef` is omitted,
so the app is restored back into itself) re-bootstraps the Postgres app
under its own name. On stock v1.6.2 it never comes back up — the cluster
is deleted and then hangs in `Setting up primary` with
`*-full-recovery-*` pods crash-looping, or the HelmRelease upgrade fails
outright. There are two independent causes, both in the same restore
patch, and both surface only in place because a to-copy restore targets
a different app.

**1. Archive-vs-recovery `serverName` collision.** The driver set the
restored cluster's WAL-archive `serverName` equal to the recovery source
(`bootstrap.serverName = bootstrap.oldName = sourceServerName`), while
the chart pinned the archiver `serverName` to the release name. In place
those coincide, so the cluster archived onto the source's own WAL prefix
and `barman-cloud-check-wal-archive` refused to start with `Expected
empty archive`. Fixed by giving the restored cluster a fresh
`bootstrap.newServerName`, keyed by the `RestoreJob` UID (unique per
restore, stable across reconciles); the chart archives there while
`externalClusters` still recovers from `bootstrap.serverName`. This is
what manual `CNPG` strategies hit (their `serverName` defaults to the
cluster name, including the shipped
`examples/backups/postgres/10-cnpg-strategy.yaml`); the `cozy-default`
flow dodged it because its `serverName` is `<namespace>-<app>`.

**2. `useSystemBucket` guard.** The restore populates explicit S3
coordinates (`destinationPath` / `s3CredentialsSecret` / `endpointCA`)
but left `backup.useSystemBucket=true`, so the chart's
`bootstrap.enabled is incompatible with useSystemBucket` guard failed
the HelmRelease upgrade and the cluster never re-rendered. This is what
the default `cozy-default` (system-bucket) flow hits on an in-place
restore. Fixed by clearing `useSystemBucket` in the restore patch, since
the app is now on the explicit-creds path. Verified live on a v1.6.2
cluster: with `useSystemBucket` cleared, the same in-place restore
re-bootstraps to a healthy cluster and the sentinel row survives.

Both are exercised only by an in-place restore; the postgres backup e2e
drove only to-copy, so neither surfaced in CI. A step 50 in-place
restore now guards cause 1 (manual-strategy flow); cause 2 is covered by
a Go unit assertion (a system-bucket in-place e2e needs the platform
bucket wired in CI and is left as a follow-up).

This is a bug in released v1.6.2 (chart and driver are byte-identical on
`main`), so it is a backport candidate.

### Screenshots

Not applicable (no UI changes).

### Downstream repositories

The only externally visible schema surface is one new field,
`spec.bootstrap.newServerName`, in the postgres `values.schema.json` /
CRD; it is set by the CNPG backup driver on restore and never authored
by tenants, exactly like the existing `bootstrap.serverName` /
`bootstrap.oldName`. (The `useSystemBucket` clearing is internal to the
Go driver — `backup.useSystemBucket` already exists in the schema.) The
chart `README.md` is regenerated from `values.yaml` automatically by the
website docs bot on release, and `terraform-provider-cozystack` models
user-authored spec (its guard tracks top-level fields, not driver-set
nested ones), so neither needs a change in practice. Leaving every box
unticked per the trigger-map guidance so a maintainer confirms rather
than acting on a speculative follow-up.

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
fix(postgres): in-place restore no longer wedges — the restored cluster archives WAL under a fresh serverName distinct from the recovery source, and a system-bucket app is switched to explicit credentials so the bootstrap/useSystemBucket guard no longer fails its HelmRelease.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for specifying a distinct WAL archive destination when
restoring PostgreSQL clusters.
- In-place restores now automatically use a fresh archive location while
continuing to recover from the original source.
- Added configuration and schema support for the optional
`bootstrap.newServerName` setting.

- **Bug Fixes**
- Prevented archive conflicts that could cause in-place restores to
fail.

- **Documentation**
  - Documented the new restore archive configuration and behavior.

- **Tests**
- Added coverage for unique restore destinations and in-place restore
validation.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-28 15:08:35 +04:00
Andrey Kolkov ac00577eb7 fix(clickhouse): scheme the backup S3_ENDPOINT on the system-bucket flow (#3963)
## What this PR does

On the default `cozy-default` / `useSystemBucket` backup flow, the
ClickHouse chart pointed the `clickhouse-backup` sidecar's `S3_ENDPOINT`
straight at the projected `cozy-backups-creds.endpoint`, which the
backup credentials projector stores as a **bare host** (no scheme) by
design. `altinity/clickhouse-backup` runs `S3_ENDPOINT` through the
aws-sdk-go-v2 endpoint resolver, which rejects a scheme-less value with
`Custom endpoint 's3.host' was not a valid URI` — so every ClickHouse
backup and restore on the default flow failed. Reproduced live on a
v1.6.2 cluster: a `cozy-default` `BackupJob` against a `useSystemBucket:
true` ClickHouse hangs, and the sidecar logs `S3 ResolveEndpoint ... was
not a valid URI`.

The fix prepends `https://` on the `useSystemBucket` path via a helper
env plus `$(VAR)` expansion (a `secretKeyRef` value can't be
string-interpolated), mirroring the platform's
`backupstrategy-controller.endpoint` helper which force-prepends
`https://` for the always-TLS external S3 ingress. The legacy and
bring-your-own-Secret paths keep reading `endpoint` verbatim, since that
value is tenant-supplied and already carries its own scheme.

ClickHouse was the only `cozy-default` app hit by this. I audited every
default-flow app: the CNPG/postgres, etcd and velero strategies already
get a scheme'd URL from the endpoint helper, and mariadb and
foundationdb deliberately pair a bare host with a separate TLS flag;
mongodb reads a full URL from its own chart values. So this is the
single scheme-class fix needed.

Not addressed here (separate, non-scheme gaps surfaced during the audit,
flagged for follow-up): MongoDB has no `useSystemBucket` wiring at all
(its `cozy-default` strategy assumes a manually-configured
`backup.endpointURL` + per-app S3 creds), and FoundationDB's
self-signed-CA trust to the operator pod is a known gap.

### Screenshots

Not applicable (no UI changes).

### Downstream repositories

No schema or API change (only a chart template and its helm-unittest).
`values.yaml` / `values.schema.json` / the CRD are untouched, so nothing
downstream is affected. Leaving every box unticked per the trigger-map
guidance.

- [ ] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
fix(clickhouse): backup and restore now work on the default system-bucket (useSystemBucket) flow — the clickhouse-backup sidecar receives a scheme'd https:// S3_ENDPOINT instead of the projected bare host, which the S3 client rejected as an invalid URI.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Corrected ClickHouse backup endpoint configuration when using the
system bucket.
* Ensured backup connections consistently use the required HTTPS
endpoint format.
* Improved handling of backup credentials across system bucket, legacy,
and custom-secret configurations.

* **Tests**
* Added coverage validating endpoint and credential wiring for supported
backup configurations.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-28 15:05:14 +04:00
Myasnikov Daniil c88da18139 test(tests): keep trace switch suite ASCII-only
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-28 15:31:22 +05:00
Myasnikov Daniil e949f39800 fix(e2e): preserve previous-log cap slots
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-28 15:29:29 +05:00
Myasnikov Daniil 76d397523b docs(tests): record why an INT/TERM trap cannot cover a killed test
The comment added with the trace switch pointed at a signal trap as the
fix for the one case quiet mode does not cover: a test killed rather
than failed never reaches the fail handler, so its trace is gone rather
than already printed. That advice is wrong, and wrong in a way that
costs whoever follows it.

POSIX sh defers a trap until the current foreground command returns, so
installing an INT/TERM handler stops the runner dying on the signal at
all. Measured: a suite hung on `sleep 30` exits in 30s with the trap
installed against 2s under the default disposition. GitHub's
cancellation escalates INT, TERM, then KILL, so the deferred handler
gets SIGKILLed before it runs -- no dump, and the grace period spent
waiting for a hang. Built it, measured it, reverted it.

Making it work means running the test pipeline in the background and
waiting on it, because `wait` is the interruptible one. That is surgery
on the path every suite in this repo takes, unit and e2e, to recover a
partial trace in a rare case that already has a 48-second escape hatch.
Not taken; recorded so the next reader does not spend the afternoon
finding out the same way.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:30:18 +05:00
Myasnikov Daniil a5c90d984e fix(tests): guard the finishedAt leaf and make three guards actually bite
Findings from two independent adversarial reviews of the preceding two
commits. Each was reproduced locally before being fixed.

The ordering inverted itself on a shape I had not probed. `finishedAt`
is a metav1.Time behind `json:",omitempty"`, which cannot omit a
struct, and its zero value marshals to `null`; the kubelet does
synthesize a terminated state with no time (ContainerStatusUnknown on a
pod deleted mid-run, which also bumps restartCount and so clears the
restart filter). Two guards deep left the leaf exposed, and kubectl
renders it as the literal `<no value>` -- which sorts ABOVE every digit,
so that row took the front of the queue from the most recent real
restart, and the header printed `previous instance ended <no value>`, a
claim about a time nothing observed. Verified with a text/template probe
over seven lastState shapes: `null` and a missing key both produced it.

Fixed at both layers, because they fail independently. The template now
guards the leaf, so the string cannot be produced. And the sort
normalises any field-6 value not shaped like a timestamp to empty, so
the ordering stays correct and the header stays quiet even if some other
producer emits one. Earlier verification against a live cluster showed
no `<no value>` and I read that as the guard being right; it only meant
no container on that cluster was in that state.

Three guards did not bite:

  - both stability fixtures were pre-sorted ascending, which is exactly
    what sort's last-resort whole-line compare reproduces with no -s at
    all, so the tests written for -s passed without it. Reversed; they
    now fail when -s is dropped.
  - no test fed a six-field row through the whole script, so the
    timestamp reached the dump header uncovered -- deleting the read
    variable left the suite green at 39/39. One integration test now
    asserts the header names the time.
  - the e2e guard matched any mention of COZYTEST_TRACE, so documenting
    the design over there would have turned it red. It now matches an
    assignment, and tolerating a comment is verified too.

Also: a failed sort no longer discards a whole namespace group. The
caller compares row counts to report the cap, so losing a group silently
became "0 of N captured, N more not captured" and sent the reader after
a cap that never fired. It falls back to discovery order and says so.
LC_ALL=C on the sort, since the key is fixed-format ASCII.

And the claim itself was too strong. "Nothing diagnostic is lost" holds
only for a test that RETURNS: the fail handler runs after the pipeline,
and there is no INT/TERM trap, so a test killed rather than failed -- a
hang reaped by the job timeout -- never dumps its log, and under quiet
mode its trace is gone rather than already printed. The comments in
hack/cozytest.sh and the Makefile now say so and name the escape hatch.
A trap that flushed the tail of the log would close it; left out
deliberately rather than bundled in here.

Every fix is mutation-proven: reverting the leaf guard, removing the
key normalisation, dropping the read variable, dropping -s, and
silencing an e2e recipe are each caught. Unit lane 1513 passed, 1 failed
(hack/ghcr-mirror_test.bats, which reads a live cluster and fails the
same way under main's runner).

Assisted-By: Claude <noreply@anthropic.com>
Assisted-By: GPT-5 <noreply@openai.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:06:53 +05:00
Myasnikov Daniil 599ace69fd test(tests): guard the COZYTEST_TRACE switch against silent regression
The quiet unit lane rests on one invariant: run_one tees the raw stream
to $log and the fail handler prints all of it for whichever test fails,
so suppressing the live stream drops the output of PASSING tests only.

That invariant fails silently. It is exercised only when a test fails,
so a green CI run proves nothing about it - a refactor of run_one's
reader loop that also broke the dump would ship, and would be found by
whoever next needed a red unit run diagnosed, with no trace and the run
already lost. Nothing in the tree covered it.

Seven tests, fixtures that fail on purpose, following the structure of
hack/cozytest-capture-gate.bats:

  - quiet mode streams no trace for a passing suite
  - the unset default stays verbose, which is what the e2e call sites in
    packages/core/testing/Makefile rely on
  - a failing test still dumps its whole trace under quiet mode, xtrace
    included, which is the load-bearing one
  - quiet mode still fails the run
  - the per-test start/end markers survive, so a hang still names the
    test that never returned
  - the unit recipe asks for the quiet runner
  - the e2e recipes do not silence theirs

Every one is mutation-proven: removing the fail-handler dump, flipping
the default, making the switch a no-op, dropping the variable from the
unit recipe, adding it to an e2e recipe, and swallowing the exit code
are each caught by exactly one test. The last of those is worth a note -
it disables the mechanism the outer runner uses to report failure, so
the run exits 0 while the failure is in its output. A probe keyed on the
exit code alone reads that as uncaught.

Two traps are recorded in the file's header rather than left to be
rediscovered. Fixtures are built with printf because the awk transform
harvests a heredoc's own @test line into the suite that contains it. And
the default is asserted under `env -u COZYTEST_TRACE`, because the
Makefile passes the variable as a command prefix, which exports it - a
nested runner started from a suite that `make unit-tests` is running
would otherwise inherit 0 and the default would never be under test.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:06:33 +05:00
Myasnikov Daniil 7741843178 test(e2e): order previous-instance log capture by restart recency
The container cap (COZY_PREVLOG_MAX=12) is spent in namespace order, and
"unrelated" has meant only "in another namespace" - time was never a
key. So a container that restarted once during cluster bring-up
competed on equal footing with one that crash-looped during the test,
and won whenever the pod list reached it first. Two measured e2e jobs
spent 1034 and 1545 lines on virt-api and cdi-apiserver, restarted once
at install time by `Unable to create certwatcher` hours before any app
suite ran; on a development cluster 178 containers carried a restart, their
timestamps spanning 24 days, so the cap binds hard enough for the order to
decide everything.

Ordering, not filtering, and deliberately: an install-time restart IS
the explanation when the install is what broke, and this capture has no
reference point for "when the test began" that would make a cutoff
honest. Namespace stays the primary key - a stale restart in the
namespace under test beats a fresh one three namespaces away. So this
fixes cap displacement, not the raw volume of any single run.

lastState.terminated.finishedAt joins the row, guarded so a missing
lastState cannot error the list call and fail the capture open (verified
against a live cluster: no <no value>, empty where the kubelet has
already dropped it). Reading it also fixes a latent bug the sixth field
would otherwise have caused - `read -r ... restarts` folds a trailing
field into the last variable, so the dump header would have printed
`restarts=1|2026-08-22T09:06:00Z`. It now names the time instead, which
is the first thing that decides whether the dump is worth reading.

Sorting is lexicographic over fixed-width RFC3339 UTC, so no date
parsing: reverse puts newest first and leaves undated rows last, and -s
keeps a node reboot's simultaneous restarts in input order so the
selection is comparable between runs. Rows with no timestamp pass
through untouched, which is what makes this a no-op for the existing
five-field fixtures.

Five tests added; both mutations are caught (a passthrough
sort_recent, and dropping -s - the latter by the pre-existing namespace
test). capture-previous-logs 39/39, cozyreport 184/184,
bats-no-exit-trap 22/22.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 22:06:33 +05:00
Andrei Kvapil c8e4d97df9 feat(ingress-nginx): bump to 1.11.5 and restrict the admission webhook (#3798)
## What this PR does

Bumps ingress-nginx to chart 4.11.5 / controller v1.11.5, and closes the
admission webhook to everything except the API server.

### Version bump

The shipped `v1.11.2` controller is a cozystack rebuild that **already
carries a backport** of the CVE-2025-1974 fix (v0.28.2, March 2025), so
this is not a fix for a live vulnerability. The problem is that it
reports itself as a plain `v1.11.2 build git-d3bb2b4`: the binary is
built from the upstream 1.11.2 tag with the patch applied on top, which
does not change the version ldflags, and the image labels are inherited
from the `registry.k8s.io` base layer. A patched build is therefore
indistinguishable from an unpatched one by anything except its digest,
and anything scanning by version string flags it. This bump removes that
ambiguity.

`packages/system/ingress-nginx` feeds three consumers — the platform
ingress, tenant Ingress applications (`extra/ingress`), and the ingress
addon of tenant Kubernetes clusters — so one bump covers all three. The
protobuf-exporter sidecar moves in lockstep, since its tag tracks the
upstream controller version.

`CHART_VERSION` is now pinned in the package Makefile. `helm pull`
carried no `--version`, so `make update` would have fetched whatever is
latest upstream (4.13.9 today) against a controller image that only
exists for selected versions.

### Admission webhook policy

The webhook is only ever called by the API server, but it was reachable
from every pod in the cluster, in any namespace — avoidable attack
surface independent of any specific CVE.

The new `CiliumNetworkPolicy` is deny-only and disables default-deny. A
Cilium rule carrying an ingress section otherwise puts the selected
endpoints behind a default-deny, which here would mean enumerating every
port the controller serves (80, 443, metrics, the exporter, tcp/udp
services) and breaking traffic on the first one missed. Subtracting one
port is the safer construction.

Two callers are deliberately not denied:

* **host-network sources**, which are not Cilium endpoints — this covers
a management cluster, where the API server runs on the host network;
* **the konnectivity agent**, which is not optional. On a Kamaji-hosted
tenant cluster the API server lives outside the cluster and reaches
in-cluster webhooks through the konnectivity tunnel (Kamaji defines only
the `cluster` egress selection), so the call arrives from the agent pod
in `kube-system`, not from the API server address. A `fromEntities:
[kube-apiserver]` allow would not match it, and every Ingress
create/update in every tenant cluster would start failing.

The policy is namespaced rather than clusterwide on purpose: the same
package is installed into several namespaces of one cluster, so a
cluster-scoped object with a fixed name would collide between releases.
It can be disabled with `admissionWebhookPolicy.enabled`.

### Verification

Tested on a live Kamaji tenant cluster (k8s 1.35.6, Cilium 1.19), with
the addon enabled for the test and disabled afterwards:

| | probe from a pod in `default` | Ingress create |
|---|---|---|
| without policy | `open` | works |
| with policy | `Connection timed out` | works |

Also confirmed on the management cluster that `kube-apiserver` runs as
static pods on the host network, so the host-network carve-out holds
there.

The package previously ran no unit tests at all; a `test` target is
added with 7 assertions covering the policy. It is scoped with
`--with-subchart=false` because the vendored chart carries upstream's
own suites, which assert upstream defaults and fail under our values.
The full `hack/helm-unit-tests.sh` sweep is green.

The controller and exporter images are rebuilt by
cozystack/ingress-nginx-with-protobuf-exporter#5, which vendors the
`11843.diff` patch instead of fetching it from a still-open upstream PR
at build time.

### Screenshots

Not applicable — no UI changes.

### Downstream repositories

Walked the trigger map in `docs/agents/contributing.md` against the
diff, which touches `packages/system/ingress-nginx/` and one comment
line in `hack/promote-retag.sh`:

* **website** — no package added, renamed or removed under
`apps/`/`extra/`; no platform component added or removed; no change to
`packages/core/platform/values.yaml`, variants, bundles or release
assets. The `package Makefiles` trigger was considered because of
`CHART_VERSION` and the new `test` target: `CHART_VERSION` does not
change the documented contract of `make update`, and `test: helm
unittest` is an existing pattern in the repo
(`packages/system/cilium-networkpolicy/Makefile`), not new tooling.
* **terraform-provider-cozystack** — every trigger is about
`packages/apps/`, `values.schema.json`, version enums or
`ApplicationDefinition`; none apply to a system package.
* **ansible-cozystack** — no change to installer values, platform keys
the role sets, variants, waited-on objects or node prerequisites.
* **ccp** — `hack/package.mk` and `hack/common-envs.mk` untouched;
nothing under `hack/` moved or renamed, and the edit to
`hack/promote-retag.sh` is a comment with no behaviour change; layout
and namespaces unchanged.
* **external-apps-example**, **talm**, **cozyhr**, **cozy-proxy**,
**cozystack-telemetry-server**, **examples** — no trigger matches.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
feat(ingress-nginx): update to chart 4.11.5 with controller v1.11.5, and restrict the admission webhook so it is reachable only by the API server. The previously shipped v1.11.2 image already contained a backport of the CVE-2025-1974 fix; it reported itself as a stock v1.11.2 build, which this update makes unambiguous.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added optional network protection for the admission webhook with
restricted access rules.
* Added PrometheusRule support and improved ServiceMonitor
configuration.
  * Webhook service ports are now configurable.

* **Updates**
  * Upgraded ingress-nginx and controller images to version 4.11.5.
  * Improved default-backend availability and topology-spread settings.
* Added Pod Security Admission guidance and refreshed configuration
documentation.

* **Bug Fixes**
* Corrected webhook service rendering and default-backend disruption
budget behavior.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-27 16:59:06 +02:00
myasnikovdaniil 3d12ece2af chore(ci): namespace the backport labels without breaking the trigger (#3844)
Takes over #3433 and finishes it. Closes #2585.

The rename itself is IvanHunters' work, replayed here on current main
with his commits intact. What was blocking it was my own review, and
this fixes that plus one thing that appeared on main afterwards.

## What was blocking it

The gate in `backport.yaml` matched `kind/backport` only after the
rename. The `aliases:` in `labels.yml` migrate labels already attached
to issues and pull requests, but they do not change what an external
applier does going forward, and the org-level dosubot still applies the
legacy names. So the bot would have quietly stopped firing for the share
of backports it labels.

Both spellings are now accepted, in the `if:` gate and in the JS step
that reads them.

## What appeared afterwards

#3569 landed while #3433 sat, and it added a concurrency guard that
appends `-label` to the group for any labeled event that is not a
backport label, so a backport label never cancels a run already in
flight. That guard enumerated the two legacy names, so once the labels
are namespaced a `kind/backport` event takes the `-label` branch and the
guard stops covering the case it exists for. Extended to the new names.

## The dual acceptance is transitional and says so

Accepting two spellings is a bridge, not a design. The plan is to switch
dosubot's pull request labelling off and leave the in-repo labeler as
the only applier, at which point the legacy arms can go. The comment in
the workflow names all three deletion sites so that cleanup is
mechanical: the `if:` gate, the JS step, and the concurrency group.

Landing this before or after dosubot is switched off both work, which is
the point.

## Checks

`actionlint` clean on the workflow. There is no bats coverage for this
gate on main; #3813 is what adds `hack/pr-labeler-contract.bats`, so the
two are worth landing in that order.

### Release note

```release-note
NONE
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **Workflow Improvements**
- Backport automation now supports standardized `kind/backport` and
`kind/backport-previous` labels while maintaining compatibility with
legacy labels during the transition.
- Stale pull request handling and backport detection now follow the
standardized labeling scheme.

- **Documentation**
- Updated contribution and release documentation with the new labels,
labeling requirements, and troubleshooting guidance.

- **Tests**
- Expanded validation to cover both standardized and legacy backport
labels.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-27 17:26:20 +05:00
Myasnikov Daniil cb60f743d3 test(ci): extend the backport label contract to both spellings
The concurrency-group key and prepare's `if:` gate now name four labels
rather than two, and release-freeze-contract.bats pinned the pair on
both sides — as a literal and as an exact count. Every namespaced name
appended to the workflow therefore reported as a widening, which is the
right default: the count exists because the literal is a substring check
and an appended exclusion satisfies it unchanged.

Here the appended names are not a widening. `kind/backport` and
`kind/backport-previous` are genuine backport requests, so they belong
on the same side of the concurrency split as the legacy pair, and the
gate has to admit them or the rename stops the bot firing. Both counts
go to four and both literals name all four operands, so a half-finished
rename still reports.

Marked TRANSITIONAL at each site, matching the comment in backport.yaml:
when dosubot's PR labelling is switched off and the legacy arms are
dropped, these go back to the pair in the same pass.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 13:08:16 +05:00
Myasnikov Daniil a0c297a08a docs(ci): mark the legacy backport label arms as transitional
The dual acceptance exists so the rename can land in any order relative
to disabling dosubot's PR labelling, not because two spellings are
wanted long term. Naming the deletion sites keeps that from becoming
permanent by default.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 13:08:16 +05:00
Myasnikov Daniil 624a424d29 fix(ci): keep the backport concurrency guard working for namespaced labels
The guard from #3569 appends -label to the concurrency group for any
labeled event that is not a backport label, so a backport label never
cancels a run already in flight. It enumerated the two legacy names
only, so once the labels are namespaced a kind/backport event takes
the -label branch and the guard stops covering the case it exists for.

Both spellings are listed for the same reason the trigger lists both:
the org-level dosubot still applies the legacy names.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 13:08:15 +05:00
IvanHunters 0fa6cf72a5 chore(ci): complete the backport label rename across all consumers
The initial rename missed several literal consumers of the old label
names beyond labels.yml and backport.yaml:

- stale.yaml exempt-pr-labels still listed backport,backport-previous.
  actions/stale matches label names exactly and does not resolve
  label-sync aliases, so after the rename a kind/backport PR would lose
  its stale exemption and get auto-closed after inactivity. This is the
  one behavioural regression; the rest are documentation drift.
- PULL_REQUEST_TEMPLATE.md told contributors to apply the backport label.
- release.md label-target table and the gh pr list --search commands
  referenced the old names (the searches would silently return nothing).
- contributing.md claimed a backport label is auto-applied on the
  [Backport] prefix; pr-labeler only adds area/release, so the line was
  already stale and is corrected while renaming.

Historical narrative in release.md and the backport scope-to-area doc
rows (a Conventional-Commit scope, not the label) are left as-is.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-27 13:08:15 +05:00
IvanHunters 7b71053a0b chore(ci): namespace the backport labels under kind/
The labels.yml header declares that names follow the Kubernetes scheme,
but backport and backport-previous were top-level, violating it. Rename
them to kind/backport and kind/backport-previous, grouped under the kind/
section, and carry the old names as aliases so EndBug/label-sync renames
in place without dropping history on already-tagged issues and PRs.

Update the label literals in backport.yaml in lockstep (the if filter and
the label-detection script). pr-labeler.yaml's 'backport' entry is a
Conventional-Commit scope-to-area mapping, not a label reference, so it is
left unchanged. The top-level release label is a functional trigger keyed
by name in several workflows; renaming it is a wider cross-workflow change
out of scope here, so it is left as-is intentionally.

Signed-off-by: IvanHunters <xorokhotnikov@gmail.com>
2026-08-27 13:08:15 +05:00
Myasnikov Daniil fdb2e89fe7 test(unit): make the bats trace stream opt-in, quiet on the unit lane
hack/cozytest.sh runs every @test body under `set -x` and streams the
trace live. Across the ~40 hack/*.bats unit suites that is 233,444 of
238,794 lines - 98% of a 31 MB job log - and the single failing
assertion sits at line 238,573 of it. The web log viewer is unusable at
that size, and anyone (or anything) reading the job to find out what
broke pays for the whole file first.

Nothing in that stream is load-bearing on the unit lane. run_one
already tees the raw output to $log and the fail handler dumps all of
it for whichever test fails, so suppressing the live stream drops the
output of PASSING tests only.

Add COZYTEST_TRACE, default 1 so the e2e call sites in
packages/core/testing/Makefile keep the live stream - there it is the
only progress signal, and the only record at all when a step timeout or
a dead runner kills the job before the fail handler can run. The
bats-unit-tests target sets 0; running the runner by hand stays
verbose, which is the case where a human is actually watching.

Measured on 25 suites locally, quiet vs verbose: 29,528 -> 951 lines,
3.9 MB -> 94 KB, and 71s -> 48s wall, the last because the per-line
formatter forks date twice for every line it prints. Quiet output is
byte-for-byte the non-trace subset of verbose, and a hanging test still
shows as a ╭ with no matching ╰.

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Myasnikov Daniil <myasnikovdaniil2001@gmail.com>
2026-08-27 12:19:17 +05:00
Andrei Kvapil 02c6942adc fix(kubeovn-webhook): restrict the webhook port to the API server (#3955)
## What this PR does

Closes the exposure behind GHSA-g883-q79m-8225 by constraining who can
reach the kube-ovn webhook's serving port. The `/mutate-pods` handler
returns namespace-derived annotations to any caller that reaches it, and
until now nothing stopped an arbitrary in-cluster pod from being that
caller.

### Why the client-certificate path does not close it

cozystack/cozystack#2698 added `--client-ca-file`, deliberately off by
default, as observation mode. Turning it on requires the API server to
**present** a certificate, which it does only when its
`AdmissionConfiguration` names a `kubeConfigFile` for the webhook
plugins.

Checked against a running Cozystack control plane rather than assumed:
the `AdmissionConfiguration` there carries a `PodSecurity` plugin block
and nothing else, so no client certificate is offered to admission
webhooks at all. Flipping to `RequireAndVerifyClientCert` on such a
cluster would reject the API server itself, and with `failurePolicy:
Fail` that stops pod creation cluster-wide.

This matters for how the remaining work was scoped: the plan had been to
observe issuer logs for a day and then pin a CA. No amount of
observation changes the outcome, because the absence is a property of
the control plane's configuration, not of the traffic. Making
`--client-ca-file` usable is a separate, larger change to how the
platform configures the API server, and it is not a prerequisite for
closing the exposure.

### What the policy does

Denies pod traffic and world traffic to the serving port, and needs no
carve-out for the legitimate caller: the API server runs on the host
network, so it is not a Cilium endpoint and `fromEndpoints` never
matches it. Unlike the ingress-nginx admission webhook this component
exists only on the management cluster, so there is no
konnectivity-tunnelled caller to exempt either — and consequently no
forgeable pod label anywhere in the selector.

It is a `CiliumNetworkPolicy` rather than a core `NetworkPolicy`, and
that distinction is the reason it works at all:
`packages/system/kubeovn/values.yaml` sets `ENABLE_NP: false`, so
kube-ovn programs no ACLs and a core NetworkPolicy would be created and
never enforced. The values comment that claimed NetworkPolicy was simply
not an option is corrected — it was true only of the core kind.
`webhookPolicy.enabled` lets a cluster whose networking variant ships no
Cilium turn the object off.

Deny-only with default-deny explicitly disabled, matching the
ingress-nginx admission policy: putting these pods behind a default-deny
would mean enumerating every port they serve and breaking traffic on the
first one forgotten.

### Verification

Eight helm-unittest cases cover the port, the all-namespaces span, the
absence of any label-based exemption, the world deny, the disabled
default-deny, the Cilium kind, the selector and the off switch. The
rendered object was additionally accepted by a live API server via
`kubectl apply --dry-run=server`, so the CRD validates it as written.

Not verified: that a denied pod actually fails to reach the port at
runtime. That needs a live negative test against the policy, which is
the same gap the ingress-nginx admission policy carries.

### Screenshots

Not applicable — no UI change.

### Downstream repositories

Walked the trigger map against the diff. Nothing is reached: the change
is one chart template, its values and its tests under
`packages/system/`, with no package added or removed under
`packages/apps/` or `packages/extra/`, no
`packages/core/platform/values.yaml` key, no platform variant or bundle
change, nothing under `hack/`, and no node prerequisite.

- [x] No downstream repository is affected by this change
- [ ] [cozystack/website](https://github.com/cozystack/website) -
follow-up:
- [ ]
[cozystack/terraform-provider-cozystack](https://github.com/cozystack/terraform-provider-cozystack)
- follow-up:
- [ ]
[cozystack/ansible-cozystack](https://github.com/cozystack/ansible-cozystack)
- follow-up:
- [ ] [cozystack/ccp](https://github.com/cozystack/ccp) - follow-up:
- [ ] [cozystack/talm](https://github.com/cozystack/talm) - follow-up:
- [ ] [cozystack/cozyhr](https://github.com/cozystack/cozyhr) -
follow-up:
- [ ] [cozystack/cozy-proxy](https://github.com/cozystack/cozy-proxy) -
follow-up:
- [ ]
[cozystack/cozystack-telemetry-server](https://github.com/cozystack/cozystack-telemetry-server)
- follow-up:
- [ ]
[cozystack/external-apps-example](https://github.com/cozystack/external-apps-example)
- follow-up:
- [ ] [cozystack/examples](https://github.com/cozystack/examples) -
follow-up:

### Release note

```release-note
fix(kubeovn-webhook): the kube-ovn webhook's serving port is now closed to in-cluster pods and external traffic by a CiliumNetworkPolicy. It previously answered any in-cluster pod with namespace-derived annotations (GHSA-g883-q79m-8225). Disable with webhookPolicy.enabled=false on clusters whose networking variant ships no Cilium.
```


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added a Cilium network policy to protect the namespace-annotation
webhook.
* The policy is enabled by default and restricts access to the webhook
port, blocking unauthorized in-cluster and external traffic.
* Standardized the webhook port across its deployment, service, and
network policy configuration.

* **Bug Fixes**
* Improved webhook network isolation when standard Kubernetes
NetworkPolicy enforcement is unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-26 23:04:24 +02:00