Deployment, supply chain and security review (S22)
Session S22, 2026-11-11. Inputs: the S22 block, D-20, D-32, D-51, D-54, the Python manifest, Dockerfile, docs/security.md and
SECURITY.md on python-archive, and src/microdcs/__init__.py there (configuration). Maintainer choices at the start: GHCR on
main and v* tags, cosign keyless sign and attest, a kind job in CI after a local run, everything in one session, and a later
session that reviews the CI runtime.
Python parity
| Id | Python behaviour | Where | Disposition |
|---|---|---|---|
| K-1 | Two Deployments, processor (2 replicas) and publisher (1), same image | deploy/k8s.yaml |
kept, by Roles (D-51) |
| K-2 | APP_IS_PROCESSOR_INSTANCE, APP_IS_PUBLISHER_INSTANCE flags |
deploy/k8s.yaml, __init__.py |
changed: MicroDcs:Roles (D-39, D-51) |
| K-3 | No probes; the process exits when Redis or MQTT is lost | deploy/k8s.yaml header |
changed: liveness and readiness on the management listener (D-54); the exit stays (D-27) |
| K-4 | Pod hardening: non-root, read-only root, no privilege escalation, drop all, RuntimeDefault seccomp, no API token, emptyDir on /tmp |
deploy/k8s.yaml |
kept; user 1654 of the chiseled image instead of 65532 of distroless; own ServiceAccount; size limit on /tmp; ephemeral-storage requests and limits (Sonar S6870, S6897) |
| K-5 | NetworkPolicy denying all ingress | deploy/k8s.yaml |
kept and extended: default deny in and out, egress to DNS, Valkey, broker, collector, management ingress from labelled clients |
| K-6 | POD_ID from metadata.uid |
deploy/k8s.yaml |
changed: metadata.name, readable in logs and inspection and unique per running pod |
| K-7 | Shared subscription name APP_PROCESSING_SHARED_SUBSCRIPTION_NAME |
__init__.py |
kept: binding SharedGroup (D-46) |
| K-8 | Retained topic TTL 48 h (APP_PUBLISHER_RETAINED_TTL_SECONDS=172800) |
deploy/k8s.yaml, __init__.py |
kept: MicroDcs:JobPublisher:RetainedTtl default 48 h, not set in the manifest |
| K-9 | Redis username, password_file (one password source only), ssl, ssl_ca_certs |
__init__.py RedisConfig |
kept: Valkey User, PasswordFile, TlsCaFile, RequireTls (S22); the file is read before every connect (D-49 revised) |
| K-10 | Redis socket timeouts, keepalive, health_check_interval |
__init__.py RedisConfig |
dropped: StackExchange.Redis heartbeat and timeouts (D-7), readiness check valkey (D-54) |
| K-11 | REQUIRE_TLS for MQTT, SAT token, password file, client certificate |
docs/security.md |
kept (S13); documented anew |
| K-12 | MessagePack-RPC listener with TLS and client certificates | docs/security.md |
dropped: MessagePack-RPC is removed (driver 4) |
| K-13 | APP_LOGGING_LOG_PAYLOADS and loggable() for payloads in DEBUG logs |
__init__.py, docs/security.md |
changed: no switch; payloads never reach logs, only the inspection service shows them with IncludePayloads (D-40) |
| K-14 | ENFORCE_SUBJECT_TOPIC_MATCH, nothing checked at level 0 |
__init__.py, docs/security.md |
changed: ScopeCheck and Level0Subjects=Reject by default (D-38, R-6) |
| K-15 | SECURITY.md with reporting, support and scope |
SECURITY.md |
kept; scope now names the signed GHCR image |
| K-16 | Distroless Python image with opentelemetry-instrument |
Dockerfile |
changed: AOT binary in chiseled runtime-deps, OTLP from the SDK (D-32, D-54) |
| K-17 | File logging off because of the read-only root (APP_LOGGING_FILENAME) |
docs/security.md |
dropped: console and OTLP only |
| K-18 | Scaffolder template scripts/init/k8s.yaml |
src/microdcs/scripts/init/ |
dropped here: dotnet new templates in S26 |
| K-19 | Image tag :latest, "pin a digest in production" |
deploy/k8s.yaml |
changed: kustomization.yaml pins the image; published images are signed (D-55) |
No row is open.
"Done in the session" behaviours, checked against the code
The list in review triage:
| Behaviour | .NET | Evidence |
|---|---|---|
TLS plus require_tls for every transport |
MQTT, NATS, Valkey (RequireTls) |
options validator tests of the three projects |
| MQTT user and password, client certificates | MqttOptions |
MqttOptionsValidatorTests, RV17 compose test |
| Password files | MQTT, NATS, Valkey, re-read per connect (D-49) | RV17 tests of MQTT, NATS and Valkey |
| Subject and topic scope check | ScopePolicy, Level0Subjects=Reject (D-38) |
Core scope tests, transport tests |
| Payloads kept out of logs | no log template takes a payload; inspection redacts (D-40) | template search in S22, inspection redaction tests |
| Valkey timeouts | StackExchange.Redis defaults, connectTimeout=/syncTimeout=/asyncTimeout= in the connection string |
CredentialTests sets connectTimeout |
| Dedupe lease: a crash never loses a message, duplicates are acknowledged | claim with lease, release on fault (D-37) | DedupeAndLeaseTests, PipelineTests |
Stable mdcsactionkey on push commands |
Commands, codec extension attribute |
SfcHostTests, SfcReplicaTests |
The acceptance tests that Q-TEST-1/2 ask for are S23's.
Findings
- Valkey credentials were missing (K-9). The S09 options had only a connection string, so a manifest had to put the password
into an environment variable. StackExchange.Redis 3.4 has no credential callback, but
ConfigurationOptions.BeforeSocketConnectruns before every socket connect and the multiplexer keeps the instance, so settingPasswordthere reaches every reconnect. The rotation test needs a Valkey that requires authentication: on a server whosedefaultuser has no password, a rejected AUTH leaves the connection working asdefault, and the multiplexer never reconnects (seen on the first attempt). Hence thevalkey-aclharness service. - Restricted ACL user. StackExchange.Redis touches a tiebreaker key and a configuration channel outside any prefix and sends
INFO,CONFIG GET,CLUSTER NODES/SLOTSon connect; with a prefix-limited user these are denied (it copes, but the ACL log fills). With one endpoint (D-12) none is needed, soValkeyConnectionturns them off. The SFC host needsTIME(D-44), which is in no command category of the rule; the restricted test mode found it (three SFC tests fail without+time). - kind and NetworkPolicy. kind 0.33 (Kubernetes 1.37) enforces NetworkPolicy with kindnet, and kubelet probes pass a deny-all ingress policy. Valkey rejects ACL rules with spaces on the command line; the test passes them in a mounted config file.
- Fail-fast at startup produces restarts when the application starts before Valkey (D-27). The kind test therefore checks that nothing restarts after the rollout; the deployment page explains the back-off.
- Trivy KSV-0109 ("ConfigMap with secrets") matched the keys
PasswordFileandBearerTokenFilealthough the values are paths; the paths moved into the Deployments, next to the secret volumes, instead of a waiver. - zizmor cache-poisoning: a
v*tag trigger turnsci.ymlinto a release workflow for zizmor, which then flags thesetup-dotnetcache. Tag builds now use no caches at all (dotnet and image);mainbuilds keep them. - Log noise: the management listener logged every probe at Information, and every start warned that
ASPNETCORE_HTTP_PORTS(set by the base image) is overridden. the listener's web host now holds theMicrosoft.AspNetCorecategories at Warning unlessLogging:LogLevel:Microsoft.AspNetCoresays otherwise (ManagementLoggerFactory), and it clears the inherited port settings. - GHCR: the image carries
org.opencontainers.image.source, which links the package to the repository. Whether a new package of a personal account is public is checked after the first publish (post-merge).
Supply chain design (D-55)
publish (matrix, native runners as in D-32) runs only on pushes after dotnet, container, integration and kubernetes
passed. It builds the image (from the container job's cache on main), exports it, creates two CycloneDX SBOMs with the Trivy gate
(scripts/scan sbom: the app's lock file, because the AOT binary hides its packages from an image scan, and the image's OS packages),
pushes by digest without provenance or buildx SBOM manifests, signs the digest and attests both SBOMs. publish-manifest creates the
index with the tags, signs it and verifies every signature and attestation against the workflow identity before it reports the digest.
cosign 3.1.4 through sigstore/cosign-installer; keyless certificates from the job's OIDC token.
Not chosen: GitHub artifact attestations (maintainer chose cosign, which more admission controllers check); provenance attestations
from buildx (they turn each platform push into an index with an unknown/unknown manifest, and the maintainer asked for signatures
and SBOMs); pushing from the container job (it runs on pull requests too, and should not hold packages: write).