# Infrastructure & Configuration Review **Date**: 2026-05-13 **Scope**: `Dockerfile`, `docker.test/Dockerfile`, `e2e/Dockerfile`, `docker-compose.test.yml`, `appsettings*.json`, `env/db/*.sql`, `.gitignore`, `.dockerignore`. No CI/CD pipeline files (`.github/workflows/`, `.gitlab-ci.yml`, `azure-pipelines.yml`) are present in this workspace. ## Container Security ### Production image (`Dockerfile`) | Check | Result | Notes | |-------|--------|-------| | Non-root user | **FAIL** | No `USER` directive — runs as root. See F-6. | | Minimal base image | PASS | `mcr.microsoft.com/dotnet/aspnet:10.0` (runtime-only) for the final stage; SDK image used only for build. | | Multi-stage build | PASS | Build / publish / runtime stages cleanly separated. | | No secrets in build args | PASS | Only `CI_COMMIT_SHA` (passed as `AZAION_REVISION` env at runtime) — non-sensitive. | | Health check defined | PASS (compose layer) | `docker-compose.test.yml:42-51` defines a TCP health check; the production deployment must define an equivalent. **Not verified in this audit** because no production compose file exists in this workspace. | | Image pinned by digest | **WARN** | Base images use `:10.0` tag, not `@sha256:...` digest. Tag floats — a poisoned upstream tag would be picked up on the next rebuild. Acceptable if the build runs from a controlled cache; otherwise pin. | | `EXPOSE` matches Kestrel binding | PASS | `EXPOSE 8080`, `ASPNETCORE_URLS=http://+:8080`. | ### Test sidecar image (`docker.test/Dockerfile`) ``` FROM alpine:latest CMD echo hello ``` This image is essentially a no-op stub. It's referenced from somewhere in the test/CI tooling but contributes nothing functional. **Recommendation**: either remove it (if nothing references it) or document its purpose. From a security standpoint it's inert — `alpine:latest` is fine for an `echo` and the floating tag is irrelevant for a stub. Flag as **operational hygiene, not a security finding**. ### E2E runner image (`e2e/Dockerfile`) | Check | Result | Notes | |-------|--------|-------| | Non-root | **FAIL** (test-only) | Same root-by-default behavior. Test runner only — no exposed network, runs in an ephemeral container per CI run. **Acceptable.** | | Uses SDK image as final | **WARN** (test-only) | Final stage is `mcr.microsoft.com/dotnet/sdk:10.0` — needed for `dotnet test`. SDK images carry more attack surface than runtime images, but this container is only reachable on the internal `e2e-net` bridge. **Acceptable for test-only.** | ## docker-compose.test.yml | Check | Result | Notes | |-------|--------|-------| | Secrets via env vars (not committed prod secrets) | PASS (test-only) | The DB password, JWT secret, and master key are committed because this is the e2e harness only. F-10 captures the rule: these literals must never appear in a production compose file. | | No port leaks | PASS | Only `8080:8080` is published from `system-under-test`; `test-db` is internal to the bridge. | | Healthcheck for the API | PASS | TCP probe on `127.0.0.1:8080`. | | Network isolation | PASS | All three services share `e2e-net` only; no `host` network mode. | | Image lock | **WARN** (test-only) | `postgres:16-alpine` floats. For test reproducibility consider pinning a digest, but not security-critical for an ephemeral test stack. | There is **no production `docker-compose.yml`** in this workspace. Any production deployment must: - Inject `JwtConfig__Secret` and both `ConnectionStrings__*` values from a secret manager (Vault, AWS Secrets Manager, Azure Key Vault). (`ResourcesConfig__EncryptionMasterKey` was the third item here pre-revert; that field has since been deleted along with the OTA feature.) - NOT carry over the `ASPNETCORE_ENVIRONMENT=Development` value used in the test compose — that environment value enables Swagger at the root path. ## Environment Configuration ### `Azaion.AdminApi/appsettings.json` | Field | Value committed | Risk | |-------|----------------|------| | `Logging.LogLevel.*` | `Information` / `Warning` | OK | | `AllowedHosts` | `"*"` | OK at the framework level — host filtering is normally enforced upstream by the reverse proxy. | | `ResourcesConfig.ResourcesFolder` | `"Content"` | Relative path; resolves under the working directory inside the container. OK. | | ~~`ResourcesConfig.EncryptionMasterKey`~~ | — | **Removed in the post-cycle-1 revert** along with the OTA feature; field no longer exists in `ResourcesConfig`, `appsettings.json`, or `docker-compose.test.yml`. Closes F-5 automatically. | | `JwtConfig.{Issuer, Audience, TokenLifetimeHours}` | committed | Public-by-design (not secrets). | | `JwtConfig.Secret` | **NOT committed** | Correct. Supplied via env var. | | `ConnectionStrings.*` | **NOT committed** | Correct. Supplied via env var. | ### `Azaion.AdminApi/appsettings.Development.json` Empty except for log levels — fine. ### `e2e/Azaion.E2E/appsettings.test.json` Test-only. F-10 captures this; not a production concern. ## Secrets Hygiene | Pattern | Where | Disposition | |---------|-------|-------------| | Plaintext DB passwords | `env/db/01_permissions.sql` | F-11 — operator template, not runtime. Add a header comment. | | Plaintext DB / JWT / master-key in `docker-compose.test.yml` | `docker-compose.test.yml:31-37` | F-10 — test-only. CI guard recommended. | | Plaintext admin/uploader passwords in `e2e/Azaion.E2E/appsettings.test.json` | as above | F-10 — test-only. | | `.env` ignored | `.gitignore:10` | PASS | | `bin/`, `obj/`, `logs/`, `Content/` ignored | `.gitignore:2-3, 7, 9` | PASS — keeps build artifacts and runtime data out of git. | ## CI/CD No CI configuration files were found in the workspace (`.github/workflows/`, `.gitlab-ci.yml`, `azure-pipelines.yml`, `Jenkinsfile` — none present). Either: - CI is configured outside this repo (e.g., upstream meta-repo), in which case the security guardrails (`dotnet list package --vulnerable`, secret-scanning, dependency-review) need to be verified there. - Or there is no automated CI today, in which case this is a meta-finding: the dependency-bump (D-1) and the test suite have no automated gate. **Recommend**: introduce at least a `dotnet build && dotnet test && dotnet list package --vulnerable` job before deploy. ## Network Security | Check | Status | Notes | |-------|--------|-------| | HTTPS enforcement in code | **FAIL** | F-13 — assumed at reverse proxy. | | HSTS | not configured | Acceptable if reverse proxy injects it. | | CORS | tight | `AdminCorsPolicy` whitelist-only (`https://admin.azaion.com`, `http://admin.azaion.com`). The plaintext `http://` origin can be removed once the SaaS UI is HTTPS-only. | | Security headers | not configured in app | `X-Frame-Options`, `X-Content-Type-Options`, `Content-Security-Policy` not set in code. Reverse proxy responsibility today; document the assumption. | ## Self-verification - [x] All Dockerfiles reviewed (`Dockerfile`, `docker.test/Dockerfile`, `e2e/Dockerfile`) - [x] All compose files reviewed (`docker-compose.test.yml` — only one in repo) - [x] All environment / config files reviewed (3 `appsettings*.json`) - [x] CI/CD reviewed (none present in repo — surfaced as meta-finding)