Skip to content

Commit 2e4871a

Browse files
committed
Release v0.5.5
1 parent feff097 commit 2e4871a

9 files changed

Lines changed: 67 additions & 60 deletions

File tree

ISSUES.md

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,19 @@
66

77
| # | Severity | Title | Status | Reference |
88
|---|---|---|---|---|
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` |
9+
| 1 | High | SQL injection via Helm value `mysql.table` in SQL template | **Accepted** | `chart/harvest.sql`, `chart/templates/configmap.yaml` |
10+
| 2 | High | Missing Secret template `mariadb-credentials` — chart broken | **Resolved** `chart/templates/secret.yaml` present | `chart/templates/secret.yaml` |
11+
| 3 | Medium | Default weak MariaDB credentials (`harvest`/`harvest`) | **Documented** — override for production | `README.md`, `chart/values.yaml:9-11` |
1212

13-
> All previously reported issues #2–9 from ISFSMS.md remain resolved; only the Secret template (#1 originally) has regressed.
13+
> Previously reported hardening issues remain resolved. Current accepted/documented risks are trusted Helm values and development-only default credentials.
1414
1515
## Detailed findings
1616

1717
### 1. SQL injection via Helm template `mysql.table` value (High)
1818

1919
**Category:** V5 Validation — Injection (CWE-89)
2020
**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`.
21+
**Sink:** `chart/harvest.sql`, rendered through `tpl` in `chart/templates/configmap.yaml`.
2222

2323
**Evidence:**
2424
The SQL file contains:
@@ -48,37 +48,20 @@ The Helm template risk is accepted. Chart installers (`helm install`, `--set`) a
4848

4949
### 2. Missing Secret template for MariaDB credentials (High)
5050

51-
**Category:** V14 Configuration — Missing security resource
51+
**Category:** V14 Configuration — Missing security resource
52+
**Status:** Resolved. `chart/templates/secret.yaml` exists and renders the `mariadb-credentials` Secret used by the StatefulSet and CronJob.
5253

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 }}
54+
**Verification:**
55+
```bash
56+
helm template harvest chart | grep -A8 'name: mariadb-credentials'
7457
```
7558

7659
### 3. Default weak MariaDB credentials (Medium)
7760

7861
**Category:** V2 Authentication — Weak credentials; V14 Configuration — Insecure defaults (CWE-521, CWE-1392)
7962

8063
**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.
64+
In `chart/values.yaml`, the bundled MariaDB defaults (`mariadb.rootPassword`, `mariadb.user`, and `mariadb.password`) are all set to the literal string `harvest`. When the bundled database is enabled, the NetworkPolicy is disabled by default (`networkPolicy.enabled: false`), meaning MariaDB is accessible from any pod in the cluster with these well-known credentials.
8265

