Skip to content

Commit feff097

Browse files
committed
Release v0.5.4
1 parent 2daf176 commit feff097

13 files changed

Lines changed: 195 additions & 91 deletions

File tree

.github/workflows/build-image.yaml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Build Image
2+
3+
on:
4+
push:
5+
branches: [main]
6+
tags:
7+
- 'v*'
8+
workflow_dispatch:
9+
10+
permissions:
11+
packages: write
12+
contents: read
13+
14+
jobs:
15+
build:
16+
name: Build and push image
17+
runs-on: ubuntu-latest
18+
timeout-minutes: 30
19+
permissions:
20+
packages: write
21+
contents: read
22+
steps:
23+
- name: Checkout
24+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
25+
26+
- name: Setup mise
27+
uses: jdx/mise-action@1648a7812b9aeae629881980618f079932869151 # v4.0.1
28+
with:
29+
install: true
30+
cache: true
31+
32+
- name: Set up Docker Buildx
33+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
34+
35+
- name: Login to GHCR
36+
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
37+
with:
38+
registry: ghcr.io
39+
username: ${{ github.actor }}
40+
password: ${{ secrets.GITHUB_TOKEN }}
41+
42+
- name: Build and push (edge)
43+
if: github.ref == 'refs/heads/main'
44+
run: just docker-build edge
45+
46+
- name: Build and push (release)
47+
if: startsWith(github.ref, 'refs/tags/v')
48+
run: just docker-build-release "${GITHUB_REF_NAME#v}"

.github/workflows/ci-nightly.yaml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,6 @@ jobs:
2727
install: true
2828
cache: true
2929

30-
- name: Install just
31-
run: |
32-
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
33-
3430
- name: Run e2e test
3531
run: just ci-test
3632

.github/workflows/release.yaml

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,25 +28,18 @@ jobs:
2828
install: true
2929
cache: true
3030

31-
- name: Install just
32-
run: |
33-
curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
31+
- name: Package release chart
32+
if: startsWith(github.ref, 'refs/tags/v')
33+
run: just helm-package "${GITHUB_REF_NAME#v}"
3434

35-
- name: Generate and package helm chart
36-
run: |
37-
# Set chart version from git tag (strip leading 'v'), fallback to 0.0.0-dev for manual runs
38-
CHART_VERSION="${GITHUB_REF_NAME#v}"
39-
if [ -z "$CHART_VERSION" ] || [ "$CHART_VERSION" = "$GITHUB_REF_NAME" ]; then
40-
CHART_VERSION="0.0.0-dev"
41-
fi
42-
sed -i "s/^version:.*/version: ${CHART_VERSION}/" chart/Chart.yaml
43-
helm package chart -d dist/
44-
echo "CHART_FILE=$(ls dist/*.tgz)" >> $GITHUB_ENV
35+
- name: Package dev chart
36+
if: ${{ !startsWith(github.ref, 'refs/tags/v') }}
37+
run: just helm-package 0.0.0-dev
4538

4639
- name: Push to GHCR
4740
run: |
4841
echo "${{ secrets.GITHUB_TOKEN }}" | helm registry login ghcr.io --username "${{ github.actor }}" --password-stdin
49-
helm push "${{ env.CHART_FILE }}" oci://ghcr.io/${{ github.repository_owner }}
42+
helm push dist/*.tgz oci://ghcr.io/${{ github.repository_owner }}
5043
5144
- name: Create GitHub Release
5245
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0

Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
ARG DUCKDB_VERSION=1.5.2
2+
FROM duckdb/duckdb:${DUCKDB_VERSION}
3+
ENV HOME=/
4+
SHELL ["/duckdb", "-c"]
5+
RUN INSTALL httpfs; INSTALL mysql;
6+
USER 1000:1000

ISSUES.md

Lines changed: 88 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,62 +1,93 @@
11
# Audit Issues
22

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
54
65
## Findings summary
76

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.

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,8 @@ just ci-test # kind → helm install → harvest job → dump → valida
8080
| `mariadb.resources.limits.cpu` | `1` | MariaDB CPU limit |
8181
| `networkPolicy.enabled` | `false` | Enable NetworkPolicy to restrict ingress to MariaDB |
8282
| `harvest.schedule` | `@hourly` | CronJob schedule |
83-
| `harvest.image.repository` | `duckdb/duckdb` | DuckDB image |
84-
| `harvest.image.tag` | `1.5.2` | DuckDB image tag |
83+
| `harvest.image.repository` | `ghcr.io/wagov-dtt/harvest-duckdb` | DuckDB image with extensions pre-installed |
84+
| `harvest.image.tag` | `""` | Image tag override; empty computes `{chart-version}-duckdb{appVersion without dots}` |
8585
| `harvest.resources.requests.memory` | `256Mi` | Harvest job memory request |
8686
| `harvest.resources.requests.cpu` | `100m` | Harvest job CPU request |
8787
| `harvest.resources.limits.memory` | `1Gi` | Harvest job memory limit |

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.3
5+
version: 0.5.4
66
appVersion: "1.5.2"

chart/harvest.sql

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,7 @@
1313
SET enable_http_logging = false;
1414
SET allow_unredacted_secrets = false;
1515

16-
INSTALL httpfs;
1716
LOAD httpfs;
18-
INSTALL mysql;
1917
LOAD mysql;
2018

2119
-- Lock down extension loading after required extensions are loaded

chart/templates/_helpers.tpl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,8 @@
22
app.kubernetes.io/name: {{ .Chart.Name }}
33
app.kubernetes.io/instance: {{ .Release.Name }}
44
{{- end }}
5+
6+
{{- define "harvest-consultations.harvestImageTag" -}}
7+
{{- $duckdb := .Chart.AppVersion | replace "." "" -}}
8+
{{- default (printf "%s-duckdb%s" .Chart.Version $duckdb) .Values.harvest.image.tag -}}
9+
{{- end }}

chart/templates/cronjob.yaml

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ spec:
1818
spec:
1919
containers:
2020
- name: harvest
21-
image: "{{ .Values.harvest.image.repository }}:{{ .Values.harvest.image.tag }}"
21+
image: "{{ .Values.harvest.image.repository }}:{{ include "harvest-consultations.harvestImageTag" . }}"
2222
imagePullPolicy: IfNotPresent
2323
command: ["duckdb", "-c", ".read /etc/config/harvest.sql"]
2424
resources:
@@ -34,8 +34,6 @@ spec:
3434
drop: ["ALL"]
3535
readOnlyRootFilesystem: true
3636
env:
37-
- name: HOME
38-
value: /tmp
3937
- name: MYSQL_HOST
4038
value: {{ .Values.mysql.host | quote }}
4139
- name: MYSQL_USER
@@ -54,12 +52,8 @@ spec:
5452
- name: config
5553
mountPath: /etc/config
5654
readOnly: true
57-
- name: duckdb-extensions
58-
mountPath: /tmp
5955
restartPolicy: Never
6056
volumes:
6157
- name: config
6258
configMap:
6359
name: harvest-config
64-
- name: duckdb-extensions
65-
emptyDir: {}

0 commit comments

Comments
 (0)