Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f93b4a4426 | ||
|
|
d698267fba | ||
|
|
f541254de7 | ||
|
|
39bc6ee0c5 | ||
|
|
3e9bda9031 | ||
|
|
eba1381ef2 | ||
|
|
b444e0c680 | ||
|
|
733ba71173 | ||
|
|
1489f68796 | ||
|
|
f4b41aca84 | ||
|
|
5494363221 |
@@ -98,6 +98,17 @@ jobs:
|
|||||||
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
[ -n "${GITHUB_STEP_SUMMARY:-}" ] || exit 0
|
||||||
python3 infra/trx-summary.py TestResults >> "$GITHUB_STEP_SUMMARY"
|
python3 infra/trx-summary.py TestResults >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
# The docs site must build with --strict (#173). setup-python so `make docs` can
|
||||||
|
# create its venv regardless of what the runner image ships.
|
||||||
|
docs:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
- uses: https://github.com/actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
- run: make docs
|
||||||
|
|
||||||
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
|
# Frontend (Nx/Angular) lane: install with pnpm, then Nx lint + test + build.
|
||||||
frontend:
|
frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -207,8 +218,14 @@ jobs:
|
|||||||
# dispatched (gitea-actions-gotchas.md §7). Default `if: success()` dispatches normally. Cost: a
|
# dispatched (gitea-actions-gotchas.md §7). Default `if: success()` dispatches normally. Cost: a
|
||||||
# failing mutation ratchet now skips verify-stack instead of running it anyway; the fix-and-re-push
|
# failing mutation ratchet now skips verify-stack instead of running it anyway; the fix-and-re-push
|
||||||
# re-run exercises verify-stack, so we still get the signal.
|
# re-run exercises verify-stack, so we still get the signal.
|
||||||
|
#
|
||||||
|
# Main only, not on PRs: the runner shares the lab node with the deployed stack, and a second
|
||||||
|
# full stack per PR was what got the runner OOM-killed (#182). PRs still gate on every job above;
|
||||||
|
# the live-stack check runs once per merge. A plain event `if` keeps the implicit success(), so it
|
||||||
|
# is not the status-function case from gotchas §7.
|
||||||
verify-stack:
|
verify-stack:
|
||||||
needs: [mutation]
|
needs: [mutation]
|
||||||
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
@@ -231,6 +248,9 @@ jobs:
|
|||||||
- name: RegisterRecord objecttype registered + published
|
- name: RegisterRecord objecttype registered + published
|
||||||
id: registerrecord
|
id: registerrecord
|
||||||
run: REGISTERRECORD_TIMEOUT=120 make verify-registerrecord
|
run: REGISTERRECORD_TIMEOUT=120 make verify-registerrecord
|
||||||
|
- name: ClamAV scans a stream (EICAR found, clean OK)
|
||||||
|
id: clamav
|
||||||
|
run: CLAMAV_TIMEOUT=120 make verify-clamav
|
||||||
- name: ACL ↔ OpenZaak integration tests
|
- name: ACL ↔ OpenZaak integration tests
|
||||||
id: acl
|
id: acl
|
||||||
run: make verify-acl
|
run: make verify-acl
|
||||||
@@ -270,6 +290,7 @@ jobs:
|
|||||||
OBJECTEN: ${{ steps.objecten.outcome }}
|
OBJECTEN: ${{ steps.objecten.outcome }}
|
||||||
REGISTERRECORD: ${{ steps.registerrecord.outcome }}
|
REGISTERRECORD: ${{ steps.registerrecord.outcome }}
|
||||||
OBJECTEN_NOTIFICATIONS: ${{ steps.objecten_nrc.outcome }}
|
OBJECTEN_NOTIFICATIONS: ${{ steps.objecten_nrc.outcome }}
|
||||||
|
CLAMAV: ${{ steps.clamav.outcome }}
|
||||||
ACL: ${{ steps.acl.outcome }}
|
ACL: ${{ steps.acl.outcome }}
|
||||||
NRC: ${{ steps.nrc.outcome }}
|
NRC: ${{ steps.nrc.outcome }}
|
||||||
PROJECTION: ${{ steps.projection.outcome }}
|
PROJECTION: ${{ steps.projection.outcome }}
|
||||||
@@ -292,6 +313,7 @@ jobs:
|
|||||||
echo "| Objecten API + token | $(icon "$OBJECTEN") |"
|
echo "| Objecten API + token | $(icon "$OBJECTEN") |"
|
||||||
echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |"
|
echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |"
|
||||||
echo "| Objecten → NRC | $(icon "$OBJECTEN_NOTIFICATIONS") |"
|
echo "| Objecten → NRC | $(icon "$OBJECTEN_NOTIFICATIONS") |"
|
||||||
|
echo "| ClamAV INSTREAM scan | $(icon "$CLAMAV") |"
|
||||||
echo "| ACL ↔ OpenZaak | $(icon "$ACL") |"
|
echo "| ACL ↔ OpenZaak | $(icon "$ACL") |"
|
||||||
echo "| OpenZaak → NRC | $(icon "$NRC") |"
|
echo "| OpenZaak → NRC | $(icon "$NRC") |"
|
||||||
echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |"
|
echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |"
|
||||||
@@ -311,7 +333,7 @@ jobs:
|
|||||||
# Log dump must precede teardown (which removes the containers).
|
# Log dump must precede teardown (which removes the containers).
|
||||||
- name: Dump container logs on failure
|
- name: Dump container logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel beheer objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten objecten-celery registerrecord-init tempo prometheus grafana 2>&1 || true
|
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel beheer objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten objecten-celery registerrecord-init clamav tempo prometheus grafana 2>&1 || true
|
||||||
- name: Tear down
|
- name: Tear down
|
||||||
if: always()
|
if: always()
|
||||||
run: make down
|
run: make down
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ jobs:
|
|||||||
# `true` fills in the medewerker OTP step for the public demo (chart value
|
# `true` fills in the medewerker OTP step for the public demo (chart value
|
||||||
# demo.otpAutofill). The fixture secret is committed: demo only.
|
# demo.otpAutofill). The fixture secret is committed: demo only.
|
||||||
OTP_AUTOFILL: ${{ vars.OTP_AUTOFILL }}
|
OTP_AUTOFILL: ${{ vars.OTP_AUTOFILL }}
|
||||||
|
# Tempo for the services' traces, e.g. http://tempo.monitoring.svc:4317 (the
|
||||||
|
# cluster monitoring stack, Infra repo). Empty = the chart default.
|
||||||
|
OTEL_ENDPOINT: ${{ vars.OTEL_ENDPOINT }}
|
||||||
steps:
|
steps:
|
||||||
- uses: https://github.com/actions/checkout@v4
|
- uses: https://github.com/actions/checkout@v4
|
||||||
|
|
||||||
@@ -100,7 +103,7 @@ jobs:
|
|||||||
make k8s-reseed \
|
make k8s-reseed \
|
||||||
TALOS_HOST=${TALOS_HOST:-localhost} \
|
TALOS_HOST=${TALOS_HOST:-localhost} \
|
||||||
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
|
K8S_REGISTRY=${TALOS_VM_IP:-192.168.122.173}:30500 \
|
||||||
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL} --set demo.otpAutofill=${OTP_AUTOFILL:-false}"
|
K8S_SET="${KEYCLOAK_URL:+--set keycloakUrl=$KEYCLOAK_URL} --set demo.otpAutofill=${OTP_AUTOFILL:-false}${OTEL_ENDPOINT:+ --set otelEndpoint=$OTEL_ENDPOINT}"
|
||||||
|
|
||||||
# `dev` is a mutable tag and helm sees an unchanged pod template, so the
|
# `dev` is a mutable tag and helm sees an unchanged pod template, so the
|
||||||
# new images only land on a restart (pullPolicy is already Always).
|
# new images only land on a restart (pullPolicy is already Always).
|
||||||
|
|||||||
@@ -61,3 +61,7 @@ __pycache__/
|
|||||||
TestResults/
|
TestResults/
|
||||||
test-output/
|
test-output/
|
||||||
tests/e2e/playwright-report.json
|
tests/e2e/playwright-report.json
|
||||||
|
|
||||||
|
# MkDocs build (`make docs`)
|
||||||
|
.venv-docs/
|
||||||
|
site/
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ COMPOSE := infra/docker-compose.yml
|
|||||||
# Long-running services with a healthcheck — the smoke polls these for readiness
|
# Long-running services with a healthcheck — the smoke polls these for readiness
|
||||||
# (infra/wait-healthy.sh). One-shot init jobs (oz-init, nrc-init, flowable-init)
|
# (infra/wait-healthy.sh). One-shot init jobs (oz-init, nrc-init, flowable-init)
|
||||||
# are not polled; they only need to have run. See docs/runbooks/gitea-actions-gotchas.md.
|
# are not polled; they only need to have run. See docs/runbooks/gitea-actions-gotchas.md.
|
||||||
WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api self-service openbaar behandel beheer objecttypen objecten
|
WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api self-service openbaar behandel beheer objecttypen objecten clamav
|
||||||
# Config files (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are streamed
|
# Config files (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are streamed
|
||||||
# into external named volumes via `docker cp` (infra/seed-config.sh) instead of
|
# into external named volumes via `docker cp` (infra/seed-config.sh) instead of
|
||||||
# bind-mounted, because bind mounts don't reach sibling containers on the
|
# bind-mounted, because bind mounts don't reach sibling containers on the
|
||||||
@@ -43,11 +43,11 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-drift k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
|
.PHONY: ci lint build unit mutation frontend docs integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-clamav verify-notifications smoke up down local verify-local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down k8s-lint k8s-drift k8s-registry k8s-images k8s-seed k8s-up k8s-reseed k8s-portals k8s-down k8s-purge help
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
||||||
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
||||||
ci: lint build unit mutation frontend verify
|
ci: lint build unit mutation frontend docs verify
|
||||||
|
|
||||||
## frontend: install deps and run the Nx lint/test/build for the portals (pnpm + Node required)
|
## frontend: install deps and run the Nx lint/test/build for the portals (pnpm + Node required)
|
||||||
# Tests run in their own phase, ahead of the build. The @angular/build:unit-test
|
# Tests run in their own phase, ahead of the build. The @angular/build:unit-test
|
||||||
@@ -81,6 +81,15 @@ unit:
|
|||||||
python3 infra/test_playwright_summary.py
|
python3 infra/test_playwright_summary.py
|
||||||
python3 infra/test_portal_caddyfiles.py
|
python3 infra/test_portal_caddyfiles.py
|
||||||
|
|
||||||
|
## docs: build the MkDocs site with --strict (a broken link or nav entry fails)
|
||||||
|
# Pinned in a throwaway venv: Material 9.7 is the last line on MkDocs 1.x, and MkDocs
|
||||||
|
# 2.0 drops the plugin/theme system this site relies on. Publishing is a separate
|
||||||
|
# decision (#173); this only proves the site builds.
|
||||||
|
docs:
|
||||||
|
python3 -m venv .venv-docs
|
||||||
|
.venv-docs/bin/pip install --quiet mkdocs==1.6.1 mkdocs-material==9.7.7
|
||||||
|
.venv-docs/bin/mkdocs build --strict
|
||||||
|
|
||||||
## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline)
|
## mutation: run the Stryker.NET ratchet on each service with branching logic (fails below baseline)
|
||||||
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
# Stryker is pinned as a local dotnet tool (.config/dotnet-tools.json); `tool restore`
|
||||||
# makes `make mutation` work from a fresh clone. Each service owns its config + break
|
# makes `make mutation` work from a fresh clone. Each service owns its config + break
|
||||||
@@ -213,6 +222,11 @@ verify-registerrecord:
|
|||||||
verify-objecten-notifications:
|
verify-objecten-notifications:
|
||||||
bash infra/run-objecten-notifications-check.sh
|
bash infra/run-objecten-notifications-check.sh
|
||||||
|
|
||||||
|
## verify-clamav: assert clamd detects EICAR and passes a clean stream over INSTREAM (S-28),
|
||||||
|
## against the already-running stack.
|
||||||
|
verify-clamav:
|
||||||
|
bash infra/run-clamav-check.sh
|
||||||
|
|
||||||
## verify: local mirror of the CI verify-stack job — full stack up once, all checks,
|
## verify: local mirror of the CI verify-stack job — full stack up once, all checks,
|
||||||
## tear down (always). For fast single-concern local iteration use `integration`
|
## tear down (always). For fast single-concern local iteration use `integration`
|
||||||
## (oz-only) or `verify-notifications` (oz+nrc) instead.
|
## (oz-only) or `verify-notifications` (oz+nrc) instead.
|
||||||
@@ -221,6 +235,7 @@ verify:
|
|||||||
docker compose -f $(COMPOSE) up -d --build
|
docker compose -f $(COMPOSE) up -d --build
|
||||||
@bash -c 'set -e; rc=0; \
|
@bash -c 'set -e; rc=0; \
|
||||||
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) \
|
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) \
|
||||||
|
&& bash infra/run-clamav-check.sh \
|
||||||
&& bash infra/run-acl-integration.sh \
|
&& bash infra/run-acl-integration.sh \
|
||||||
&& bash infra/run-notification-check.sh \
|
&& bash infra/run-notification-check.sh \
|
||||||
&& bash infra/run-projection-check.sh \
|
&& bash infra/run-projection-check.sh \
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# ADR-0035: The deployed stack is published through the existing labs Caddy
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-09-25
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Slice:** [#177](https://git.labs.respellion.tech/eho/register-referentie/issues/177) —
|
||||||
|
that issue proposed the opposite (an in-cluster Caddy edge); this ADR records why the
|
||||||
|
host-side option won. Implemented in #179, #180 and #181.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The stack deploys to a single-node Talos VM (ADR-0033, #175). Until now it was only usable
|
||||||
|
through five SSH port-forwards: the portals' OIDC flow uses PKCE, PKCE needs
|
||||||
|
`crypto.subtle`, and browsers expose that only in a **secure context**, meaning HTTPS or a
|
||||||
|
`localhost` origin. A NodePort on the VM's address is neither. We want a URL a demo
|
||||||
|
audience can simply open.
|
||||||
|
|
||||||
|
Three facts about where things run shape the answer:
|
||||||
|
|
||||||
|
- The Talos VM is a libvirt guest on a **Fedora hypervisor in the office**, behind NAT
|
||||||
|
with no public address. The only way in from outside is an existing reverse SSH tunnel
|
||||||
|
(`autossh-reverse-tunnel.service`) into an `openssh-server` container on the labs
|
||||||
|
server.
|
||||||
|
- The **labs server** (public IP) already runs Caddy for `*.labs.respellion.tech`, with
|
||||||
|
the wildcard certificate (DNS-01 via Cloudflare) and ports 80/443. Every other labs
|
||||||
|
service is published there (repo `Infra`, `infra/development/`).
|
||||||
|
- #177 proposed a Caddy **inside the cluster**, fed by a layer-4 forward on the host, so
|
||||||
|
that routing and certificates would be cluster state. That assumes the public IP is on
|
||||||
|
the hypervisor. It isn't: the hypervisor has no inbound path, and 80/443 on the labs
|
||||||
|
server are already taken by the labs Caddy.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**Publish the portals and Keycloak through the existing labs Caddy. Carry the traffic to
|
||||||
|
the cluster over a second reverse SSH tunnel from the hypervisor.**
|
||||||
|
|
||||||
|
```
|
||||||
|
browser ─https─▶ labs Caddy ─▶ openssh-server:3014x/30180
|
||||||
|
─reverse SSH tunnel─▶ Fedora hypervisor ─▶ Talos NodePorts
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Hostnames** under the existing wildcard: `big-register` (openbaar), `big-mijn`
|
||||||
|
(self-service), `big-behandel`, `big-beheer`, and `big-auth` (Keycloak, with `/admin*`
|
||||||
|
answered 404).
|
||||||
|
- **Tunnel:** `big-portals-tunnel.service` on the hypervisor (repo `Infra`)
|
||||||
|
reverse-forwards the five browser-facing NodePorts into `openssh-server`. It is
|
||||||
|
separate from the access tunnel on `:6667`, so a failed forward can't cut SSH access.
|
||||||
|
Caddy joins the `openssh_default` network to reach the tunnel ends.
|
||||||
|
- **Keycloak's issuer** is the public origin. The chart value `keycloakUrl` replaces
|
||||||
|
`host` + NodePort in one helper, `big.keycloakUrl`, which feeds both `KC_HOSTNAME` and
|
||||||
|
the portals' `config.json` authority, so the two cannot drift (ADR-0010). The deploy
|
||||||
|
workflow sets it from the `KEYCLOAK_URL` repository variable.
|
||||||
|
- **`KC_PROXY_HEADERS=xforwarded`:** `KC_HOSTNAME_BACKCHANNEL_DYNAMIC` builds the token,
|
||||||
|
userinfo and certs URLs from the request. That request reaches Keycloak as plain HTTP,
|
||||||
|
so the URLs came out `http://` and browsers blocked them as mixed content. Trusting
|
||||||
|
Caddy's `X-Forwarded-Proto` keeps them HTTPS. In-cluster calls send no such header and
|
||||||
|
still use `keycloak:8080`.
|
||||||
|
- **Demo MFA (optional):** `demo.otpAutofill` (`OTP_AUTOFILL`) makes the `big-demo` theme
|
||||||
|
(`infra/keycloak/themes/big-demo`) Keycloak's default. Its script fills in and submits
|
||||||
|
the medewerker OTP from the fixture secret (ADR-0031), so the step is visibly enforced
|
||||||
|
without an authenticator. It is off by default.
|
||||||
|
|
||||||
|
### Alternatives considered
|
||||||
|
|
||||||
|
- **In-cluster Caddy edge (#177, PR #178).** It would keep routes and certificates in
|
||||||
|
cluster state. But it needs a public inbound path to the hypervisor that doesn't exist,
|
||||||
|
plus a second certificate authority beside the labs Caddy, which already holds the
|
||||||
|
wildcard. Closed unmerged.
|
||||||
|
- **Port-forward on the office router to the hypervisor.** This opens the office network
|
||||||
|
itself to the internet. Rejected.
|
||||||
|
- **Move the cluster to a host with a public IP.** It would remove the tunnel, but it's a
|
||||||
|
bigger change than publishing one demo. It remains the natural step if the stack
|
||||||
|
outgrows a lab VM.
|
||||||
|
- **Keep the SSH port-forwards.** Fine for one developer, but not something you can send
|
||||||
|
to someone.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**
|
||||||
|
|
||||||
|
- Real hostnames and HTTPS, so PKCE works in any browser with no client-side setup.
|
||||||
|
- No new certificate handling: the labs Caddy's wildcard covers the new hosts.
|
||||||
|
- The chart stays edge-agnostic. With `keycloakUrl` empty it renders exactly as before,
|
||||||
|
so compose, CI and the `localhost` workflow are untouched.
|
||||||
|
|
||||||
|
**Negative / costs**
|
||||||
|
|
||||||
|
- **Routing lives outside the cluster**, in the Infra repo's Caddyfile. That is exactly
|
||||||
|
what #177 wanted to avoid. Adding a portal means changing three places: a NodePort in
|
||||||
|
the chart, a forward in the tunnel unit, and a host in the Caddyfile.
|
||||||
|
- **Two SSH hops in the data path.** If the hypervisor or the tunnel is down, the
|
||||||
|
portals return 502 even though the cluster is healthy.
|
||||||
|
- **One issuer string.** With `keycloakUrl` set, the `localhost` port-forward workflow
|
||||||
|
(runbook §5) can no longer log in.
|
||||||
|
- **Keycloak trusts `X-Forwarded-*`** from anything that reaches it. Today that is only
|
||||||
|
in-cluster callers and the tunnel. `KC_PROXY_TRUSTED_ADDRESSES` can narrow it if the
|
||||||
|
NodePort is ever exposed more widely.
|
||||||
|
- **The portals are public.** Anyone with the link can log in with the committed test
|
||||||
|
credentials, and with `OTP_AUTOFILL` on, no second factor stands in the way. That is
|
||||||
|
acceptable for synthetic data. Put the labs Caddy's Azure `authorize` in front of the
|
||||||
|
`big-*` hosts if the audience must be restricted.
|
||||||
|
|
||||||
|
**Follow-up**
|
||||||
|
|
||||||
|
- Runbook: `docs/runbooks/kubernetes-talos.md`, "Publishing through the labs Caddy".
|
||||||
|
- Dev-mode Keycloak generates new signing keys on every restart, and the BFF re-fetches
|
||||||
|
them at most every 5 minutes, so expect a few minutes of 401s after a Keycloak restart.
|
||||||
|
Persisting Keycloak's database (runbook §6) would remove that.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# ADR-0036: Uploaded documents are scanned by ClamAV in the Domain Service, fail closed
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-10-02
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Slice:** proposed in [#190](https://git.labs.respellion.tech/eho/register-referentie/issues/190);
|
||||||
|
clamd deployed in [#191](https://git.labs.respellion.tech/eho/register-referentie/issues/191) (S-28),
|
||||||
|
scanning wired in [#192](https://git.labs.respellion.tech/eho/register-referentie/issues/192) (S-29).
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A zorgprofessional's diploma upload goes portal → BFF → Domain (`ProvideDocuments`) →
|
||||||
|
ACL → OpenZaak. Nothing on that path looks at the file. It is not checked for malware,
|
||||||
|
and nobody checks that it is a PDF. Behandelaars open these files later, so the
|
||||||
|
register stores, and then serves, whatever a citizen sends.
|
||||||
|
|
||||||
|
Scanning needs a signature engine that stays up to date. That means a new peer service,
|
||||||
|
and that makes it an ADR (CLAUDE.md §14).
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **Engine:** the ClamAV daemon (`clamd`), official image `clamav/clamav`, pinned tag,
|
||||||
|
as its own service in compose and in the Helm chart. `freshclam` in the same container
|
||||||
|
keeps the signatures current, and they live on a volume.
|
||||||
|
2. **Where the check lives:** in the **Domain Service**, behind an `IDocumentScanner` port
|
||||||
|
in `Big.Application`. "Only a clean PDF is stored and unblocks beoordeling" is a rule
|
||||||
|
of the provide-documents use case. The BFF is a thin proxy (§8.3), and the ACL
|
||||||
|
translates ZGW and nothing else (§8.1). A check in the domain also covers every
|
||||||
|
entry point, not just the portal.
|
||||||
|
3. **Protocol:** the adapter in `Big.Infrastructure` speaks clamd's INSTREAM protocol over
|
||||||
|
`TcpClient`: `zINSTREAM\0`, length-prefixed chunks, a zero-length terminator, then a
|
||||||
|
`stream: OK` or `stream: <name> FOUND` reply. That is a few lines of code, so we add
|
||||||
|
**no NuGet package** for it (nClam and similar).
|
||||||
|
4. **Fail closed:** if clamd can't be reached, the upload is refused (503). Nothing is
|
||||||
|
stored and the document wait stays open. We never store an unscanned file.
|
||||||
|
5. **Type check:** content must also start with `%PDF-`, checked **after** the scan.
|
||||||
|
clamd matches EICAR (and many real signatures) only at the start of a file, so a type
|
||||||
|
check in front of the scan would report malware as merely "not a PDF". The check also
|
||||||
|
refuses a renamed non-PDF that is clean.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- One more long-running service. clamd holds its signatures in memory (about 1 GB idle).
|
||||||
|
`ConcurrentDatabaseReload no` stops a signature reload from holding a second copy,
|
||||||
|
but clamd pauses scans for the few seconds a reload takes. Compose caps it at
|
||||||
|
`mem_limit: 2g`, and the chart requests 1200Mi. This counts against the verify-stack
|
||||||
|
runner's memory ceiling (#182).
|
||||||
|
- The first start downloads about 300 MB of signatures from the ClamAV CDN, so the
|
||||||
|
runner and the cluster node need outbound internet (as `seed-zaaktype` already does).
|
||||||
|
The CDN rate-limits by IP. A CI runner that starts fresh often can get throttled, and
|
||||||
|
then the health check doesn't go green. If that happens, mirror the signatures
|
||||||
|
(`cvdupdate`) rather than retrying.
|
||||||
|
- Tests use the EICAR test string, built from two halves so the repo itself does not trip
|
||||||
|
an on-access scanner. No real malware is ever committed.
|
||||||
|
- Infected or non-PDF uploads are refused with 422 and a business message. We don't keep
|
||||||
|
a quarantine copy: a refused file is simply not stored.
|
||||||
+6
-2
@@ -19,9 +19,10 @@ and CI cannot drift:
|
|||||||
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
||||||
| `unit` | `make unit` → `dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
|
| `unit` | `make unit` → `dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
|
||||||
| `frontend` | `make frontend` → Nx lint/test/build for the four portals | pnpm + Node |
|
| `frontend` | `make frontend` → Nx lint/test/build for the four portals | pnpm + Node |
|
||||||
|
| `docs` | `make docs` → `mkdocs build --strict` in a pinned venv (fails on a broken link or nav entry; the site is not published yet, #173) | Python 3 |
|
||||||
| `k8s` | `make k8s-lint` (render + schema-check the Helm chart) → `make k8s-drift` (chart still describes the same stack as `infra/docker-compose.yml`) | pinned `helm` binary + `docker compose` |
|
| `k8s` | `make k8s-lint` (render + schema-check the Helm chart) → `make k8s-drift` (chart still describes the same stack as `infra/docker-compose.yml`) | pinned `helm` binary + `docker compose` |
|
||||||
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
| `mutation` | `make mutation` → `dotnet tool restore` → `dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
|
||||||
| `verify-stack` | the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
| `verify-stack` | **push to `main` only, skipped on PRs** (#182) — the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
|
||||||
|
|
||||||
> **Why one `verify-stack` job, not three.** The single self-hosted runner runs jobs
|
> **Why one `verify-stack` job, not three.** The single self-hosted runner runs jobs
|
||||||
> **sequentially**, so booting OpenZaak once (instead of once per check) is the
|
> **sequentially**, so booting OpenZaak once (instead of once per check) is the
|
||||||
@@ -57,9 +58,12 @@ dotnet tool (`.config/dotnet-tools.json`), so it runs identically locally and in
|
|||||||
make mutation # dotnet tool restore + dotnet stryker on the ACL
|
make mutation # dotnet tool restore + dotnet stryker on the ACL
|
||||||
```
|
```
|
||||||
|
|
||||||
Config lives in [`services/acl/stryker-config.json`](../../services/acl/stryker-config.json).
|
Config lives in `services/acl/stryker-config.json`.
|
||||||
It runs in **solution mode** against `Acl.slnx`, mutating the two projects under test
|
It runs in **solution mode** against `Acl.slnx`, mutating the two projects under test
|
||||||
(`Acl.Application`, `Acl.Infrastructure`); `Acl.Api` has no tests and is skipped.
|
(`Acl.Application`, `Acl.Infrastructure`); `Acl.Api` has no tests and is skipped.
|
||||||
|
`Acl.slnx` leaves out `Acl.IntegrationTests`: it needs a live OpenZaak, and Stryker
|
||||||
|
runs every test project in the solution, so keeping it in makes 8 tests fail in the
|
||||||
|
initial run (#174).
|
||||||
|
|
||||||
**Baseline (the ratchet):** the ACL is the first service with branching logic, so it
|
**Baseline (the ratchet):** the ACL is the first service with branching logic, so it
|
||||||
sets the repo-wide baseline. Observed score **95%**; enforced `break` threshold **90%**
|
sets the repo-wide baseline. Observed score **95%**; enforced `break` threshold **90%**
|
||||||
|
|||||||
@@ -356,6 +356,7 @@ immutable, so `helm upgrade` is rejected with `cannot patch "…" with kind Job`
|
|||||||
| Login redirects but the portal stays logged out, or the BFF answers 401 | `TALOS_HOST` doesn't match the address in the browser's URL bar — issuer mismatch. Re-run `make k8s-up` with the right value |
|
| Login redirects but the portal stays logged out, or the BFF answers 401 | `TALOS_HOST` doesn't match the address in the browser's URL bar — issuer mismatch. Re-run `make k8s-up` with the right value |
|
||||||
| A portal returns 502 on `/self-service/…` | the BFF is unreachable from the portal pod: check `kubectl -n big get svc bff` and the BFF's own readiness |
|
| A portal returns 502 on `/self-service/…` | the BFF is unreachable from the portal pod: check `kubectl -n big get svc bff` and the BFF's own readiness |
|
||||||
| Public register empty after a submit | usually a wiped `emptyDir` database (§6): `make k8s-reseed`. Confirm with `kubectl -n big logs deploy/event-subscriber \| grep 42P01` |
|
| Public register empty after a submit | usually a wiped `emptyDir` database (§6): `make k8s-reseed`. Confirm with `kubectl -n big logs deploy/event-subscriber \| grep 42P01` |
|
||||||
|
| `clamav` not Ready for minutes | first start downloads ~300 MB of signatures, which needs outbound internet. A `429`/`cool-down` in `kubectl -n big logs deploy/clamav` means the ClamAV CDN is throttling this IP (ADR-0036) |
|
||||||
| `helm upgrade` fails with `cannot patch … with kind Job` | see §7 — use `make k8s-reseed` |
|
| `helm upgrade` fails with `cannot patch … with kind Job` | see §7 — use `make k8s-reseed` |
|
||||||
| Pods `Evicted` / `OOMKilled` | the VM is too small (§0) |
|
| Pods `Evicted` / `OOMKilled` | the VM is too small (§0) |
|
||||||
| A Job shows `BackoffLimitExceeded` | read it: `kubectl -n big logs job/<name>` |
|
| A Job shows `BackoffLimitExceeded` | read it: `kubectl -n big logs job/<name>` |
|
||||||
@@ -403,6 +404,8 @@ upgrade path.
|
|||||||
|
|
||||||
## Publishing through the labs Caddy
|
## Publishing through the labs Caddy
|
||||||
|
|
||||||
|
Why this route and not an in-cluster edge: [ADR-0035](../architecture/adr-0035-public-access-through-the-labs-caddy.md).
|
||||||
|
|
||||||
The portals can be reached on real hostnames through the Caddy that already fronts
|
The portals can be reached on real hostnames through the Caddy that already fronts
|
||||||
`*.labs.respellion.tech` (repo `Infra`, `infra/development/`). The chain:
|
`*.labs.respellion.tech` (repo `Infra`, `infra/development/`). The chain:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""S-28 (#191): prove clamd is up, has signatures loaded, and scans a stream over INSTREAM.
|
||||||
|
|
||||||
|
The EICAR test file must come back FOUND and a clean payload OK — the same protocol the domain's
|
||||||
|
scanner adapter will speak (ADR-0036). EICAR is assembled from two halves so this file itself is
|
||||||
|
not flagged by an on-access scanner on a developer laptop. Stdlib only (python:3-slim).
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
HOST = os.environ["CLAMAV"]
|
||||||
|
TIMEOUT = int(os.environ.get("CLAMAV_TIMEOUT", "60"))
|
||||||
|
EICAR = (r"X5O!P%@AP[4\PZX54(P^)7CC)7}$" + r"EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*").encode()
|
||||||
|
|
||||||
|
|
||||||
|
def instream(payload):
|
||||||
|
with socket.create_connection((HOST, 3310), timeout=30) as s:
|
||||||
|
s.sendall(b"zINSTREAM\0" + struct.pack(">I", len(payload)) + payload + struct.pack(">I", 0))
|
||||||
|
return s.recv(4096).rstrip(b"\0").decode()
|
||||||
|
|
||||||
|
|
||||||
|
deadline = time.time() + TIMEOUT
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
clean, infected = instream(b"%PDF-1.4 clean"), instream(EICAR)
|
||||||
|
break
|
||||||
|
except OSError as e:
|
||||||
|
if time.time() > deadline:
|
||||||
|
sys.exit(f"FAIL: clamd at {HOST}:3310 unreachable: {e}")
|
||||||
|
time.sleep(3)
|
||||||
|
|
||||||
|
print(f"clean → {clean!r}; eicar → {infected!r}")
|
||||||
|
if clean != "stream: OK":
|
||||||
|
sys.exit("FAIL: clean payload was not reported OK")
|
||||||
|
if not infected.endswith("FOUND"):
|
||||||
|
sys.exit("FAIL: EICAR was not detected")
|
||||||
|
print("OK: clamd detects EICAR and passes a clean stream")
|
||||||
@@ -751,6 +751,26 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
|
# ClamAV daemon (S-28, ADR-0036): the domain scans uploaded diplomas over clamd's INSTREAM
|
||||||
|
# protocol on :3310 before they reach OpenZaak (S-29). The first start downloads ~300 MB of
|
||||||
|
# signatures with freshclam; the volume keeps them across restarts. clamd holds them in memory
|
||||||
|
# (~1 GB), and a reload would briefly hold two copies — ConcurrentDatabaseReload off prevents
|
||||||
|
# that, at the cost of clamd pausing scans during a signature reload.
|
||||||
|
clamav:
|
||||||
|
image: docker.io/clamav/clamav:1.4.6
|
||||||
|
environment:
|
||||||
|
CLAMD_CONF_ConcurrentDatabaseReload: "no"
|
||||||
|
# The image's own healthcheck (clamdcheck.sh: PING → PONG) polls every 30s; poll faster so
|
||||||
|
# wait-healthy sees it as soon as the signatures are loaded.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "clamdcheck.sh"]
|
||||||
|
interval: 5s
|
||||||
|
start_period: 360s
|
||||||
|
mem_limit: 2g
|
||||||
|
volumes:
|
||||||
|
- clamav-db:/var/lib/clamav
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
oz-db:
|
oz-db:
|
||||||
nrc-db:
|
nrc-db:
|
||||||
@@ -758,6 +778,7 @@ volumes:
|
|||||||
projection-db:
|
projection-db:
|
||||||
objecttypen-db:
|
objecttypen-db:
|
||||||
objecten-db:
|
objecten-db:
|
||||||
|
clamav-db:
|
||||||
# Carries the seed-generated acl.env (server-assigned zaaktype URLs) from local-seed to the ACL.
|
# Carries the seed-generated acl.env (server-assigned zaaktype URLs) from local-seed to the ACL.
|
||||||
seed-env:
|
seed-env:
|
||||||
|
|
||||||
|
|||||||
@@ -785,6 +785,26 @@ services:
|
|||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
|
# ClamAV daemon (S-28, ADR-0036): the domain scans uploaded diplomas over clamd's INSTREAM
|
||||||
|
# protocol on :3310 before they reach OpenZaak (S-29). The first start downloads ~300 MB of
|
||||||
|
# signatures with freshclam; the volume keeps them across restarts. clamd holds them in memory
|
||||||
|
# (~1 GB), and a reload would briefly hold two copies — ConcurrentDatabaseReload off prevents
|
||||||
|
# that, at the cost of clamd pausing scans during a signature reload.
|
||||||
|
clamav:
|
||||||
|
image: docker.io/clamav/clamav:1.4.6
|
||||||
|
environment:
|
||||||
|
CLAMD_CONF_ConcurrentDatabaseReload: "no"
|
||||||
|
# The image's own healthcheck (clamdcheck.sh: PING → PONG) polls every 30s; poll faster so
|
||||||
|
# wait-healthy sees it as soon as the signatures are loaded.
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "clamdcheck.sh"]
|
||||||
|
interval: 5s
|
||||||
|
start_period: 360s
|
||||||
|
mem_limit: 2g
|
||||||
|
volumes:
|
||||||
|
- clamav-db:/var/lib/clamav
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
# ── Observability backplane (S-16a, ADR-0023) ──────────────────────────────
|
# ── Observability backplane (S-16a, ADR-0023) ──────────────────────────────
|
||||||
# Grafana-native stack: Tempo ingests OTLP traces (the .NET services export
|
# Grafana-native stack: Tempo ingests OTLP traces (the .NET services export
|
||||||
# straight to it — no collector hop, S-16b), Prometheus scrapes service
|
# straight to it — no collector hop, S-16b), Prometheus scrapes service
|
||||||
@@ -836,6 +856,7 @@ volumes:
|
|||||||
projection-db:
|
projection-db:
|
||||||
objecttypen-db:
|
objecttypen-db:
|
||||||
objecten-db:
|
objecten-db:
|
||||||
|
clamav-db:
|
||||||
# Config volumes — created and populated out-of-band by infra/seed-config.sh
|
# Config volumes — created and populated out-of-band by infra/seed-config.sh
|
||||||
# (docker cp), because bind mounts don't reach sibling containers on the CI
|
# (docker cp), because bind mounts don't reach sibling containers on the CI
|
||||||
# runner. `external` keeps the names deterministic; the seed step manages them.
|
# runner. `external` keeps the names deterministic; the seed step manages them.
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ host: 192.168.122.100
|
|||||||
# portals' authority (runbook, "Publishing through the labs Caddy").
|
# portals' authority (runbook, "Publishing through the labs Caddy").
|
||||||
keycloakUrl: ""
|
keycloakUrl: ""
|
||||||
|
|
||||||
|
# Where the .NET services send traces (OTLP gRPC). The default is the chart's own
|
||||||
|
# `tempo` workload (off by default, like compose). Point it at a Tempo outside the
|
||||||
|
# release, e.g. the cluster monitoring stack's http://tempo.monitoring.svc:4317 —
|
||||||
|
# with no Tempo at all, every export fails and is counted as a .NET exception.
|
||||||
|
otelEndpoint: http://tempo:4317
|
||||||
|
|
||||||
demo:
|
demo:
|
||||||
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
|
# Fill in and submit the medewerker OTP step from the fixture secret, so a public
|
||||||
# demo shows MFA enforced without an authenticator: makes the big-demo theme
|
# demo shows MFA enforced without an authenticator: makes the big-demo theme
|
||||||
@@ -160,10 +166,11 @@ envGroups:
|
|||||||
NOTIFICATIONS_DISABLED: "false"
|
NOTIFICATIONS_DISABLED: "false"
|
||||||
RUN_SETUP_CONFIG: "true"
|
RUN_SETUP_CONFIG: "true"
|
||||||
|
|
||||||
# Traces for the .NET services. Always set, like compose: the exporter fails
|
# Traces for the .NET services. Always set, like compose. With no Tempo behind
|
||||||
# harmlessly when Tempo is absent (services/*/Program.cs).
|
# `otelEndpoint` the exporter fails quietly but throws on every batch, which
|
||||||
|
# shows up as HttpRequestException/SocketException in dotnet_exceptions_total.
|
||||||
otel:
|
otel:
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tempo:4317
|
OTEL_EXPORTER_OTLP_ENDPOINT: '{{ .Values.otelEndpoint }}'
|
||||||
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
OTEL_EXPORTER_OTLP_PROTOCOL: grpc
|
||||||
|
|
||||||
# ── Workloads ──────────────────────────────────────────────────────────────────
|
# ── Workloads ──────────────────────────────────────────────────────────────────
|
||||||
@@ -286,6 +293,11 @@ workloads:
|
|||||||
# Only rendered with demo.otpAutofill (big.env skips empty values); off, Keycloak
|
# Only rendered with demo.otpAutofill (big.env skips empty values); off, Keycloak
|
||||||
# keeps its stock theme and the mounted big-demo theme is unused.
|
# keeps its stock theme and the mounted big-demo theme is unused.
|
||||||
KC_SPI_THEME_DEFAULT: '{{ if .Values.demo.otpAutofill }}big-demo{{ end }}'
|
KC_SPI_THEME_DEFAULT: '{{ if .Values.demo.otpAutofill }}big-demo{{ end }}'
|
||||||
|
# Behind a TLS proxy (keycloakUrl) the dynamic backchannel URLs — token,
|
||||||
|
# userinfo, certs — take their scheme from the request, which reaches Keycloak
|
||||||
|
# as plain http; trusting X-Forwarded-Proto keeps them https so the browser
|
||||||
|
# doesn't block them as mixed content. In-cluster calls send no such header.
|
||||||
|
KC_PROXY_HEADERS: xforwarded
|
||||||
ports: [{ name: http, port: 8080 }]
|
ports: [{ name: http, port: 8080 }]
|
||||||
# TCP, not /health/ready on the management port: nothing here gates on realm
|
# TCP, not /health/ready on the management port: nothing here gates on realm
|
||||||
# import, and a wrong health path would leave the Service with no endpoints.
|
# import, and a wrong health path would leave the Service with no endpoints.
|
||||||
@@ -576,6 +588,24 @@ workloads:
|
|||||||
envFrom: [objecten]
|
envFrom: [objecten]
|
||||||
waitFor: [objecten-db:5432, objecten-redis:6379]
|
waitFor: [objecten-db:5432, objecten-redis:6379]
|
||||||
|
|
||||||
|
# ── ClamAV (S-28, ADR-0036) ─────────────────────────────────────────────────
|
||||||
|
# The domain scans uploaded diplomas over clamd's INSTREAM protocol (S-29).
|
||||||
|
# First start pulls ~300 MB of signatures, so the node needs outbound internet
|
||||||
|
# (like seed-zaaktype); the data volume keeps them when persistence is on.
|
||||||
|
clamav:
|
||||||
|
image: docker.io/clamav/clamav:1.4.6
|
||||||
|
env:
|
||||||
|
CLAMD_CONF_ConcurrentDatabaseReload: "no"
|
||||||
|
ports: [{ name: clamd, port: 3310 }]
|
||||||
|
data: { mountPath: /var/lib/clamav, size: 1Gi }
|
||||||
|
probe:
|
||||||
|
exec: { command: [clamdcheck.sh] }
|
||||||
|
periodSeconds: 5
|
||||||
|
failureThreshold: 72
|
||||||
|
resources:
|
||||||
|
requests: { memory: 1200Mi }
|
||||||
|
limits: { memory: 2Gi }
|
||||||
|
|
||||||
# ── Bootstrap the flow, like the local compose stack does (S-B04, ADR-0020) ──
|
# ── Bootstrap the flow, like the local compose stack does (S-B04, ADR-0020) ──
|
||||||
# Seeds + publishes the BIG zaaktype through the same FQDN the ACL uses, so the
|
# Seeds + publishes the BIG zaaktype through the same FQDN the ACL uses, so the
|
||||||
# server-assigned URLs are host-consistent. The ACL then resolves them by
|
# server-assigned URLs are host-consistent. The ACL then resolves them by
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
#
|
||||||
|
# S-28 (#191): assert clamd scans over INSTREAM (EICAR → FOUND, clean → OK), against an
|
||||||
|
# ALREADY-RUNNING stack. Runs the check in a python:3-slim container on the stack network (the
|
||||||
|
# runner can't reach published ports — gitea-actions-gotchas.md §5/§6).
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
|
||||||
|
av="$(docker ps -q --filter 'name=[-_]clamav[-_][0-9]+$' | head -1)"
|
||||||
|
[ -n "$av" ] || { echo "ERROR: no running clamav container — bring the stack up first" >&2; exit 1; }
|
||||||
|
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$av" | head -1)"
|
||||||
|
ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$av")"
|
||||||
|
echo ">> network=$net clamav=$ip"
|
||||||
|
|
||||||
|
cid="$(docker create --network "$net" -e "CLAMAV=$ip" -e "CLAMAV_TIMEOUT=${CLAMAV_TIMEOUT:-60}" \
|
||||||
|
python:3-slim python /clamav-check.py)"
|
||||||
|
docker cp "$here/clamav-check.py" "$cid:/clamav-check.py" >/dev/null
|
||||||
|
rc=0; docker start -a "$cid" || rc=$?
|
||||||
|
docker rm -f "$cid" >/dev/null
|
||||||
|
exit $rc
|
||||||
@@ -56,6 +56,8 @@ nav:
|
|||||||
- "ADR-0032: Werkbak live refresh": architecture/adr-0032-werkbak-live-refresh.md
|
- "ADR-0032: Werkbak live refresh": architecture/adr-0032-werkbak-live-refresh.md
|
||||||
- "ADR-0033: Kubernetes via one Helm chart": architecture/adr-0033-kubernetes-via-one-helm-chart.md
|
- "ADR-0033: Kubernetes via one Helm chart": architecture/adr-0033-kubernetes-via-one-helm-chart.md
|
||||||
- "ADR-0034: Caddy serves the portals": architecture/adr-0034-caddy-serves-the-portals.md
|
- "ADR-0034: Caddy serves the portals": architecture/adr-0034-caddy-serves-the-portals.md
|
||||||
|
- "ADR-0035: Public access through the labs Caddy": architecture/adr-0035-public-access-through-the-labs-caddy.md
|
||||||
|
- "ADR-0036: Scan uploads with ClamAV": architecture/adr-0036-scan-uploads-with-clamav.md
|
||||||
- FDS-architectuur:
|
- FDS-architectuur:
|
||||||
- Overzicht: architecture/fds/README.md
|
- Overzicht: architecture/fds/README.md
|
||||||
- Componentview (L3): architecture/fds/c4-component-view.md
|
- Componentview (L3): architecture/fds/c4-component-view.md
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
|
<!-- Stryker-only. Acl.IntegrationTests is left out on purpose: it needs a live
|
||||||
|
OpenZaak, so in the mutation job it fails its initial run (#174). The root
|
||||||
|
register-referentie.slnx still builds and lints it. -->
|
||||||
<Project Path="Acl.Api/Acl.Api.csproj" />
|
<Project Path="Acl.Api/Acl.Api.csproj" />
|
||||||
<Project Path="Acl.Application/Acl.Application.csproj" />
|
<Project Path="Acl.Application/Acl.Application.csproj" />
|
||||||
<Project Path="Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
<Project Path="Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
||||||
<Project Path="Acl.IntegrationTests/Acl.IntegrationTests.csproj" />
|
|
||||||
<Project Path="Acl.Tests/Acl.Tests.csproj" />
|
<Project Path="Acl.Tests/Acl.Tests.csproj" />
|
||||||
</Solution>
|
</Solution>
|
||||||
|
|||||||
Reference in New Issue
Block a user