Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c9eeccacc | ||
|
|
f3e10c6642 | ||
|
|
86381d7059 |
@@ -248,9 +248,6 @@ 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
|
||||||
@@ -290,7 +287,6 @@ 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 }}
|
||||||
@@ -313,7 +309,6 @@ 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") |"
|
||||||
@@ -333,7 +328,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 clamav 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 tempo prometheus grafana 2>&1 || true
|
||||||
- name: Tear down
|
- name: Tear down
|
||||||
if: always()
|
if: always()
|
||||||
run: make down
|
run: make down
|
||||||
|
|||||||
@@ -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 clamav
|
WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api self-service openbaar behandel beheer objecttypen objecten
|
||||||
# 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,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.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
|
.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-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).
|
||||||
@@ -222,11 +222,6 @@ 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.
|
||||||
@@ -235,7 +230,6 @@ 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 \
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
# 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.
|
|
||||||
@@ -15,7 +15,6 @@ those containers share a filesystem — or a `localhost` — breaks.
|
|||||||
| `pg_isready` passes before PostGIS is ready | add a `PostGIS_Version()` probe | the db healthchecks |
|
| `pg_isready` passes before PostGIS is ready | add a `PostGIS_Version()` probe | the db healthchecks |
|
||||||
| `upload-artifact@v4` fails ("not supported on GHES") | pin `@v3` | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
| `upload-artifact@v4` fails ("not supported on GHES") | pin `@v3` | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
||||||
| `upload-artifact@v3` fails with "Artifact service responded with 500" | mark the upload `continue-on-error: true` (server-side; issue #62) | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
| `upload-artifact@v3` fails with "Artifact service responded with 500" | mark the upload `continue-on-error: true` (server-side; issue #62) | `.gitea/workflows/ci.yaml` (`mutation` job) |
|
||||||
| PR says "out-of-date" but *Update branch* fails ("Unable to update pull request") | push a new head SHA (`commit --amend --no-edit` + `push --force-with-lease`) | §10, server-side |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -290,41 +289,3 @@ re-run hides the failed one, so keep the failing job id from the original report
|
|||||||
- Remember `concurrency.cancel-in-progress: true` in `ci.yaml`: a new push to the same
|
- Remember `concurrency.cancel-in-progress: true` in `ci.yaml`: a new push to the same
|
||||||
ref, or a re-run, kills the in-flight run the same way. Check `run_attempt` before
|
ref, or a re-run, kills the in-flight run the same way. Check `run_attempt` before
|
||||||
concluding a job hung.
|
concluding a job hung.
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. A retargeted stacked PR says "out-of-date" but *Update branch* fails
|
|
||||||
|
|
||||||
**Symptom** — the PR shows *This branch is out-of-date with the base branch* and
|
|
||||||
*This pull request is blocked because it's outdated* (branch protection
|
|
||||||
`block_on_outdated_branch` on `main`), yet **Update branch** answers *Unable to update
|
|
||||||
pull request* (API: `HeadBranch of PR NN is up to date`). `git merge-base` confirms the
|
|
||||||
branch sits on the tip of `main`. Seen on #194 (#195).
|
|
||||||
|
|
||||||
**Why** — Gitea 1.27 keeps two answers to "is it behind?":
|
|
||||||
|
|
||||||
- The banner and the merge block read a **stored** `pull_request.commits_behind`
|
|
||||||
(`MergeBlockedByOutdatedBranch`: `CommitsBehind > 0`).
|
|
||||||
- *Update branch* recomputes it **live** from git (`services/pull/update.go`) and refuses
|
|
||||||
when nothing is behind.
|
|
||||||
|
|
||||||
The stored count is only refreshed by a push to the head or base branch, or by a
|
|
||||||
retarget. #194 was stacked on #193; its rebased branch was force-pushed in the **same
|
|
||||||
second** that merging #193 deleted the parent branch and Gitea retargeted #194 to `main`.
|
|
||||||
The retarget compared `main` against `refs/pull/194/head` before the push queue had
|
|
||||||
updated that ref (the old head missed the squash commit → `CommitsBehind = 1`); the push
|
|
||||||
handler's own resync ran against the deleted old base and failed. Nothing pushed
|
|
||||||
afterwards, so the stale `1` stuck. Close/reopen does not recompute it.
|
|
||||||
|
|
||||||
**Fix** — give the head a new SHA with the same content; the push resyncs the count:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git commit --amend --no-edit # new committer date → new SHA
|
|
||||||
git push --force-with-lease origin <branch>
|
|
||||||
```
|
|
||||||
|
|
||||||
PR CI re-runs on the new SHA (the old statuses don't carry over).
|
|
||||||
|
|
||||||
**Avoid it** — when the parent of a stacked PR merges, let Gitea finish retargeting the
|
|
||||||
child to `main` (its timeline shows *changed target branch*) **before** pushing the
|
|
||||||
rebased child branch.
|
|
||||||
|
|||||||
@@ -356,7 +356,6 @@ 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>` |
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
#!/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,26 +751,6 @@ 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:
|
||||||
@@ -778,7 +758,6 @@ 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,26 +785,6 @@ 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
|
||||||
@@ -856,7 +836,6 @@ 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.
|
||||||
|
|||||||
@@ -588,24 +588,6 @@ 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
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -57,7 +57,6 @@ nav:
|
|||||||
- "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-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
|
||||||
|
|||||||
Reference in New Issue
Block a user