Skip to content

Commit bb88818

Browse files
Nicolas Brieusselclaude
authored andcommitted
feat: replace Infisical with OpenBao as OSS secrets backend
Deploys OpenBao (MPL 2.0) via ArgoCD app-of-apps with S3 state on Scaleway Object Storage, so secrets survive full cluster destroy/recreate. Key additions: - platform/{local,scaleway}/openbao.yml — OpenBao Helm chart (0.28.4), S3 backend pointing to backup-dev-id bucket, creds from K8s Secret - platform/{local,scaleway}/external-secrets.yml — ESO (2.6.0) - apps/openbao-init/ — in-house Helm chart: PostSync init/unseal Job (reads/writes keys from S3), RBAC, per-app HCL policies, ESO ClusterSecretStore; idempotent across boots - clusters/{local,scaleway}/templates/openbao-init.yaml — ArgoCD Applications wiring the init chart into the app-of-apps tree - Sync-wave order: wave 0 openbao → wave 1 ESO → wave 2 openbao-init - .github/workflows/ci.yml — 3 jobs: helm-lint, infisical-absent, secrets-restore (destroy/recreate canary test against real Scaleway S3) Cross-repo dependency: infra Terraform must create scaleway-s3-credentials Secret in openbao namespace before ArgoCD first sync. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KoJ6zVHxYtUKtSNXinmct7
1 parent 9878608 commit bb88818

31 files changed

Lines changed: 2199 additions & 1 deletion

