diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 293dca512..92d48aefd 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -70,6 +70,11 @@ jobs: context: ./authbridge/authproxy/quickstart/demo-app dockerfile: Dockerfile + # Vault Fetcher - init container for fetching secrets from Vault + - name: vault-fetcher + context: ./authbridge + dockerfile: vault-fetcher/Dockerfile + steps: # 1. Checkout code - name: Checkout repository diff --git a/CLAUDE.md b/CLAUDE.md index a1f3fde72..52fe6368f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -157,6 +157,7 @@ All images are pushed to `ghcr.io/kagenti/kagenti-extensions/`: | `spiffe-helper` | `authbridge/spiffe-helper/Dockerfile` | Fetches SPIFFE credentials from SPIRE | | `auth-proxy` | `authbridge/authproxy/Dockerfile` | Example pass-through proxy (for demos) | | `demo-app` | `authbridge/authproxy/quickstart/demo-app/Dockerfile` | Demo target service | +| **`vault-fetcher`** | **`authbridge/vault-fetcher/Dockerfile`** | **Init container for fetching secrets from Vault using SPIFFE identity** | ## Pre-commit Hooks diff --git a/STACKED_PR_WORKFLOW.md b/STACKED_PR_WORKFLOW.md new file mode 100644 index 000000000..c99e3df75 --- /dev/null +++ b/STACKED_PR_WORKFLOW.md @@ -0,0 +1,448 @@ +# Stacked PR Workflow — kagenti-extensions + +This document describes how to work with stacked PRs in this repository, including the critical rebase process when earlier branches receive changes. + +## Current Stack: Vault Pattern Implementation + +``` +main + │ + ├─> PR #379: feat/vault-integration (Phase 1) + │ └─ authlib/vault library + │ └─ Branch: feat/vault-integration + │ + └──> PR #380: feat/vault-fetcher-cli (Phase 2) + │ └─ vault-fetcher CLI tool + │ └─ Branch: feat/vault-fetcher-cli + │ └─ Base: feat/vault-integration + │ + └──> PR #TBD: feat/vault-webhook (Phase 3) + │ └─ Webhook integration docs + │ └─ Branch: feat/vault-webhook + │ └─ Base: feat/vault-fetcher-cli + │ + └──> PR #TBD: feat/vault-docs-polish (Phase 4) + └─ CI/CD, docs, polish + └─ Branch: feat/vault-docs-polish + └─ Base: feat/vault-webhook +``` + +## Understanding Stacked PRs + +### What is a Stacked PR? + +A stacked PR is when one pull request's base branch is another feature branch (not `main`). + +**Example:** +- PR #379: `feat/vault-integration` → `main` +- PR #380: `feat/vault-fetcher-cli` → `feat/vault-integration` ← Stacked! + +### Benefits + +1. **Parallel development** — Don't wait for Phase 1 approval to start Phase 2 +2. **Isolated reviews** — Each PR shows only its changes +3. **Clear dependencies** — Stack structure shows what depends on what +4. **Faster iteration** — Continue working while earlier PRs are in review + +### How GitHub Shows Stacked PRs + +When viewing PR #380 (Phase 2): +- **Files changed:** Only shows vault-fetcher files (not authlib files) +- **Commits:** Shows Phase 2 commits + all Phase 1 commits +- **Base branch:** `feat/vault-integration` (not `main`) + +## Creating Stacked PRs + +### Method 1: Manual (What We Used) + +```bash +# Create Phase 1 +git checkout -b feat/vault-integration main +# ... make changes ... +git commit -m "feat: Phase 1" +git push -u origin feat/vault-integration +gh pr create --base main --title "Phase 1" --draft + +# Create Phase 2 (stacked on Phase 1) +git checkout -b feat/vault-fetcher-cli feat/vault-integration +# ... make changes ... +git commit -m "feat: Phase 2" +git push -u origin feat/vault-fetcher-cli +gh pr create --base feat/vault-integration --title "Phase 2" --draft + +# Create Phase 3 (stacked on Phase 2) +git checkout -b feat/vault-webhook feat/vault-fetcher-cli +# ... make changes ... +git commit -m "feat: Phase 3" +git push -u origin feat/vault-webhook +gh pr create --base feat/vault-fetcher-cli --title "Phase 3" --draft + +# And so on... +``` + +### Method 2: gh-stack (Requires Repo Settings) + +```bash +# Initialize stack +gh stack init --adopt feat/vault-integration feat/vault-fetcher-cli feat/vault-webhook feat/vault-docs-polish + +# View stack +gh stack view + +# Submit all PRs +gh stack submit --draft +``` + +**Note:** `gh stack submit` requires "Stacked PRs" to be enabled in repository settings. + +## The Critical Rebase Process + +### When Earlier Branches Get Changes + +**Scenario:** PR #379 (Phase 1) receives review feedback. You make changes to `feat/vault-integration`. + +**Problem:** Branches 2, 3, and 4 are now out of sync! + +``` +main + │ + ├─> feat/vault-integration (UPDATED ✏️) + │ + └──> feat/vault-fetcher-cli (OUT OF SYNC ⚠️) + │ + └──> feat/vault-webhook (OUT OF SYNC ⚠️) + │ + └──> feat/vault-docs-polish (OUT OF SYNC ⚠️) +``` + +### Solution: Cascade Rebase + +**You MUST rebase all subsequent branches onto the updated branch.** + +```bash +# Step 1: Make changes to Phase 1 +git checkout feat/vault-integration +# ... make changes based on review feedback ... +git commit -m "fix: address review feedback" +git push --force-with-lease origin feat/vault-integration + +# Step 2: Rebase Phase 2 onto updated Phase 1 +git checkout feat/vault-fetcher-cli +git rebase feat/vault-integration +# Resolve conflicts if any +git push --force-with-lease origin feat/vault-fetcher-cli + +# Step 3: Rebase Phase 3 onto updated Phase 2 +git checkout feat/vault-webhook +git rebase feat/vault-fetcher-cli +# Resolve conflicts if any +git push --force-with-lease origin feat/vault-webhook + +# Step 4: Rebase Phase 4 onto updated Phase 3 +git checkout feat/vault-docs-polish +git rebase feat/vault-webhook +# Resolve conflicts if any +git push --force-with-lease origin feat/vault-docs-polish +``` + +### Rebase Checklist + +After making changes to ANY branch in the stack: + +- [ ] **Identify affected branches** — All branches above the changed one +- [ ] **Rebase in order** — Start from the immediate child, work upward +- [ ] **Test after each rebase** — Ensure builds still work +- [ ] **Push with --force-with-lease** — Safer than --force +- [ ] **Notify reviewers** — Comment on PRs that rebase occurred + +### Example: Fixing Phase 1 After Review + +```bash +# Reviewer says: "authlib/vault/config.go needs better validation" + +# 1. Fix Phase 1 +git checkout feat/vault-integration +vim authbridge/authlib/vault/config.go +git add authbridge/authlib/vault/config.go +git commit -m "fix: improve config validation per review" +git push --force-with-lease origin feat/vault-integration + +# 2. Cascade rebase +git checkout feat/vault-fetcher-cli +git rebase feat/vault-integration +git push --force-with-lease origin feat/vault-fetcher-cli + +git checkout feat/vault-webhook +git rebase feat/vault-fetcher-cli +git push --force-with-lease origin feat/vault-webhook + +git checkout feat/vault-docs-polish +git rebase feat/vault-webhook +git push --force-with-lease origin feat/vault-docs-polish + +# 3. Comment on PRs +gh pr comment 379 --body "Updated based on review feedback" +gh pr comment 380 --body "Rebased onto updated Phase 1" +gh pr comment 381 --body "Rebased onto updated Phase 2" +gh pr comment 382 --body "Rebased onto updated Phase 3" +``` + +## Handling Conflicts + +### Conflict During Rebase + +```bash +git checkout feat/vault-fetcher-cli +git rebase feat/vault-integration + +# If conflicts occur: +# CONFLICT (content): Merge conflict in authbridge/go.work +Auto-merging authbridge/go.work +CONFLICT (content): Merge conflict in authbridge/go.work +``` + +**Resolution:** + +```bash +# 1. See what files have conflicts +git status + +# 2. Edit conflicted files +vim authbridge/go.work + +# 3. Look for conflict markers +<<<<<<>>>>>> + +# 4. Resolve conflicts, remove markers +# 5. Stage resolved files +git add authbridge/go.work + +# 6. Continue rebase +git rebase --continue + +# 7. Push +git push --force-with-lease origin feat/vault-fetcher-cli +``` + +### Aborting a Rebase + +If things go wrong: + +```bash +git rebase --abort +# Returns to state before rebase started +``` + +## Merging Strategy + +### Order of Merging + +**Always merge from bottom to top (Phase 1 → 2 → 3 → 4).** + +```bash +# 1. Merge Phase 1 +gh pr merge 379 --squash # or --rebase, or --merge + +# 2. Update Phase 2's base to main +gh pr edit 380 --base main +# Or GitHub may offer to do this automatically + +# 3. Merge Phase 2 +gh pr merge 380 --squash + +# 4. Update Phase 3's base to main +gh pr edit 381 --base main + +# 5. Merge Phase 3 +gh pr merge 381 --squash + +# 6. Update Phase 4's base to main +gh pr edit 382 --base main + +# 7. Merge Phase 4 +gh pr merge 382 --squash +``` + +### Alternative: Squash Before Merging + +```bash +# After Phase 1 merges, you can squash Phase 2-4 together +git checkout feat/vault-docs-polish # Top of stack +git rebase -i main + +# Interactive rebase: squash commits +# Then PR #382 (Phase 4) contains all changes from Phases 2-4 +# Merge just that one PR +``` + +## Common Scenarios + +### Scenario 1: Adding New Branch Mid-Stack + +**Want to add Phase 2.5 between Phase 2 and 3:** + +```bash +# Create branch from Phase 2 +git checkout feat/vault-fetcher-cli +git checkout -b feat/vault-fetcher-tests + +# Make changes +git commit -m "feat: add vault-fetcher tests" +git push -u origin feat/vault-fetcher-tests + +# Update Phase 3 to stack on new branch +git checkout feat/vault-webhook +git rebase feat/vault-fetcher-tests +git push --force-with-lease origin feat/vault-webhook + +# Create PR for new branch +gh pr create --base feat/vault-fetcher-cli --title "Phase 2.5: vault-fetcher tests" +``` + +### Scenario 2: Removing Branch from Stack + +**Want to remove Phase 3, merge Phase 4 directly to Phase 2:** + +```bash +# Rebase Phase 4 onto Phase 2 +git checkout feat/vault-docs-polish +git rebase feat/vault-fetcher-cli +git push --force-with-lease origin feat/vault-docs-polish + +# Update PR base +gh pr edit 382 --base feat/vault-fetcher-cli + +# Close Phase 3 PR +gh pr close 381 +``` + +### Scenario 3: Cherry-Picking Fix Across Stack + +**Fix in Phase 1 needs to apply to all branches:** + +```bash +# Make fix in Phase 1 +git checkout feat/vault-integration +git commit -m "fix: critical bug in auth.go" +git push --force-with-lease origin feat/vault-integration + +# Rebase all subsequent branches (CASCADE!) +git checkout feat/vault-fetcher-cli && git rebase feat/vault-integration && git push --force-with-lease +git checkout feat/vault-webhook && git rebase feat/vault-fetcher-cli && git push --force-with-lease +git checkout feat/vault-docs-polish && git rebase feat/vault-webhook && git push --force-with-lease +``` + +## Automation with gh-stack + +### Set Up gh-stack + +```bash +gh extension install github/gh-stack +gh stack init --adopt feat/vault-integration feat/vault-fetcher-cli feat/vault-webhook feat/vault-docs-polish +``` + +### Useful Commands + +```bash +# View current stack +gh stack view + +# Rebase entire stack automatically +gh stack rebase + +# Push all branches +gh stack push + +# Navigate stack +gh stack up # Move up one branch +gh stack down # Move down one branch +gh stack top # Jump to top of stack +gh stack bottom # Jump to bottom of stack +``` + +## Best Practices + +1. **Small, focused PRs** — Each phase should be reviewable independently +2. **Rebase immediately** — When changes land in earlier branches +3. **Test after rebase** — Run builds/tests after each rebase +4. **Use --force-with-lease** — Safer than --force +5. **Communicate rebases** — Comment on PRs when rebasing +6. **Keep stack shallow** — 3-5 branches max (we have 4, perfect) +7. **Document dependencies** — PR descriptions should mention stack position + +## Troubleshooting + +### "Your branch has diverged" + +```bash +# After rebasing +error: failed to push some refs +hint: Updates were rejected because the tip of your current branch is behind +``` + +**Solution:** Use `--force-with-lease` (safe) or `--force` (less safe) + +```bash +git push --force-with-lease origin feat/vault-fetcher-cli +``` + +### "Cannot rebase: You have unstaged changes" + +```bash +git status +# Uncommitted changes + +# Option 1: Commit them +git add . && git commit -m "wip" + +# Option 2: Stash them +git stash +git rebase feat/vault-integration +git stash pop +``` + +### PR Shows Wrong Diff + +**Problem:** PR #380 shows authlib changes (from Phase 1) + +**Cause:** Base branch is wrong + +**Solution:** +```bash +gh pr edit 380 --base feat/vault-integration +``` + +## Summary Commands + +### Quick Rebase Entire Stack + +```bash +# After changing feat/vault-integration +git checkout feat/vault-fetcher-cli && git rebase feat/vault-integration && git push --force-with-lease && \ +git checkout feat/vault-webhook && git rebase feat/vault-fetcher-cli && git push --force-with-lease && \ +git checkout feat/vault-docs-polish && git rebase feat/vault-webhook && git push --force-with-lease +``` + +### Check Stack Status + +```bash +# Show all PRs in stack +gh pr list --label "vault-pattern" + +# Show commit graph +git log --oneline --graph --all feat/vault-integration feat/vault-fetcher-cli feat/vault-webhook feat/vault-docs-polish +``` + +## References + +- [GitHub Stacked PRs Documentation](https://github.blog/changelog/2023-09-27-stacked-pull-requests/) +- [gh-stack Extension](https://github.com/github/gh-stack) +- [Git Rebase Documentation](https://git-scm.com/docs/git-rebase) + +--- + +**Remember:** When in doubt, **cascade rebase**. It's better to rebase too often than not enough! diff --git a/authbridge/vault-fetcher/WEBHOOK_INTEGRATION.md b/authbridge/vault-fetcher/WEBHOOK_INTEGRATION.md new file mode 100644 index 000000000..c4a31aa07 --- /dev/null +++ b/authbridge/vault-fetcher/WEBHOOK_INTEGRATION.md @@ -0,0 +1,456 @@ +# Webhook Integration Guide — vault-fetcher + +This document describes how to integrate vault-fetcher with the kagenti-operator webhook for automatic injection. + +## Overview + +The kagenti-operator webhook can automatically inject vault-fetcher as an init container when pods are labeled with `kagenti.io/vault-fetcher-inject: "true"`. + +## Architecture + +``` +┌─────────────────────────────────────────────────────────┐ +│ WORKLOAD POD │ +│ │ +│ INIT CONTAINERS (sequential): │ +│ ┌────────────────────────────────────────────┐ │ +│ │ 1. proxy-init (iptables setup) │ │ +│ └────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ 2. client-registration (Keycloak) │ │ +│ └────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ 3. vault-fetcher (NEW) │ │ +│ │ - Reads JWT-SVID from spiffe-helper │ │ +│ │ - Authenticates to Vault │ │ +│ │ - Fetches configured secrets │ │ +│ │ - Writes to /shared/secrets/* │ │ +│ │ - Exits │ │ +│ └────────────────────────────────────────────┘ │ +│ │ +│ SIDECARS (continuous): │ +│ ┌────────────────────────────────────────────┐ │ +│ │ spiffe-helper → writes JWT-SVID │ │ +│ └────────────────────────────────────────────┘ │ +│ ┌────────────────────────────────────────────┐ │ +│ │ authbridge (envoy + ext-proc) │ │ +│ │ - Validates inbound JWTs │ │ +│ │ - Exchanges tokens for outbound │ │ +│ └────────────────────────────────────────────┘ │ +│ │ +│ APPLICATION CONTAINER: │ +│ ┌────────────────────────────────────────────┐ │ +│ │ Your Agent/App │ │ +│ │ - Reads secrets from /shared/secrets/* │ │ +│ │ - Makes HTTP calls (AuthBridge handles) │ │ +│ └────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────┘ +``` + +## Webhook Implementation + +### Label-Based Injection + +**Pod label:** +```yaml +metadata: + labels: + kagenti.io/type: agent + kagenti.io/inject: "true" + kagenti.io/vault-fetcher-inject: "true" # Enable vault-fetcher +``` + +**Webhook behavior:** +- Checks for `kagenti.io/vault-fetcher-inject: "true"` label +- Injects vault-fetcher init container if label present +- Mounts required volumes and ConfigMaps + +### Required Resources + +**1. ConfigMap: `vault-fetcher-config`** +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-fetcher-config + namespace: +data: + config.yaml: | + vault: + address: "https://vault.vault.svc.cluster.local:8200" + auth_method: "jwt" + role: "" + secrets: + - path: "secret/data/github/token" + field: "token" + output: "/shared/secrets/github-token" +``` + +**Must exist before pod creation.** Webhook does not create this. + +### Webhook Injection Logic + +**File:** `kagenti-operator/pkg/webhook/mutator.go` (example) + +```go +func (m *Mutator) injectVaultFetcher(pod *corev1.Pod) error { + // Check label + if pod.Labels["kagenti.io/vault-fetcher-inject"] != "true" { + return nil + } + + // Add init container + initContainer := corev1.Container{ + Name: "vault-fetcher", + Image: "ghcr.io/kagenti/kagenti-extensions/vault-fetcher:latest", + Args: []string{"--config=/etc/vault-fetcher/config.yaml"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "spiffe-svid", + MountPath: "/opt", + ReadOnly: true, + }, + { + Name: "shared-secrets", + MountPath: "/shared/secrets", + }, + { + Name: "vault-fetcher-config", + MountPath: "/etc/vault-fetcher", + ReadOnly: true, + }, + }, + SecurityContext: &corev1.SecurityContext{ + RunAsUser: pointer.Int64(65532), + RunAsGroup: pointer.Int64(65532), + RunAsNonRoot: pointer.Bool(true), + AllowPrivilegeEscalation: pointer.Bool(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + SeccompProfile: &corev1.SeccompProfile{ + Type: corev1.SeccompProfileTypeRuntimeDefault, + }, + }, + } + + // Insert after client-registration, before app container + pod.Spec.InitContainers = append(pod.Spec.InitContainers, initContainer) + + // Add volumes + pod.Spec.Volumes = append(pod.Spec.Volumes, + corev1.Volume{ + Name: "shared-secrets", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, + corev1.Volume{ + Name: "vault-fetcher-config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "vault-fetcher-config", + }, + }, + }, + }, + ) + + // Mount shared-secrets in app container + for i := range pod.Spec.Containers { + pod.Spec.Containers[i].VolumeMounts = append( + pod.Spec.Containers[i].VolumeMounts, + corev1.VolumeMount{ + Name: "shared-secrets", + MountPath: "/shared/secrets", + ReadOnly: true, + }, + ) + } + + return nil +} +``` + +### Injection Order + +**Critical:** Init containers run sequentially. Order matters! + +1. **proxy-init** — Sets up iptables (runs first) +2. **client-registration** — Registers with Keycloak +3. **vault-fetcher** — Fetches secrets (after client-registration completes) +4. Application container starts (all init containers done) + +**Why this order?** +- vault-fetcher needs JWT-SVID from spiffe-helper (running as sidecar) +- spiffe-helper starts with other sidecars, writes JWT-SVID early +- vault-fetcher reads JWT-SVID when it starts + +### Configuration Examples + +#### Minimal (GitHub token only) + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-fetcher-config +data: + config.yaml: | + vault: + address: "https://vault.vault.svc:8200" + auth_method: "jwt" + role: "github-agent" + secrets: + - path: "secret/data/github/token" + field: "token" + output: "/shared/secrets/github-token" +``` + +#### Multiple secrets + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-fetcher-config +data: + config.yaml: | + vault: + address: "https://vault.vault.svc:8200" + auth_method: "jwt" + role: "data-agent" + secrets: + - path: "secret/data/github/token" + field: "token" + output: "/shared/secrets/github-token" + - path: "secret/data/slack/webhook" + field: "url" + output: "/shared/secrets/slack-webhook" + - path: "secret/data/database/postgres" + field: "password" + output: "/shared/secrets/db-password" + output_formats: + env_file: + enabled: true + path: "/shared/secrets/.env" +``` + +#### With Kubernetes SA auth (no SPIFFE) + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-fetcher-config +data: + config.yaml: | + vault: + address: "https://vault.vault.svc:8200" + auth_method: "kubernetes" # Use K8s SA instead of JWT-SVID + role: "my-app-role" + secrets: + - path: "secret/data/api-key" + field: "key" + output: "/shared/secrets/api-key" +``` + +## Vault Setup + +### JWT Auth Configuration + +```bash +# Enable JWT auth method +vault auth enable jwt + +# Configure with SPIRE OIDC discovery URL +vault write auth/jwt/config \ + oidc_discovery_url="http://spire-server.spire.svc.cluster.local:8081" \ + default_role="default-agent" + +# Create role for GitHub agents +vault write auth/jwt/role/github-agent \ + role_type="jwt" \ + bound_audiences="vault" \ + bound_claims_type="glob" \ + bound_claims='{"sub":"spiffe://localtest.me/ns/*/sa/github-agent/*"}' \ + user_claim="sub" \ + policies="github-agent" \ + ttl="1h" + +# Create policy +vault policy write github-agent - < -c vault-fetcher + +# Common issues: +# 1. ConfigMap not found +kubectl get cm vault-fetcher-config -n + +# 2. JWT-SVID not ready +kubectl logs -c spiffe-helper + +# 3. Vault auth fails +kubectl exec -c vault-fetcher -- cat /opt/jwt_svid.token +# Verify SPIFFE ID matches Vault role's bound_claims + +# 4. Secret not found in Vault +vault kv get secret/github/token +``` + +### ConfigMap not mounted + +```bash +# Verify ConfigMap exists +kubectl get cm vault-fetcher-config -n -o yaml + +# Check pod spec +kubectl get pod -o yaml | grep -A 10 vault-fetcher-config +``` + +### Secrets not accessible by app + +```bash +# Verify secrets were written +kubectl exec -- ls -la /shared/secrets + +# Check file permissions +kubectl exec -- stat /shared/secrets/github-token + +# Verify volume mount in app container +kubectl get pod -o yaml | grep -A 5 "name: shared-secrets" +``` + +## Migration Guide + +### From Manual ConfigMap to Webhook + +**Before (manual):** +```yaml +spec: + initContainers: + - name: vault-fetcher + image: ghcr.io/kagenti/kagenti-extensions/vault-fetcher:latest + # ... full config ... +``` + +**After (webhook):** +```yaml +metadata: + labels: + kagenti.io/vault-fetcher-inject: "true" +spec: + # Webhook injects everything automatically +``` + +### From Hardcoded Secrets to Vault + +**Before:** +```yaml +env: +- name: GITHUB_TOKEN + value: "ghp_xxxxx" # ❌ Hardcoded +``` + +**After:** +```yaml +env: +- name: GITHUB_TOKEN_FILE + value: "/shared/secrets/github-token" +``` + +**App code:** +```python +# Before +token = os.environ["GITHUB_TOKEN"] + +# After +with open(os.environ["GITHUB_TOKEN_FILE"]) as f: + token = f.read().strip() +``` + +## Security Considerations + +1. **ConfigMap permissions:** Restrict who can edit `vault-fetcher-config` +2. **Vault policies:** Grant minimal permissions (only secrets needed) +3. **File permissions:** vault-fetcher uses 0600 by default +4. **SPIFFE ID validation:** Vault roles should use specific SPIFFE ID patterns +5. **Namespace isolation:** Each namespace has its own vault-fetcher-config + +## Future Enhancements + +- [ ] Sidecar mode for secret rotation +- [ ] Multiple ConfigMaps (default + pod-specific) +- [ ] Secret templating in ConfigMap +- [ ] Webhook validation (fail fast if ConfigMap missing) +- [ ] Metrics endpoint for monitoring + +## Related Documentation + +- [vault-fetcher README](../README.md) — CLI usage +- [authlib/vault README](../../authlib/vault/README.md) — Library reference +- [VAULT_PATTERN_OVERVIEW.md](../../../VAULT_PATTERN_OVERVIEW.md) — Architecture diff --git a/authbridge/vault-fetcher/k8s/configmap-vault-fetcher.yaml b/authbridge/vault-fetcher/k8s/configmap-vault-fetcher.yaml new file mode 100644 index 000000000..96a9e9552 --- /dev/null +++ b/authbridge/vault-fetcher/k8s/configmap-vault-fetcher.yaml @@ -0,0 +1,48 @@ +# Example ConfigMap for vault-fetcher configuration +# This should be created in the same namespace as the workload +apiVersion: v1 +kind: ConfigMap +metadata: + name: vault-fetcher-config + namespace: default # Change to your namespace +data: + config.yaml: | + vault: + # Vault server address + address: "https://vault.vault.svc.cluster.local:8200" + + # Authentication method: jwt (SPIFFE), kubernetes, or token + auth_method: "jwt" + + # Vault role name + role: "github-agent-role" + + # JWT-SVID path (written by spiffe-helper) + jwt_path: "/opt/jwt_svid.token" + + # JWT audience + jwt_audience: "vault" + + # Cache TTL + cache_ttl: "5m" + + # Secrets to fetch + secrets: + - path: "secret/data/github/token" + field: "token" + output: "/shared/secrets/github-token" + mode: "0600" + + - path: "secret/data/slack/webhook" + field: "url" + output: "/shared/secrets/slack-webhook" + mode: "0600" + + # Optional: Additional output formats + output_formats: + env_file: + enabled: false + path: "/shared/secrets/.env" + json_file: + enabled: false + path: "/shared/secrets/credentials.json" diff --git a/authbridge/vault-fetcher/k8s/example-deployment.yaml b/authbridge/vault-fetcher/k8s/example-deployment.yaml new file mode 100644 index 000000000..edcdd3a2f --- /dev/null +++ b/authbridge/vault-fetcher/k8s/example-deployment.yaml @@ -0,0 +1,73 @@ +# Example deployment with vault-fetcher init container +# This shows manual injection (before webhook integration) +apiVersion: v1 +kind: Pod +metadata: + name: github-agent + namespace: default + labels: + app: github-agent +spec: + serviceAccountName: github-agent + + # Init containers run before application container + initContainers: + # 1. vault-fetcher: Fetch secrets from Vault + - name: vault-fetcher + image: ghcr.io/kagenti/kagenti-extensions/vault-fetcher:latest + args: + - --config=/etc/vault-fetcher/config.yaml + volumeMounts: + - name: spiffe-svid + mountPath: /opt + readOnly: true + - name: shared-secrets + mountPath: /shared/secrets + - name: vault-fetcher-config + mountPath: /etc/vault-fetcher + readOnly: true + securityContext: + runAsUser: 65532 + runAsGroup: 65532 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + # Application container + containers: + - name: agent + image: my-agent:latest + volumeMounts: + - name: shared-secrets + mountPath: /shared/secrets + readOnly: true + env: + # Application can read secrets from files + - name: GITHUB_TOKEN_FILE + value: "/shared/secrets/github-token" + - name: SLACK_WEBHOOK_FILE + value: "/shared/secrets/slack-webhook" + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault + + # Volumes + volumes: + - name: spiffe-svid + emptyDir: {} + - name: shared-secrets + emptyDir: {} + - name: vault-fetcher-config + configMap: + name: vault-fetcher-config