feat(k8s-portal): update onboarding + architecture with SOPS state docs

Onboarding (namespace-owner):
- Add steps for sops/terragrunt install, state decrypt, apply workflow
- Add flow diagram showing auth → decrypt → apply → encrypt → push
- Add architecture overview with security model table
- Add access control callout explaining per-stack Transit keys

Architecture:
- Add secrets & state encryption section with ASCII diagrams
- Add request flow diagram (Cloudflare → Traefik → pods)
- Add CI/CD pipeline diagram (GHA → Woodpecker → K8s)

[ci skip]
This commit is contained in:
Viktor Barzin 2026-03-17 23:17:47 +00:00
parent ccbcebb670
commit 0fff155f17
2 changed files with 226 additions and 18 deletions

View file

@ -59,6 +59,74 @@ Proxmox (Dell R730)
└── ... (70+ more)</pre>
<p>Changes go through git: branch → PR → review → merge → CI applies automatically.</p>
</section>
<section>
<h2>Secrets &amp; State Encryption</h2>
<p>Terraform state is committed to git as SOPS-encrypted JSON. Secrets live in HashiCorp Vault.</p>
<pre class="output">
Authentication &amp; Authorization
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
User → Authentik SSO (OIDC) → Vault Token
┌──────────────┼──────────────┐
▼ ▼ ▼
KV Secrets Transit Keys K8s Creds
(per-stack) (per-stack) (deployer)
State Encryption Flow
━━━━━━━━━━━━━━━━━━━━
.tfstate ──SOPS──▶ .tfstate.enc ──git──▶ repo
encrypted with:
├── Vault Transit key (per-stack)
└── age keys (admin DR fallback)
Access Control
━━━━━━━━━━━━━━
Admin: vault-admin policy → all transit keys
User: sops-user-* policy → own stack keys only
Fallback: age key on disk → admin only (no users)</pre>
</section>
<section>
<h2>Request Flow</h2>
<pre class="output">
Internet → Cloudflare (CDN + WAF)
Cloudflared tunnel
Traefik (3 replicas)
├── CrowdSec bouncer (rate limit, bot block)
├── Authentik forward-auth (SSO for protected apps)
└── TLS termination
K8s Service → Pod(s)
├── NFS volume (app data)
└── iSCSI volume (databases)</pre>
</section>
<section>
<h2>CI/CD Pipeline</h2>
<pre class="output">
git push
├──▶ GitHub Actions (build Docker image, push to DockerHub)
│ │
│ ▼
│ POST Woodpecker API (trigger deploy)
│ │
│ ▼
│ Woodpecker (kubectl set image, Slack notify)
└──▶ Woodpecker (terragrunt apply — infra changes)
Kubernetes API → rolling update</pre>
</section>
</main>
<style>

View file