.github/workflows/ci.yml

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
name: CI
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
jobs:
10+
# ── Job 1: Helm lint & render ──────────────────────────────────────────────
11+
helm-lint:
12+
name: Helm lint
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Install Helm
18+
uses: azure/setup-helm@v4
19+
with:
20+
version: "latest"
21+
22+
- name: Lint openbao-init (local values)
23+
run: helm lint apps/openbao-init/ -f apps/openbao-init/values-local.yaml
24+
25+
- name: Lint openbao-init (scaleway values)
26+
run: helm lint apps/openbao-init/ -f apps/openbao-init/values-scaleway.yaml
27+
28+
- name: Render bootstrap chart (local)
29+
run: helm template boot bootstrap/ --set env=local --set revision=main
30+
31+
- name: Render clusters/local chart
32+
run: helm template loc clusters/local/ --set revision=main
33+
34+
- name: Render clusters/scaleway chart
35+
run: helm template loc clusters/scaleway/ --set revision=main
36+
37+
# ── Job 2: Infisical absent ────────────────────────────────────────────────
38+
infisical-absent:
39+
name: Infisical absent
40+
runs-on: ubuntu-latest
41+
steps:
42+
- uses: actions/checkout@v4
43+
44+
- name: Assert no Infisical references in active GitOps manifests
45+
run: |
46+
if git grep -ri infisical apps/ platform/ clusters/ bootstrap/ 2>/dev/null; then
47+
echo "ERROR: Infisical references found in GitOps manifests"
48+
exit 1
49+
fi
50+
echo "OK: No Infisical references found"
51+
52+
# ── Job 3: Secrets restore (destroy/recreate) ──────────────────────────────
53+
secrets-restore:
54+
name: Secrets restore
55+
runs-on: ubuntu-latest
56+
environment: dev
57+
env:
58+
ARGOCD_VERSION: "v2.13.0"
59+
BAO_VERSION: "2.0.3"
60+
REPO_URL: https://github.com/${{ github.repository }}
61+
BRANCH: ${{ github.head_ref || github.ref_name }}
62+
63+
steps:
64+
- uses: actions/checkout@v4
65+
66+
- name: Install Helm
67+
uses: azure/setup-helm@v4
68+
with:
69+
version: "latest"
70+
71+
- name: Install bao CLI
72+
run: |
73+
curl -sSfL \
74+
"https://github.com/openbao/openbao/releases/download/v${BAO_VERSION}/bao_${BAO_VERSION}_linux_amd64.zip" \
75+
-o bao.zip
76+
unzip -q bao.zip bao
77+
sudo mv bao /usr/local/bin/bao
78+
79+
- name: Start minikube
80+
uses: medyagh/setup-minikube@latest
81+
82+
- name: Install ArgoCD
83+
run: |
84+
kubectl create namespace argocd
85+
kubectl apply -n argocd \
86+
-f "https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml"
87+
kubectl rollout status deployment/argocd-server -n argocd --timeout=180s
88+
89+
- name: Expose ArgoCD + login
90+
id: argocd-login
91+
run: |
92+
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
93+
sleep 8
94+
PASS=$(kubectl -n argocd get secret argocd-initial-admin-secret \
95+
-o jsonpath="{.data.password}" | base64 -d)
96+
argocd login localhost:8080 --username admin --password "${PASS}" --insecure
97+
# Allow ArgoCD to pull from this (possibly private) repo via GITHUB_TOKEN
98+
argocd repo add "${REPO_URL}" \
99+
--username x-access-token \
100+
--password "${{ secrets.GITHUB_TOKEN }}" \
101+
--insecure-skip-server-verification || true
102+
103+
- name: Inject S3 credentials Secret (pre-sync)
104+
run: |
105+
kubectl create namespace openbao --dry-run=client -o yaml | kubectl apply -f -
106+
kubectl create secret generic scaleway-s3-credentials \
107+
--namespace openbao \
108+
--from-literal=access_key="${{ secrets.SCW_ACCESS_KEY }}" \
109+
--from-literal=secret_key="${{ secrets.SCW_SECRET_KEY }}" \
110+
--from-literal=bucket="${{ vars.SCW_S3_BUCKET }}"
111+
112+
- name: Apply bootstrap Application
113+
run: |
114+
argocd app create bootstrap \
115+
--repo "${REPO_URL}" \
116+
--path bootstrap \
117+
--dest-server https://kubernetes.default.svc \
118+
--dest-namespace argocd \
119+
--helm-set env=local \
120+
--helm-set "revision=${BRANCH}" \
121+
--helm-set "repoURL=${REPO_URL}" \
122+
--sync-policy automated \
123+
--upsert
124+
125+
- name: Wait for OpenBao to unseal
126+
id: wait-unseal
127+
run: |
128+
# Poll health endpoint until initialized + unsealed (HTTP 200)
129+
BAO_ADDR="http://$(kubectl get svc openbao -n openbao \
130+
-o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo 'pending'):8200"
131+
echo "BAO_ADDR=${BAO_ADDR}" >> "${GITHUB_ENV}"
132+
133+
for i in $(seq 1 72); do
134+
# Refresh ClusterIP in case the svc wasn't ready yet
135+
CIP=$(kubectl get svc openbao -n openbao -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
136+
if [ -n "${CIP}" ]; then
137+
BAO_ADDR="http://${CIP}:8200"
138+
echo "BAO_ADDR=${BAO_ADDR}" >> "${GITHUB_ENV}"
139+
fi
140+
141+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
142+
"${BAO_ADDR}/v1/sys/health" 2>/dev/null || echo "000")
143+
echo "Attempt ${i}/72 — OpenBao status: ${STATUS}"
144+
145+
if [ "${STATUS}" = "200" ] || [ "${STATUS}" = "429" ]; then
146+
echo "OpenBao is unsealed."
147+
break
148+
fi
149+
sleep 5
150+
done
151+
152+
if [ "${STATUS}" != "200" ] && [ "${STATUS}" != "429" ]; then
153+
echo "ERROR: OpenBao did not unseal within 6 minutes"
154+
kubectl get events -n openbao --sort-by='.lastTimestamp' | tail -20 || true
155+
kubectl logs -l 'app.kubernetes.io/name=openbao' -n openbao --tail=50 || true
156+
exit 1
157+
fi
158+
159+
- name: Write canary secret
160+
run: |
161+
CANARY_VALUE="survive-${{ github.run_id }}"
162+
echo "CANARY_VALUE=${CANARY_VALUE}" >> "${GITHUB_ENV}"
163+
164+
# Authenticate via Kubernetes auth using the external-secrets SA token
165+
SA_JWT=$(kubectl create token external-secrets -n external-secrets --duration=5m)
166+
BAO_TOKEN=$(curl -sf "${BAO_ADDR}/v1/auth/kubernetes/login" \
167+
-d "{\"role\":\"external-secrets\",\"jwt\":\"${SA_JWT}\"}" \
168+
| jq -r '.auth.client_token')
169+
170+
curl -sf "${BAO_ADDR}/v1/kv/data/apps/test/canary" \
171+
-H "X-Vault-Token: ${BAO_TOKEN}" \
172+
-d "{\"data\":{\"value\":\"${CANARY_VALUE}\"}}"
173+
174+
echo "Canary written: ${CANARY_VALUE}"
175+
176+
# ── Cluster destroy & recreate ─────────────────────────────────────────
177+
178+
- name: Destroy minikube
179+
run: minikube delete
180+
181+
- name: Recreate minikube
182+
uses: medyagh/setup-minikube@latest
183+
184+
- name: Re-install ArgoCD
185+
run: |
186+
kubectl create namespace argocd
187+
kubectl apply -n argocd \
188+
-f "https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml"
189+
kubectl rollout status deployment/argocd-server -n argocd --timeout=180s
190+
191+
- name: Re-expose ArgoCD + login
192+
run: |
193+
kubectl port-forward svc/argocd-server -n argocd 8080:443 &
194+
sleep 8
195+
PASS=$(kubectl -n argocd get secret argocd-initial-admin-secret \
196+
-o jsonpath="{.data.password}" | base64 -d)
197+
argocd login localhost:8080 --username admin --password "${PASS}" --insecure
198+
argocd repo add "${REPO_URL}" \
199+
--username x-access-token \
200+
--password "${{ secrets.GITHUB_TOKEN }}" \
201+
--insecure-skip-server-verification || true
202+
203+
- name: Re-inject S3 credentials Secret
204+
run: |
205+
kubectl create namespace openbao --dry-run=client -o yaml | kubectl apply -f -
206+
kubectl create secret generic scaleway-s3-credentials \
207+
--namespace openbao \
208+
--from-literal=access_key="${{ secrets.SCW_ACCESS_KEY }}" \
209+
--from-literal=secret_key="${{ secrets.SCW_SECRET_KEY }}" \
210+
--from-literal=bucket="${{ vars.SCW_S3_BUCKET }}"
211+
212+
- name: Re-apply bootstrap Application
213+
run: |
214+
argocd app create bootstrap \
215+
--repo "${REPO_URL}" \
216+
--path bootstrap \
217+
--dest-server https://kubernetes.default.svc \
218+
--dest-namespace argocd \
219+
--helm-set env=local \
220+
--helm-set "revision=${BRANCH}" \
221+
--helm-set "repoURL=${REPO_URL}" \
222+
--sync-policy automated \
223+
--upsert
224+
225+
- name: Wait for OpenBao to unseal after recreate
226+
id: wait-unseal-2
227+
run: |
228+
for i in $(seq 1 72); do
229+
CIP=$(kubectl get svc openbao -n openbao -o jsonpath='{.spec.clusterIP}' 2>/dev/null || true)
230+
if [ -n "${CIP}" ]; then BAO_ADDR="http://${CIP}:8200"; fi
231+
232+
STATUS=$(curl -sf -o /dev/null -w "%{http_code}" \
233+
"${BAO_ADDR}/v1/sys/health" 2>/dev/null || echo "000")
234+
echo "Attempt ${i}/72 — OpenBao status: ${STATUS}"
235+
236+
if [ "${STATUS}" = "200" ] || [ "${STATUS}" = "429" ]; then
237+
echo "OpenBao is unsealed after recreate."
238+
break
239+
fi
240+
sleep 5
241+
done
242+
243+
if [ "${STATUS}" != "200" ] && [ "${STATUS}" != "429" ]; then
244+
echo "ERROR: OpenBao did not unseal after cluster recreate"
245+
kubectl get events -n openbao --sort-by='.lastTimestamp' | tail -20 || true
246+
exit 1
247+
fi
248+
249+
echo "BAO_ADDR=${BAO_ADDR}" >> "${GITHUB_ENV}"
250+
251+
- name: Assert canary secret restored (SC-001)
252+
run: |
253+
SA_JWT=$(kubectl create token external-secrets -n external-secrets --duration=5m)
254+
BAO_TOKEN=$(curl -sf "${BAO_ADDR}/v1/auth/kubernetes/login" \
255+
-d "{\"role\":\"external-secrets\",\"jwt\":\"${SA_JWT}\"}" \
256+
| jq -r '.auth.client_token')
257+
258+
RESTORED=$(curl -sf "${BAO_ADDR}/v1/kv/data/apps/test/canary" \
259+
-H "X-Vault-Token: ${BAO_TOKEN}" \
260+
| jq -r '.data.data.value')
261+
262+
echo "Expected : ${CANARY_VALUE}"
263+
echo "Restored : ${RESTORED}"
264+
265+
if [ "${RESTORED}" != "${CANARY_VALUE}" ]; then
266+
echo "ERROR: Canary mismatch — secret not restored after destroy/recreate"
267+
exit 1
268+
fi
269+
echo "OK: SC-001 verified — secrets survive cluster destroy/recreate"
270+
271+
- name: Assert no Infisical pods (SC-003)
272+
run: |
273+
if kubectl get pods -A -o name | grep -i infisical; then
274+
echo "ERROR: Infisical pods found in cluster"
275+
exit 1
276+
fi
277+
echo "OK: No Infisical pods in cluster"

.gitignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# OS / editor
2+
.DS_Store
3+
Thumbs.db
4+
*.swp
5+
*.swo
6+
.vscode/
7+
.idea/
8+
9+
# Secrets (never commit)
10+
*.secret.yaml
11+
*.secret.yml
12+
kubeconfig*
13+
*.key
14+
*.pem
15+
secrets/
16+
17+
# Helm
18+
*.tgz
19+
20+
# Temp
21+
tmp/

.specify/feature.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"feature_directory": "specs/001-replace-infisical-oss-secrets"
3+
}

CLAUDE.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,21 @@ Always target the local cluster explicitly to avoid accidental execution against
4040
```bash
4141
# Lint a Helm chart
4242
helm lint apps/demo/ -f apps/demo/values-local.yaml
43+
helm lint apps/openbao-init/ -f apps/openbao-init/values-local.yaml
44+
helm lint apps/openbao-init/ -f apps/openbao-init/values-scaleway.yaml
4345

4446
# Render an app chart to inspect output
4547
helm template demo apps/demo/ -f apps/demo/values-local.yaml
48+
helm template openbao-init apps/openbao-init/ -f apps/openbao-init/values-local.yaml
4649

4750
# Render the app-of-apps charts (confirm revision propagation)
4851
helm template boot bootstrap/ --set env=local --set revision=<branch>
49-
helm template loc clusters/local/ --set revision=<branch>
52+
helm template loc clusters/local/ --set revision=<branch> --set repoURL=<repo>
53+
helm template loc clusters/scaleway/ --set revision=<branch> --set repoURL=<repo>
5054

5155
# Validate a platform Application manifest
5256
kubectl --context minikube apply --dry-run=client -f platform/local/kube-prometheus-stack.yml
57+
kubectl --context minikube apply --dry-run=client -f platform/local/openbao.yml
5358
```
5459