8366
**Impact:**
8467
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.
@@ -87,7 +70,6 @@ If a deployment uses the defaults (e.g., an automated pipeline that neglects to
8770
The chart is installed with default values, a likely accidental scenario for users who skip reading the “override for production” note.
8871

8972
**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.
73+
- For production, set `mariadb.enabled=false` and point `mysql.host` at an externally managed database.
74+
- Always override `mariadb.user` and `mariadb.password` for production/external databases.
75+
- A future hardening change could enforce non-empty credentials with `required` or generate strong random install-time passwords.

README.md

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,27 @@
11
# DuckDB SQL Pipeline for Harvesting Consultations
22

33
A single SQL file pulls consultation data from WA government Citizen Space and
4-
EngagementHQ APIs, normalises it, and mirrors it into MariaDB for downstream consumption.
4+
EngagementHQ APIs, normalises it, and mirrors it into a MariaDB table for downstream consumption.
55
The runtime harvest path is DuckDB SQL only; Python is not required.
66

77
## Quick start
88

9-
Set the standard [DuckDB MySQL environment variables](https://duckdb.org/docs/current/core_extensions/mysql#configuration) before running locally:
9+
The recommended path is Helm, because `chart/harvest.sql` is templated with the
10+
configured output table name.
11+
12+
For local DuckDB runs, set the standard [DuckDB MySQL environment variables](https://duckdb.org/docs/current/core_extensions/mysql#configuration), render the table name, then run the rendered SQL:
1013

1114
```bash
1215
export MYSQL_HOST=localhost MYSQL_USER=harvest MYSQL_PWD=harvest MYSQL_DATABASE=harvest
13-
duckdb -c ".read chart/harvest.sql"
16+
sed 's/{{ .Values.mysql.table }}/consultations/g' chart/harvest.sql > /tmp/harvest.sql
17+
duckdb -c ".read /tmp/harvest.sql"
1418
```
1519

1620
> **Note:** The pipeline needs a dedicated MySQL user with write access to the
17-
> `harvest` database. The local defaults use user/password `harvest` for developer
18-
> convenience. Override credentials for production.
21+
> target database/table. Helm installs write to `mysql.database`.`mysql.table`,
22+
> defaulting to `harvest`.`consultations`. Use a simple table identifier such as
23+
> `consultations`; do not pass untrusted input to `mysql.table`. The local defaults
24+
> use user/password `harvest` for developer convenience. Override credentials for production.
1925
2026
## Kubernetes
2127

@@ -28,23 +34,31 @@ just test # Trigger a one-off harvest job
2834
just clean # Tear down cluster
2935
```
3036

37+
Defaults deploy an in-cluster MariaDB for local/dev use and write to table
38+
`harvest.consultations`. For production, disable the bundled database with
39+
`--set mariadb.enabled=false` and point `mysql.host` at an externally managed
40+
MySQL/MariaDB service. Change the table with `--set mysql.table=...` or the
41+
`table` variable in `justfile`.
42+
3143
### Helm install
3244

3345
```bash
3446
helm upgrade --install harvest chart \
3547
--namespace harvest-consultations --create-namespace \
36-
--set mysql.host=mariadb
48+
--set mysql.host=mariadb \
49+
--set mysql.table=consultations
3750
```
3851

39-
Override for external databases or production credentials:
52+
For production/external databases, disable the bundled MariaDB StatefulSet and provide the external host plus credentials:
4053

4154
```bash
4255
helm upgrade --install harvest chart \
4356
--namespace harvest-consultations --create-namespace \
44-
--set mysql.host=external-db \
57+
--set mariadb.enabled=false \
58+
--set mysql.host=external-db.example.internal \
4559
--set mysql.database=harvest \
60+
--set mysql.table=consultations \
4661
--set harvest.schedule="@daily" \
47-
--set mariadb.rootPassword='change-me' \
4862
--set mariadb.user=harvest \
4963
--set mariadb.password='change-me'
5064
```
@@ -66,19 +80,21 @@ just ci-test # kind → helm install → harvest job → dump → valida
6680
| Key | Default | Description |
6781
|-----|---------|-------------|
6882
| `mysql.host` | `mariadb` | MySQL hostname for the harvest job |
69-
| `mysql.database` | `harvest` | Database name |
70-
| `mariadb.rootPassword` | `harvest` | MariaDB root password (init only; not used by app/healthcheck) |
71-
| `mariadb.user` | `harvest` | Application database user |
72-
| `mariadb.password` | `harvest` | Application database password |
73-
| `mariadb.image.repository` | `mariadb` | MariaDB image |
74-
| `mariadb.image.tag` | `11` | MariaDB image tag |
75-
| `mariadb.storage.size` | `1Gi` | PVC size for MariaDB data |
76-
| `mariadb.storage.storageClassName` | (unset) | StorageClass for PVC (set to `"encrypted"` for at-rest encryption) |
77-
| `mariadb.resources.requests.memory` | `256Mi` | MariaDB memory request |
78-
| `mariadb.resources.requests.cpu` | `100m` | MariaDB CPU request |
79-
| `mariadb.resources.limits.memory` | `1Gi` | MariaDB memory limit |
80-
| `mariadb.resources.limits.cpu` | `1` | MariaDB CPU limit |
81-
| `networkPolicy.enabled` | `false` | Enable NetworkPolicy to restrict ingress to MariaDB |
83+
| `mysql.database` | `harvest` | Target MySQL database name |
84+
| `mysql.table` | `consultations` | Target MySQL table replaced by each harvest run |
85+
| `mariadb.enabled` | `true` | Deploy bundled MariaDB StatefulSet/Service for local/dev; set `false` for production/external databases |
86+
| `mariadb.rootPassword` | `harvest` | Bundled MariaDB root password (init only; not rendered when `mariadb.enabled=false`) |
87+
| `mariadb.user` | `harvest` | Application database user for bundled or external DB |
88+
| `mariadb.password` | `harvest` | Application database password for bundled or external DB |
89+
| `mariadb.image.repository` | `mariadb` | Bundled MariaDB image |
90+
| `mariadb.image.tag` | `11` | Bundled MariaDB image tag |
91+
| `mariadb.storage.size` | `1Gi` | PVC size for bundled MariaDB data |
92+
| `mariadb.storage.storageClassName` | (unset) | StorageClass for bundled MariaDB PVC (set to `"encrypted"` for at-rest encryption) |
93+
| `mariadb.resources.requests.memory` | `256Mi` | Bundled MariaDB memory request |
94+
| `mariadb.resources.requests.cpu` | `100m` | Bundled MariaDB CPU request |
95+
| `mariadb.resources.limits.memory` | `1Gi` | Bundled MariaDB memory limit |
96+
| `mariadb.resources.limits.cpu` | `1` | Bundled MariaDB CPU limit |
97+
| `networkPolicy.enabled` | `false` | Enable NetworkPolicy to restrict ingress to bundled MariaDB; ignored when `mariadb.enabled=false` |
8298
| `harvest.schedule` | `@hourly` | CronJob schedule |
8399
| `harvest.image.repository` | `ghcr.io/wagov-dtt/harvest-duckdb` | DuckDB image with extensions pre-installed |
84100
| `harvest.image.tag` | `""` | Image tag override; empty computes `{chart-version}-duckdb{appVersion without dots}` |
@@ -91,7 +107,7 @@ just ci-test # kind → helm install → harvest job → dump → valida
91107

92108
| Path | Purpose |
93109
|------|---------|
94-
| `chart/harvest.sql` | SQL-only DuckDB harvest pipeline |
110+
| `chart/harvest.sql` | SQL-only DuckDB harvest pipeline; Helm templates `mysql.table` into the final write statement |
95111
| `chart/templates/secret.yaml` | Mariadb-credentials Secret (auto-generated from values) |
96112
| `chart/templates/networkpolicy.yaml` | Optional NetworkPolicy for MariaDB ingress isolation |
97113
| `chart/` | Helm chart (hand-written, source of truth) |

chart/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ apiVersion: v2
22
name: harvest-consultations
33
description: DuckDB harvest pipeline for WA government consultation data
44
type: application
5-
version: 0.5.4
5+
version: 0.5.5
66
appVersion: "1.5.2"

chart/harvest.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
-- harvest.sql — DuckDB SQL pipeline for harvesting consultations
2-
-- Run: duckdb -c ".read harvest.sql"
2+
-- Rendered by Helm so mysql.table can set the target table name.
33
-- MySQL target via DuckDB MySQL env vars: MYSQL_HOST, MYSQL_USER, MYSQL_PWD, MYSQL_DATABASE
44
-- https://duckdb.org/docs/current/core_extensions/mysql#configuration
55
--

chart/templates/networkpolicy.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{{- if .Values.networkPolicy.enabled }}
1+
{{- if and .Values.networkPolicy.enabled .Values.mariadb.enabled }}
22
apiVersion: networking.k8s.io/v1
33
kind: NetworkPolicy
44
metadata:

chart/templates/secret.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ metadata:
77
{{- include "harvest-consultations.labels" . | nindent 4 }}
88
type: Opaque
99
data:
10+
{{- if .Values.mariadb.enabled }}
1011
MARIADB_ROOT_PASSWORD: {{ .Values.mariadb.rootPassword | b64enc | quote }}
12+
{{- end }}
1113
MARIADB_USER: {{ .Values.mariadb.user | b64enc | quote }}
1214
MARIADB_PASSWORD: {{ .Values.mariadb.password | b64enc | quote }}

chart/templates/service.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{{- if .Values.mariadb.enabled }}
12
apiVersion: v1
23
kind: Service
34
metadata:
@@ -12,3 +13,4 @@ spec:
1213
targetPort: 3306
1314
selector:
1415
app: mariadb
16+
{{- end }}

chart/templates/statefulset.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
{{- if .Values.mariadb.enabled }}
12
apiVersion: apps/v1
23
kind: StatefulSet
34
metadata:
@@ -75,3 +76,4 @@ spec:
7576
resources:
7677
requests:
7778
storage: {{ .Values.mariadb.storage.size | quote }}
79+
{{- end }}

chart/values.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ mysql:
44
table: consultations
55

66
mariadb:
7-
# rootPassword is only used for MariaDB initialization; the application and
8-
# readiness probe connect via the dedicated user/password below.
7+
# Enable only for local/dev installs. For production, use an external database
8+
# and set mysql.host plus mariadb.user/password.
9+
enabled: true
10+
# rootPassword is only used when the bundled MariaDB StatefulSet is enabled;
11+
# the application and readiness probe connect via the dedicated user/password below.
912
rootPassword: harvest
1013
user: harvest
1114
password: harvest

0 commit comments

Comments
 (0)