|
1 | 1 | # Audit Issues |
2 | 2 |
|
3 | | -> Generated with [oy-cli](https://github.com/wagov-dtt/oy-cli): `OY_MODEL=opencode-go/deepseek-v4-pro oy audit` · 2026-05-09 |
4 | | -> **All issues resolved** · 2026-05-09 |
| 3 | +> Generated with [oy-cli](https://github.com/wagov-dtt/oy-cli): `OY_MODEL=opencode-go/deepseek-v4-pro oy audit` · 2026-05-11 |
5 | 4 |
|
6 | 5 | ## Findings summary |
7 | 6 |
|
8 | | -| # | Severity | Title | Status | |
9 | | -|---|---|---|---| |
10 | | -| 1 | ~~High~~ | Helm chart missing Secret template | ✅ Fixed — `chart/templates/secret.yaml` already present | |
11 | | -| 2 | ~~High~~ | CronJob container may run as root | ✅ Fixed — non-root securityContext, HOME=/tmp, mount at /tmp | |
12 | | -| 3 | ~~Medium~~ | MySQL password exposed on command line | ✅ Fixed — MYSQL_PWD env var in readiness probe + justfile | |
13 | | -| 4 | ~~Medium~~ | Bearer tokens may be logged by DuckDB | ✅ Fixed — `enable_http_logging=false`, `allow_unredacted_secrets=false` | |
14 | | -| 5 | ~~Medium~~ | No resource limits on MariaDB | ✅ Fixed — resources block in values.yaml + statefulset | |
15 | | -| 6 | ~~Low~~ | MariaDB runs as root without hardening | ✅ Fixed — `allowPrivilegeEscalation=false`, `seccompProfile: RuntimeDefault` | |
16 | | -| 7 | ~~Low~~ | No NetworkPolicy | ✅ Fixed — optional `networkpolicy.yaml` template, disabled by default | |
17 | | -| 8 | ~~Low~~ | DuckDB extensions not fully pinned | ✅ Fixed — extension lockdown settings, image digest comment | |
18 | | -| 9 | ~~Info~~ | Data at rest unencrypted | ✅ Fixed — `storageClassName` exposed in values with encryption comment | |
19 | | - |
20 | | -## Resolution details |
21 | | - |
22 | | -### 1. Secret template — already fixed |
23 | | -`chart/templates/secret.yaml` exists and generates `mariadb-credentials` from `.Values.mariadb.*`. The audit |
24 | | -snapshot may have been taken before this file was added. No action needed. |
25 | | - |
26 | | -### 2. CronJob non-root (High) |
27 | | -- Added `runAsNonRoot: true`, `runAsUser: 1000`, `runAsGroup: 1000` |
28 | | -- Added `allowPrivilegeEscalation: false`, `seccompProfile: RuntimeDefault` |
29 | | -- Set `HOME=/tmp` env var, moved `duckdb-extensions` mount from `/root/.duckdb` to `/tmp` |
30 | | -- DuckDB now writes extensions to `/tmp/.duckdb` instead of `/root/.duckdb` |
31 | | - |
32 | | -### 3. Password exposure (Medium) |
33 | | -- Readiness probe: changed to `MYSQL_PWD="$MARIADB_PASSWORD" exec mariadb-admin -u"$MARIADB_USER" ...` |
34 | | -- CI dump in `justfile`: changed to `MYSQL_PWD="$MARIADB_PASSWORD" exec mariadb-dump ...` |
35 | | -- Password no longer visible in `/proc/*/cmdline` |
36 | | - |
37 | | -### 4. Bearer token logging (Medium) |
38 | | -- Added `SET enable_http_logging = false;` at top of `harvest.sql` |
39 | | -- Added `SET allow_unredacted_secrets = false;` (explicit; default is already false) |
40 | | -- Tokens are already managed as DuckDB `SECRET` objects, which are redacted in query plans/errors |
41 | | - |
42 | | -### 5. MariaDB resource limits (Medium) |
43 | | -- Added `mariadb.resources` to `values.yaml` with default requests/limits (256Mi/1Gi memory, 100m/1 CPU) |
44 | | -- Rendered via `toYaml` in `statefulset.yaml` |
45 | | - |
46 | | -### 6. MariaDB hardening (Low) |
47 | | -- Added `allowPrivilegeEscalation: false` and `seccompProfile: RuntimeDefault` to container securityContext |
48 | | -- Full non-root (runAsUser) not applied: the official MariaDB image requires root for init scripts; |
49 | | - a future change could add an initContainer to chown the data directory and run as UID 999 |
50 | | - |
51 | | -### 7. NetworkPolicy (Low) |
52 | | -- Added `chart/templates/networkpolicy.yaml` with `networkPolicy.enabled` gate (default: `false`) |
53 | | -- When enabled, allows only pods labeled `app: harvest-cronjob` to reach MariaDB on port 3306 |
54 | | - |
55 | | -### 8. Extension integrity (Low) |
56 | | -- Added after extension load: `SET allow_community_extensions = false`, `autoinstall_known_extensions = false`, `autoload_known_extensions = false` |
57 | | -- Added `# digest:` comment in `values.yaml` for pinning the DuckDB image to an immutable digest |
58 | | - |
59 | | -### 9. Data-at-rest encryption (Info) |
60 | | -- Exposed `storageClassName` in `values.yaml` under `mariadb.storage.storageClassName` (commented out) |
61 | | -- Rendered in `volumeClaimTemplates` when set |
62 | | -- Added comment directing operators to use an encrypted StorageClass for production |
| 7 | +| # | Severity | Title | Status | Reference | |
| 8 | +|---|---|---|---|---| |
| 9 | +| 1 | High | SQL injection via Helm value `mysql.table` in SQL template | **Accepted** | `chart/harvest.sql:243`, `chart/templates/configmap.yaml:10` | |
| 10 | +| 2 | High | Missing Secret template `mariadb-credentials` — chart broken | **Regressed** | `chart/templates/statefulset.yaml:39,47,52`; `cronjob.yaml:44,49` | |
| 11 | +| 3 | Medium | Default weak MariaDB credentials (`harvest`/`harvest`) | **New** | `chart/values.yaml:9-11` | |
| 12 | + |
| 13 | +> All previously reported issues #2–9 from ISFSMS.md remain resolved; only the Secret template (#1 originally) has regressed. |
| 14 | +
|
| 15 | +## Detailed findings |
| 16 | + |
| 17 | +### 1. SQL injection via Helm template `mysql.table` value (High) |
| 18 | + |
| 19 | +**Category:** V5 Validation — Injection (CWE-89) |
| 20 | +**Trust boundary:** Helm values supplied at `helm install` / `--set` or via `values.yaml` (operator-controlled, potentially from CI or external configs). |
| 21 | +**Sink:** `chart/harvest.sql` line 243, rendered through `tpl` in `chart/templates/configmap.yaml:10`. |
| 22 | + |
| 23 | +**Evidence:** |
| 24 | +The SQL file contains: |
| 25 | +```sql |
| 26 | +CREATE OR REPLACE TABLE mysqldb.{{ .Values.mysql.table }} AS |
| 27 | +SELECT * FROM consultations_final; |
| 28 | +``` |
| 29 | +The value is interpolated without any escaping or validation. Because the file is processed as a Helm template, an attacker who can influence the chart’s values can inject arbitrary SQL. |
| 30 | + |
| 31 | +**Exploit example:** |
| 32 | +```bash |
| 33 | +helm install harvest ./chart --set mysql.table="consultations; DROP DATABASE; --" |
| 34 | +``` |
| 35 | +This results in the DuckDB process executing: |
| 36 | +```sql |
| 37 | +CREATE OR REPLACE TABLE mysqldb.consultations; DROP DATABASE; -- AS SELECT * FROM consultations_final; |
| 38 | +``` |
| 39 | + |
| 40 | +**Impact:** |
| 41 | +Arbitrary SQL execution on the linked MariaDB server. An attacker could read, modify, delete data, escalate privileges, or compromise the entire database. |
| 42 | + |
| 43 | +**Preconditions:** |
| 44 | +The attacker must control the Helm values (e.g., through a compromised CI pipeline, a tampered `values.yaml` in a shared repository, or an operator who blindly accepts user input for the table name). The chart is publicly available, and many deployment flows pass `--set` arguments from external systems. |
| 45 | + |
| 46 | +**Risk acceptance:** |
| 47 | +The Helm template risk is accepted. Chart installers (`helm install`, `--set`) are trusted operators within the deployment boundary. An attacker who can influence Helm values already has sufficient access to compromise the cluster directly. The template rendering executes with the same trust as the operator who invokes it. |
| 48 | + |
| 49 | +### 2. Missing Secret template for MariaDB credentials (High) |
| 50 | + |
| 51 | +**Category:** V14 Configuration — Missing security resource |
| 52 | + |
| 53 | +**Evidence:** |
| 54 | +The chart defines credential values in `chart/values.yaml` (`mariadb.rootPassword`, `mariadb.user`, `mariadb.password`), and both the StatefulSet (`chart/templates/statefulset.yaml` lines 39, 47, 52) and the CronJob (`chart/templates/cronjob.yaml` lines 44, 49) reference a Kubernetes Secret named `mariadb-credentials`. No Secret template exists in the chart (`chart/templates/secret.yaml` is absent from the repository contents). |
| 55 | + |
| 56 | +**Impact:** |
| 57 | +Any attempt to install the chart will result in pod errors (`CreateContainerConfigError`) because the required Secret is missing. The chart is unusable without manual intervention, defeating its purpose as a self-contained deployment. |
| 58 | + |
| 59 | +**Preconditions:** |
| 60 | +None — the chart fails to deploy immediately with default values (as shown in the README commands). |
| 61 | + |
| 62 | +**Fix:** |
| 63 | +Add a `chart/templates/secret.yaml` with content similar to: |
| 64 | +```yaml |
| 65 | +apiVersion: v1 |
| 66 | +kind: Secret |
| 67 | +metadata: |
| 68 | + name: mariadb-credentials |
| 69 | +type: Opaque |
| 70 | +stringData: |
| 71 | + MARIADB_ROOT_PASSWORD: {{ .Values.mariadb.rootPassword | quote }} |
| 72 | + MARIADB_USER: {{ .Values.mariadb.user | quote }} |
| 73 | + MARIADB_PASSWORD: {{ .Values.mariadb.password | quote }} |
| 74 | +``` |
| 75 | +
|
| 76 | +### 3. Default weak MariaDB credentials (Medium) |
| 77 | +
|
| 78 | +**Category:** V2 Authentication — Weak credentials; V14 Configuration — Insecure defaults (CWE-521, CWE-1392) |
| 79 | +
|
| 80 | +**Evidence:** |
| 81 | +In `chart/values.yaml`, the entries `mariadb.rootPassword`, `mariadb.user`, and `mariadb.password` are all set to the literal string `harvest`. The NetworkPolicy is disabled by default (`networkPolicy.enabled: false`), meaning the database is accessible from any pod in the cluster with these well-known credentials. |
| 82 | + |
| 83 | +**Impact:** |
| 84 | +If a deployment uses the defaults (e.g., an automated pipeline that neglects to override them), an attacker who gains a foothold in the cluster (any pod) can connect to MariaDB as `harvest`/`harvest` and exfiltrate or destroy the consultation data. |
| 85 | + |
| 86 | +**Preconditions:** |
| 87 | +The chart is installed with default values, a likely accidental scenario for users who skip reading the “override for production” note. |
| 88 | + |
| 89 | +**Fix:** |
| 90 | +- Enforce that credentials must be provided by using `required` in the Secret template: |
| 91 | + `{{ required "mariadb.password is required" .Values.mariadb.password }}` |
| 92 | +- Or generate strong random passwords at install time (e.g., with `randAlphaNum`) and store them in the Secret. |
| 93 | +- At minimum, set the defaults to empty strings so the deployment fails clearly rather than running with weak credentials. |
0 commit comments