5560
## Adding things
@@ -63,3 +68,39 @@ kubectl --context minikube apply --dry-run=client -f platform/local/kube-prometh
6368
4. `bootstrap/templates/staging.yaml` guarded by `{{- if eq .Values.env "staging" }}` as the entry point for external provisioners
6469

6570
**New platform tool:** Add a file to `platform/<env>/` following `platform/local/ingress-nginx.yml`.
71+
72+
<!-- SPECKIT START -->
73+
## Active Feature
74+
75+
**Feature**: Replace Infisical with OSS Secrets Backend
76+
**Plan**: [specs/001-replace-infisical-oss-secrets/plan.md](specs/001-replace-infisical-oss-secrets/plan.md)
77+
**Tasks**: [specs/001-replace-infisical-oss-secrets/tasks.md](specs/001-replace-infisical-oss-secrets/tasks.md)
78+
**Status**: Tasks generated — ready for `/speckit-implement`
79+
**Tool**: OpenBao (MPL 2.0) — decided 2026-06-21
80+
81+
### Quick reference
82+
83+
| Artifact | Path |
84+
|----------|------|
85+
| Spec | `specs/001-replace-infisical-oss-secrets/spec.md` |
86+
| Plan | `specs/001-replace-infisical-oss-secrets/plan.md` |
87+
| Research | `specs/001-replace-infisical-oss-secrets/research.md` |
88+
| Data model | `specs/001-replace-infisical-oss-secrets/data-model.md` |
89+
| Contracts | `specs/001-replace-infisical-oss-secrets/contracts/` |
90+
| Quickstart | `specs/001-replace-infisical-oss-secrets/quickstart.md` |
91+
| Tasks | `specs/001-replace-infisical-oss-secrets/tasks.md` |
92+
93+
### MVP scope (US1, Phases 1–3, tasks T001–T020)
94+
95+
1. Scaffold `apps/openbao-init/` Helm chart (T001–T004)
96+
2. Write `platform/<env>/openbao.yml` + `external-secrets.yml` (T005–T009)
97+
3. Write init Job, RBAC, policies, ClusterSecretStore, cluster templates, sync waves (T010–T018)
98+
4. Validate: `helm lint apps/openbao-init/` + local Scenario 1 smoke test (T019–T020)
99+
100+
### Key constraints
101+
102+
- `scaleway-s3-credentials` Secret created by infra Terraform, not this repo — init Job reads it
103+
- S3 backend: no HA on Scaleway (no DynamoDB locking) → single replica
104+
- Init Job must be idempotent; root token revoked after first init
105+
- All external Helm chart versions must be pinned explicitly
106+
<!-- SPECKIT END -->

apps/openbao-init/.helmignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.helmignore
2+
.gitignore
3+
*.md
4+
specs/

apps/openbao-init/Chart.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
apiVersion: v2
2+
name: openbao-init
3+
version: 0.1.0
4+
description: OpenBao init, unseal, and ESO configuration
5+
type: application
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: external-secrets.io/v1beta1
2+
kind: ClusterSecretStore
3+
metadata:
4+
name: {{ .Values.eso.clusterSecretStoreName }}
5+
spec:
6+
provider:
7+
vault:
8+
server: {{ .Values.openbao.address | quote }}
9+
path: "kv"
10+
version: "v2"
11+
auth:
12+
kubernetes:
13+
mountPath: "kubernetes"
14+
role: "external-secrets"
15+
serviceAccountRef:
16+
name: external-secrets
17+
namespace: external-secrets

0 commit comments

Comments
 (0)