@ -64,11 +64,75 @@
<h2>Step 5 — Clone the infra repo</h2>
<pre>git clone https://github.com/ViktorBarzin/infra.git
cd infra</pre>
<p>This is where all the infrastructure configuration lives.</p>
<p>This is where all the infrastructure configuration lives. Terraform state is committed as encrypted files.</p>
</section>
<section>
<h2>Step 6 — Create your first app stack</h2>
<h2>Step 6 — Install tools</h2>
<p>You need <code>sops</code> and <code>terragrunt</code> to work with infrastructure state:</p>
<h3>macOS</h3>
<pre>brew install sops terragrunt</pre>
<h3>Linux</h3>
<pre># sops
curl -LO https://github.com/getsops/sops/releases/latest/download/sops-v3.9.4.linux.amd64
sudo mv sops-*.linux.amd64 /usr/local/bin/sops && sudo chmod +x /usr/local/bin/sops
# terragrunt
curl -LO https://github.com/gruntwork-io/terragrunt/releases/latest/download/terragrunt_linux_amd64
sudo mv terragrunt_linux_amd64 /usr/local/bin/terragrunt && sudo chmod +x /usr/local/bin/terragrunt</pre>
</section>
<section>
<h2>Step 7 — Decrypt your state</h2>
<p>Terraform state is encrypted with SOPS. Your Vault login gives you access to <strong>only your stacks</strong>.</p>
<pre># Make sure you're logged into Vault
vault login -method=oidc
# Decrypt your stack's state
scripts/state-sync decrypt YOUR_NAMESPACE
# Plan changes (auto-decrypts before, auto-encrypts after)
cd stacks/YOUR_NAMESPACE
../../scripts/tg plan</pre>
<div class="diagram">
<h3>How state encryption works</h3>
<div class="flow-diagram">
<div class="flow-row">
<div class="flow-box accent">vault login -method=oidc</div>
<div class="flow-arrow"></div>
<div class="flow-box">Authentik SSO</div>
<div class="flow-arrow"></div>
<div class="flow-box accent">~/.vault-token</div>
</div>
<div class="flow-separator"></div>
<div class="flow-row">
<div class="flow-box accent">scripts/tg plan</div>
<div class="flow-arrow"></div>
<div class="flow-box">state-sync decrypt</div>
<div class="flow-arrow"></div>
<div class="flow-box">Vault Transit<br/><small>sops-state-YOUR_NS</small></div>
</div>
<div class="flow-separator"></div>
<div class="flow-row">
<div class="flow-box">terragrunt plan/apply</div>
<div class="flow-arrow"></div>
<div class="flow-box">state-sync encrypt</div>
<div class="flow-arrow"></div>
<div class="flow-box accent">git commit + push</div>
</div>
</div>
</div>
<div class="callout info">
<strong>Access control:</strong> You can only decrypt state for your own namespaces.
Each namespace has its own Vault Transit encryption key. Your Vault policy
(<code>sops-user-YOUR_USERNAME</code>) only grants access to your keys.
</div>
</section>
<section>
<h2>Step 8 — Create your first app stack</h2>
<ol>
<li>Copy the template: <pre>cp -r stacks/_template stacks/myapp
mv stacks/myapp/main.tf.example stacks/myapp/main.tf</pre></li>
@ -76,17 +140,68 @@ mv stacks/myapp/main.tf.example stacks/myapp/main.tf</pre></li>
<li>Store secrets in Vault:
<pre>vault kv put secret/YOUR_USERNAME/myapp DB_PASSWORD=secret123</pre>
</li>
<li>Add your app domain to <code>domains</code> list in Vault KV <code>k8s_users</code></li>
<li>Submit a PR:
<pre>git checkout -b feat/myapp
git add stacks/myapp/
git commit -m "add myapp stack"
git push -u origin feat/myapp</pre>
<li>Apply your stack:
<pre>cd stacks/myapp && ../../scripts/tg apply</pre>
</li>
<li>Commit encrypted state:
<pre>cd ../..
git add stacks/myapp/ state/stacks/myapp/terraform.tfstate.enc
git commit -m "add myapp stack"
git push</pre>
</li>
<li>Viktor reviews and merges</li>
<li>After merge: <code>cd stacks/myapp && terragrunt apply</code></li>
</ol>
</section>
<section>
<h2>Architecture Overview</h2>
<p>Here's how your changes flow through the system:</p>
<div class="diagram">
<h3>Apply workflow</h3>
<div class="arch-grid">
<div class="arch-col">
<div class="arch-header">Your Machine</div>
<div class="arch-box">git pull</div>
<div class="arch-arrow"></div>
<div class="arch-box">scripts/tg plan</div>
<div class="arch-arrow"><small>auto-decrypt</small></div>
<div class="arch-box">scripts/tg apply</div>
<div class="arch-arrow"><small>auto-encrypt</small></div>
<div class="arch-box">git push</div>
</div>
<div class="arch-col">
<div class="arch-header">Vault</div>
<div class="arch-box small">OIDC auth<br/><small>Authentik SSO</small></div>
<div class="arch-arrow"></div>
<div class="arch-box small">Transit decrypt<br/><small>sops-state-*</small></div>
<div class="arch-arrow"></div>
<div class="arch-box small">Transit encrypt<br/><small>per-stack key</small></div>
</div>
<div class="arch-col">
<div class="arch-header">Cluster</div>
<div class="arch-box small">K8s API</div>
<div class="arch-arrow"></div>
<div class="arch-box small">Your namespace<br/><small>pods, services</small></div>
<div class="arch-arrow"></div>
<div class="arch-box small">Traefik ingress<br/><small>*.viktorbarzin.me</small></div>
</div>
</div>
</div>
<div class="diagram">
<h3>Security model</h3>
<table>
<thead><tr><th>Layer</th><th>What</th><th>How</th></tr></thead>
<tbody>
<tr><td>Authentication</td><td>Who are you?</td><td>Authentik SSO (OIDC) → Vault token</td></tr>
<tr><td>Authorization</td><td>What can you access?</td><td>Vault policy (<code>sops-user-*</code>) scoped to your namespaces</td></tr>
<tr><td>Encryption at rest</td><td>State in git</td><td>SOPS + Vault Transit (per-stack key)</td></tr>
<tr><td>Encryption fallback</td><td>Bootstrap / DR</td><td>age keys (admin only)</td></tr>
<tr><td>Network</td><td>Cluster access</td><td>Headscale VPN (private 10.0.20.0/24)</td></tr>
</tbody>
</table>
</div>
</section>
{:else}
<section>
<h2>Step 3 — Verify access</h2>
@ -109,14 +224,7 @@ cd infra</pre>
</section>
<section>
<h2>Step 5 — Install your AI assistant (optional)</h2>
<p>Install <a href="https://github.com/openai/codex" target="_blank">Codex CLI</a> for AI-assisted cluster management:</p>
<pre>npm install -g @openai/codex</pre>
<p>Codex reads the <code>AGENTS.md</code> file in the repo and knows how to work with the cluster.</p>
</section>
<section>
<h2>Step 6 — Your first change</h2>
<h2>Step 5 — Your first change</h2>
<ol>
<li>Create a branch: <pre>git checkout -b my-first-change</pre></li>
<li>Edit a service file (e.g., change an image tag in <code>stacks/echo/main.tf</code>)</li>
@ -143,4 +251,36 @@ cd infra</pre>
.role-tabs { display: flex; gap: 0; margin: 1.5rem 0; border-bottom: 2px solid #e0e0e0; }
.role-tabs a { padding: 0.5rem 1.5rem; text-decoration: none; color: #666; border-bottom: 2px solid transparent; margin-bottom: -2px; }
.role-tabs a.active { color: #333; border-bottom-color: #333; font-weight: 600; }
table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
th, td { border: 1px solid #ddd; padding: 0.5rem; text-align: left; }
th { background: #f5f5f5; }
.callout { padding: 1rem; border-radius: 6px; margin: 1rem 0; }
.callout.info { background: #e8f4fd; border-left: 4px solid #2196f3; }
.callout.warning { background: #fff3cd; border-left: 4px solid #ffc107; }
.diagram { background: #fafafa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 1.5rem; margin: 1.5rem 0; }
.diagram h3 { margin: 0 0 1rem 0; color: #333; font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.5px; }
.flow-diagram { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
.flow-row { display: flex; align-items: center; gap: 0.5rem; flex-wrap: wrap; justify-content: center; }
.flow-box { background: white; border: 2px solid #ddd; border-radius: 6px; padding: 0.5rem 1rem; font-family: monospace; font-size: 0.85rem; text-align: center; }
.flow-box.accent { border-color: #333; font-weight: 600; }
.flow-box small { display: block; color: #888; font-weight: normal; }
.flow-arrow { color: #999; font-size: 1.2rem; }
.flow-separator { color: #999; font-size: 1.2rem; }
.arch-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; }
.arch-col { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
.arch-header { font-weight: 700; font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.5px; color: #555; margin-bottom: 0.5rem; padding: 0.25rem 0.75rem; background: #e8e8e8; border-radius: 4px; }
.arch-box { background: white; border: 2px solid #ddd; border-radius: 6px; padding: 0.5rem 0.75rem; font-family: monospace; font-size: 0.8rem; text-align: center; width: 100%; box-sizing: border-box; }
.arch-box.small { font-size: 0.75rem; }
.arch-box small { display: block; color: #888; font-family: monospace; }
.arch-arrow { color: #999; font-size: 1rem; }
@media (max-width: 600px) {
.arch-grid { grid-template-columns: 1fr; }
.flow-row { flex-direction: column; }
.flow-arrow { transform: rotate(90deg); }
}
</style>