Compare commits
107
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2071da9d8 | ||
|
|
2485b5f165 | ||
|
|
056964075e | ||
|
|
a1bb64dd98 | ||
|
|
438fcbe9fe | ||
|
|
afec7e9867 | ||
|
|
fc63acbcc9 | ||
|
|
ac73035c9e | ||
|
|
12c66fb612 | ||
|
|
50a751de44 | ||
|
|
863c11d75f | ||
|
|
a525a52580 | ||
|
|
3e92f34153 | ||
|
|
549e0be502 | ||
|
|
03cebb0aeb | ||
|
|
cc7c30ee20 | ||
|
|
6768d1c762 | ||
|
|
80af9b1fd0 | ||
|
|
7f29d640fd | ||
|
|
9c3b2bfbbe | ||
|
|
81e6e9bc03 | ||
|
|
aa1f224349 | ||
|
|
37933de756 | ||
|
|
468ed16737 | ||
|
|
ab9c2b444b | ||
|
|
b6e1e0c591 | ||
|
|
701d79deac | ||
|
|
fbc1b6a33d | ||
|
|
3f93ddc002 | ||
|
|
eaf9804374 | ||
|
|
903a72a6fd | ||
|
|
81883a8ea1 | ||
|
|
7f96da880f | ||
|
|
f37e5f32cb | ||
|
|
8c4a900b40 | ||
|
|
e6ba83cd38 | ||
|
|
a02bee50e2 | ||
|
|
f9af686c36 | ||
|
|
3ec46d26f0 | ||
|
|
52bca6b1b6 | ||
|
|
3e079b1cf7 | ||
|
|
227a67a8c1 | ||
|
|
d4e9dbf350 | ||
|
|
be44a576d4 | ||
|
|
39081222a3 | ||
|
|
7e5b793670 | ||
|
|
354814a531 | ||
|
|
54fac02783 | ||
|
|
eb83aa6290 | ||
|
|
6ebecec1c4 | ||
|
|
091ecdd7bb | ||
|
|
7251032081 | ||
|
|
b7fc6ed60b | ||
|
|
6312674058 | ||
|
|
2fe6d50ffd | ||
|
|
e2d9717ce1 | ||
|
|
111b19dac2 | ||
|
|
e335694cbb | ||
|
|
d1a7d3be6e | ||
|
|
2f664c32d9 | ||
|
|
6c98ca7436 | ||
|
|
6ee22401fd | ||
|
|
e9817283e6 | ||
|
|
6e9586aa15 | ||
|
|
979a413a5b | ||
|
|
2f62351731 | ||
|
|
f81ff0e2fc | ||
|
|
cb1219bf96 | ||
|
|
8e9ac37583 | ||
|
|
3c851b16c9 | ||
|
|
e93d4ba86c | ||
|
|
7a1213d504 | ||
|
|
d21ee9630f | ||
|
|
40aa23dfdf | ||
|
|
71fce30506 | ||
|
|
0666715a5e | ||
|
|
949ae5cb38 | ||
|
|
e150969070 | ||
|
|
abfdda5f57 | ||
|
|
e4caa7e841 | ||
|
|
1ce9f86635 | ||
|
|
2d9e3abee8 | ||
|
|
9fa8d9cf60 | ||
|
|
2e4fad3bf7 | ||
|
|
5d22691c3e | ||
|
|
4eee161e0f | ||
|
|
719fa77521 | ||
|
|
ebb1b9b02d | ||
|
|
cc62f74404 | ||
|
|
64cd1f2c4e | ||
|
|
d7b96fb29b | ||
|
|
71b3cef2eb | ||
|
|
f780e31f9c | ||
|
|
c93a24f786 | ||
|
|
b6e317b34c | ||
|
|
f3015522e3 | ||
|
|
d60b520968 | ||
|
|
c7d52b5971 | ||
|
|
3c3b243790 | ||
|
|
420b5d6da6 | ||
|
|
1f3439c9e0 | ||
|
|
6ae36981bb | ||
|
|
e6fd6f2b62 | ||
|
|
c83d456ecd | ||
|
|
8bd40c5152 | ||
|
|
c472a994ce | ||
|
|
bb24418a2e |
@@ -31,6 +31,17 @@ AUTHENTIK_BOOTSTRAP_TOKEN=
|
||||
# openssl rand -base64 24
|
||||
AUTHENTIK_PG_PASSWORD=
|
||||
|
||||
# ── ZITADEL (Authentik replacement) ─────────────────────────────────────────────
|
||||
# Instance URL, same hostname as the OIDC issuer.
|
||||
ZITADEL_URL="https://zitadel.riotpiao.com"
|
||||
|
||||
# PAT of the 'iam-admin' machine user, created by the chart's setup job on the
|
||||
# first sync. Leave empty until then. Read it with:
|
||||
# kubectl -n iam get secret iam-admin-pat -o jsonpath='{.data.pat}' | base64 -d
|
||||
# scripts/iam/zitadel-provision.py reads this variable first, then the cluster
|
||||
# Secret. The masterkey and the client secrets never live in this file.
|
||||
ZITADEL_BOOTSTRAP_TOKEN=
|
||||
|
||||
# ── MinIO ──────────────────────────────────────────────────────────────────────
|
||||
MINIO_ROOT_USER=
|
||||
MINIO_ROOT_PASSWORD=
|
||||
|
||||
+20
@@ -47,6 +47,15 @@ k8s/storage/test/test
|
||||
terraform/.terraform/
|
||||
terraform/*.tfstate
|
||||
terraform/*.tfstate.*
|
||||
# The Zitadel root module keeps its own state (see terraform/zitadel/providers.tf).
|
||||
# That state carries generated client secrets and machine keys.
|
||||
terraform/zitadel/*.tfstate
|
||||
terraform/zitadel/*.tfstate.*
|
||||
terraform/zitadel/*.tfvars
|
||||
terraform/edge/*.tfstate
|
||||
terraform/edge/*.tfstate.*
|
||||
terraform/edge/*.tfvars
|
||||
terraform/edge/terraform.tfvars.local
|
||||
terraform.tfvars.local
|
||||
skills-lock.json
|
||||
secrets-plaintext.yaml
|
||||
@@ -69,3 +78,14 @@ k8s/**/*-secret.yaml
|
||||
|
||||
# IAM provisioning scripts contain credential references — never commit
|
||||
scripts/iam/*.py
|
||||
# Exception: this one reads every credential from the cluster at run time and
|
||||
# embeds none. Keep it reviewable in git.
|
||||
!scripts/iam/zitadel-provision.py
|
||||
|
||||
# Runtime credential export from the old Authentik provisioning run. Holds live
|
||||
# M2M tokens; never commit it.
|
||||
/authentik-m2m-tokens.env
|
||||
|
||||
# Never commit backup copies of plaintext tfvars (they carry cluster PKI)
|
||||
terraform/*.bak
|
||||
terraform/edge/.secrets/
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"nextId": 2,
|
||||
"loops": [
|
||||
{
|
||||
"id": "1",
|
||||
"prompt": "Check Longhorn and CNPG recovery progress in the homelab cluster and report terse. Run: kubectl -n longhorn-system get volumes.longhorn.io -o json (count by state/robustness), kubectl -n longhorn-system get replicas.longhorn.io -o json (count running/stopped/failed), kubectl get clusters.postgresql.cnpg.io -A (ready instances per cluster), kubectl -n cicd get pods,pvc | grep forgejo-db. Report: volumes healthy vs degraded, replicas running vs stopped, and each CNPG cluster ready count. If every volume is attached/healthy and every CNPG cluster shows 3/3 ready, say RECOVERED, list any leftover anomalies, and stop this loop with LoopDelete. Otherwise name what is still rebuilding and keep going.",
|
||||
"trigger": {
|
||||
"type": "cron",
|
||||
"schedule": "*/5 * * * *"
|
||||
},
|
||||
"status": "expired",
|
||||
"recurring": true,
|
||||
"createdAt": 1792898370946,
|
||||
"updatedAt": 1792901371403,
|
||||
"expiresAt": 1793503170946,
|
||||
"source": "tool",
|
||||
"readOnly": true,
|
||||
"maxFires": 6,
|
||||
"fireCount": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "architecture",
|
||||
"meta": {
|
||||
"title": "Poimen Memory — Schema Relations",
|
||||
"quality_profile": "showcase",
|
||||
"viewBox": [900, 480]
|
||||
},
|
||||
"components": [
|
||||
{ "id": "node", "type": "database", "label": "memory_node", "sublabel": "Episodes (raw messages)", "pos": [40, 60], "size": [150, 55], "tag": "BIGSERIAL" },
|
||||
{ "id": "entity", "type": "database", "label": "memory_entity", "sublabel": "Extracted entities", "pos": [260, 60], "size": [150, 55], "tag": "UUID" },
|
||||
{ "id": "edge", "type": "database", "label": "memory_edge", "sublabel": "Facts / relationships", "pos": [480, 60], "size": [150, 55], "tag": "UUID" },
|
||||
{ "id": "community", "type": "database", "label": "memory_community", "sublabel": "Entity clusters", "pos": [700, 60], "size": [150, 55], "tag": "UUID" },
|
||||
{ "id": "pgvector", "type": "cloud", "label": "pgvector HNSW", "sublabel": "768-dim cosine search", "pos": [140, 260], "size": [160, 55], "tag": "nomic-embed-v2" },
|
||||
{ "id": "tsvector", "type": "cloud", "label": "pg ts_rank", "sublabel": "Full-text search", "pos": [400, 260], "size": [160, 55], "tag": "GIN index" },
|
||||
{ "id": "tenant", "type": "security", "label": "tenant_id", "sublabel": "Multi-tenant isolation", "pos": [660, 260], "size": [140, 55], "tag": "W7 / mig 014" }
|
||||
],
|
||||
"boundaries": [
|
||||
{ "kind": "region", "label": "Temporal Knowledge Graph (Zep §2)", "wraps": ["node", "entity", "edge", "community"] },
|
||||
{ "kind": "security-group", "label": "Search & Isolation", "wraps": ["pgvector", "tsvector", "tenant"] }
|
||||
],
|
||||
"connections": [
|
||||
{ "id": "node-entity", "from": "node", "to": "entity", "variant": "emphasis" },
|
||||
{ "id": "entity-edge", "from": "entity", "to": "edge", "variant": "emphasis" },
|
||||
{ "id": "entity-community", "from": "community", "to": "entity", "variant": "dashed" },
|
||||
{ "id": "edge-node", "from": "edge", "to": "node", "variant": "dashed" },
|
||||
{ "id": "entity-vec", "from": "entity", "to": "pgvector", "variant": "default" },
|
||||
{ "id": "entity-fts", "from": "entity", "to": "tsvector", "variant": "dashed" },
|
||||
{ "id": "edge-fts", "from": "edge", "to": "tsvector", "variant": "dashed" },
|
||||
{ "id": "tenant-all", "from": "tenant", "to": "entity", "variant": "security" }
|
||||
],
|
||||
"cards": [
|
||||
{ "dot": "emerald", "title": "Three-Tier Graph", "items": ["memory_node → episode subgraph", "memory_entity → entity subgraph", "memory_edge → fact relationships", "memory_community → clusters (Phase 4)"] },
|
||||
{ "dot": "rose", "title": "Bi-Temporal", "items": ["t_valid / t_invalid: event time", "t_created / t_expired: transaction time", "Contradiction: active → candidate → confirmed"] },
|
||||
{ "dot": "cyan", "title": "Search", "items": ["HNSW: name, summary, fact embeddings", "ts_rank: BM25 fallback (always available)", "tenant_id: per-table isolation (mig 014)"] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "dataflow",
|
||||
"meta": {
|
||||
"title": "Poimen Memory \u2014 Schema Relations",
|
||||
"viewBox": [
|
||||
960,
|
||||
480
|
||||
],
|
||||
"quality_profile": "showcase"
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"label": "Episodes"
|
||||
},
|
||||
{
|
||||
"label": "Entities"
|
||||
},
|
||||
{
|
||||
"label": "Relationships"
|
||||
},
|
||||
{
|
||||
"label": "Indexes"
|
||||
}
|
||||
],
|
||||
"nodes": [
|
||||
{
|
||||
"id": "node",
|
||||
"type": "database",
|
||||
"label": "memory_node",
|
||||
"sublabel": "Raw messages",
|
||||
"stage": 0,
|
||||
"row": 0,
|
||||
"tag": "BIGSERIAL"
|
||||
},
|
||||
{
|
||||
"id": "entity",
|
||||
"type": "database",
|
||||
"label": "memory_entity",
|
||||
"sublabel": "Named entities",
|
||||
"stage": 1,
|
||||
"row": 0,
|
||||
"tag": "UUID + tenant_id"
|
||||
},
|
||||
{
|
||||
"id": "community",
|
||||
"type": "database",
|
||||
"label": "memory_community",
|
||||
"sublabel": "Clusters (Phase 4)",
|
||||
"stage": 1,
|
||||
"row": 1,
|
||||
"tag": "UUID"
|
||||
},
|
||||
{
|
||||
"id": "edge",
|
||||
"type": "database",
|
||||
"label": "memory_edge",
|
||||
"sublabel": "Facts + bi-temporal",
|
||||
"stage": 2,
|
||||
"row": 0,
|
||||
"tag": "UUID + tenant_id"
|
||||
},
|
||||
{
|
||||
"id": "hnsw",
|
||||
"type": "cloud",
|
||||
"label": "HNSW Index",
|
||||
"sublabel": "768-dim cosine",
|
||||
"stage": 3,
|
||||
"row": 0,
|
||||
"tag": "pgvector"
|
||||
},
|
||||
{
|
||||
"id": "fts",
|
||||
"type": "cloud",
|
||||
"label": "GIN Index",
|
||||
"sublabel": "ts_rank BM25",
|
||||
"stage": 3,
|
||||
"row": 1,
|
||||
"tag": "full-text"
|
||||
}
|
||||
],
|
||||
"flows": [
|
||||
{
|
||||
"from": "node",
|
||||
"to": "entity",
|
||||
"label": "LLM extraction",
|
||||
"classification": "ingest",
|
||||
"variant": "emphasis",
|
||||
"route": "straight"
|
||||
},
|
||||
{
|
||||
"from": "entity",
|
||||
"to": "edge",
|
||||
"label": "source + target FK",
|
||||
"classification": "foreign key",
|
||||
"variant": "emphasis",
|
||||
"route": "straight"
|
||||
},
|
||||
{
|
||||
"from": "entity",
|
||||
"to": "community",
|
||||
"label": "community_id",
|
||||
"classification": "membership",
|
||||
"variant": "dashed",
|
||||
"route": "straight",
|
||||
"labelDy": 30
|
||||
},
|
||||
{
|
||||
"from": "edge",
|
||||
"to": "hnsw",
|
||||
"classification": "vector",
|
||||
"variant": "default",
|
||||
"route": "straight",
|
||||
"label": "embedding",
|
||||
"labelDy": -30
|
||||
},
|
||||
{
|
||||
"from": "edge",
|
||||
"to": "fts",
|
||||
"label": "fact text",
|
||||
"classification": "text",
|
||||
"variant": "dashed",
|
||||
"route": "vertical-channel",
|
||||
"labelDy": 30
|
||||
}
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "Three-Tier Graph",
|
||||
"items": [
|
||||
"memory_node \u2192 episodes (raw messages)",
|
||||
"memory_entity \u2192 extracted entities",
|
||||
"memory_edge \u2192 facts between entities",
|
||||
"memory_community \u2192 clusters (Phase 4)"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "rose",
|
||||
"title": "Bi-Temporal + Tenant",
|
||||
"items": [
|
||||
"t_valid/t_invalid: when true in reality",
|
||||
"t_created/t_expired: system transaction time",
|
||||
"tenant_id: row-level multi-tenant isolation",
|
||||
"Contradiction: active \u2192 candidate \u2192 confirmed"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "cyan",
|
||||
"title": "Search Indexes",
|
||||
"items": [
|
||||
"HNSW: name, summary, fact embeddings",
|
||||
"GIN: ts_rank on name+summary and fact",
|
||||
"RRF fusion: sem_weight + lex_weight"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "sequence",
|
||||
"meta": {
|
||||
"title": "Service Acting on Behalf of User",
|
||||
"viewBox": [1000, 600],
|
||||
"column_fit": "spread",
|
||||
"quality_profile": "showcase"
|
||||
},
|
||||
"participants": [
|
||||
{ "id": "user", "type": "external", "label": "User", "sublabel": "browser" },
|
||||
{ "id": "app", "type": "frontend", "label": "Web App", "sublabel": "poimen UI" },
|
||||
{ "id": "authentik", "type": "security", "label": "Authentik", "sublabel": "OIDC" },
|
||||
{ "id": "worker", "type": "backend", "label": "Backend Worker", "sublabel": "temporal/queue" },
|
||||
{ "id": "memory", "type": "backend", "label": "Memory API", "sublabel": "poimen-memory" },
|
||||
{ "id": "jwks", "type": "security", "label": "JWKS", "sublabel": "RS256" }
|
||||
],
|
||||
"segments": [
|
||||
{ "from": 150, "to": 260, "label": "User Auth" },
|
||||
{ "from": 270, "to": 370, "label": "Delegation" },
|
||||
{ "from": 380, "to": 510, "label": "Downstream" }
|
||||
],
|
||||
"messages": [
|
||||
{ "id": "login", "from": "user", "to": "app", "y": 165, "label": "login (OIDC)", "variant": "default" },
|
||||
{ "id": "oidc-flow", "from": "app", "to": "authentik", "y": 200, "label": "authorization_code flow", "variant": "emphasis" },
|
||||
{ "id": "user-jwt", "from": "authentik", "to": "app", "y": 235, "label": "user JWT {groups,roles}", "variant": "return" },
|
||||
{ "id": "trigger-job", "from": "user", "to": "app", "y": 285, "label": "trigger async job", "variant": "default" },
|
||||
{ "id": "queue-job", "from": "app", "to": "worker", "y": 320, "label": "enqueue job + user JWT", "variant": "emphasis", "note": "JWT passed as job metadata" },
|
||||
{ "id": "job-ack", "from": "worker", "to": "app", "y": 355, "label": "job queued", "variant": "return" },
|
||||
{ "id": "call-memory", "from": "worker", "to": "memory", "y": 400, "label": "POST /memory/ingest", "variant": "emphasis", "note": "Bearer <user JWT>" },
|
||||
{ "id": "fetch-jwks", "from": "memory", "to": "jwks", "y": 435, "label": "GET /jwks", "variant": "dashed" },
|
||||
{ "id": "jwks-keys", "from": "jwks", "to": "memory", "y": 470, "label": "RSA pubkey", "variant": "return" },
|
||||
{ "id": "ingest-ok", "from": "memory", "to": "worker", "y": 505, "label": "202 Accepted", "variant": "return", "note": "uses USER's groups/roles" }
|
||||
],
|
||||
"activations": [
|
||||
{ "participant": "user", "from": 160, "to": 291, "type": "external" },
|
||||
{ "participant": "app", "from": 165, "to": 361, "type": "frontend" },
|
||||
{ "participant": "authentik", "from": 195, "to": 241, "type": "security" },
|
||||
{ "participant": "worker", "from": 315, "to": 511, "type": "backend" },
|
||||
{ "participant": "memory", "from": 395, "to": 511, "type": "backend" },
|
||||
{ "participant": "jwks", "from": 430, "to": 476, "type": "security" }
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "Delegation Pattern",
|
||||
"items": [
|
||||
"User JWT travels with the job payload",
|
||||
"Worker uses user's token for downstream",
|
||||
"Memory sees USER as the principal"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "rose",
|
||||
"title": "Security Model",
|
||||
"items": [
|
||||
"No service-account escalation",
|
||||
"User's permissions bound the action",
|
||||
"Audit trail traces to user.sub"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "architecture",
|
||||
"meta": {
|
||||
"title": "Memory Service Fine-Grained RBAC",
|
||||
"quality_profile": "showcase",
|
||||
"viewBox": [900, 520]
|
||||
},
|
||||
"components": [
|
||||
{ "id": "jwt", "type": "security", "label": "JWT Token", "sublabel": "groups[] + roles[]", "pos": [40, 60], "size": [140, 60], "tag": "Authentik" },
|
||||
{ "id": "cap-check", "type": "backend", "label": "Capability Check", "sublabel": "has_capability()", "pos": [260, 60], "size": [160, 60], "tag": "Phase 1" },
|
||||
{ "id": "scope-checker", "type": "backend", "label": "Scope Checker", "sublabel": "filter_resources()", "pos": [500, 60], "size": [160, 60], "tag": "Phase 2" },
|
||||
{ "id": "decision", "type": "backend", "label": "AccessDecision", "sublabel": "Allow / Deny", "pos": [740, 60], "size": [130, 60] },
|
||||
{ "id": "project-scope", "type": "database", "label": "ProjectScope", "sublabel": "dev/staging/prod/*", "pos": [40, 200], "size": [140, 55] },
|
||||
{ "id": "visibility-scope", "type": "database", "label": "VisibilityScope", "sublabel": "public/private", "pos": [220, 200], "size": [140, 55] },
|
||||
{ "id": "owner-scope", "type": "database", "label": "OwnerScope", "sublabel": "self/any/user", "pos": [400, 200], "size": [140, 55] },
|
||||
{ "id": "group-scope", "type": "database", "label": "GroupScope", "sublabel": "in_any_group()", "pos": [580, 200], "size": [140, 55] },
|
||||
{ "id": "entities", "type": "database", "label": "Entities", "sublabel": "memory_entity", "pos": [40, 340], "size": [120, 50] },
|
||||
{ "id": "edges", "type": "database", "label": "Edges", "sublabel": "memory_edge", "pos": [200, 340], "size": [120, 50] },
|
||||
{ "id": "projects", "type": "database", "label": "Projects", "sublabel": "project_id", "pos": [360, 340], "size": [120, 50] },
|
||||
{ "id": "convos", "type": "database", "label": "Conversations", "sublabel": "owner_id", "pos": [520, 340], "size": [120, 50] }
|
||||
],
|
||||
"boundaries": [
|
||||
{ "kind": "security-group", "label": "Access Control Pipeline", "wraps": ["cap-check", "scope-checker", "decision"] },
|
||||
{ "kind": "region", "label": "Scope Checkers", "wraps": ["project-scope", "visibility-scope", "owner-scope", "group-scope"] },
|
||||
{ "kind": "region", "label": "Protected Resources", "wraps": ["entities", "edges", "projects", "convos"] }
|
||||
],
|
||||
"connections": [
|
||||
{ "id": "jwt-to-cap", "from": "jwt", "to": "cap-check", "label": "claims", "variant": "emphasis" },
|
||||
{ "id": "cap-to-scope", "from": "cap-check", "to": "scope-checker", "variant": "emphasis" },
|
||||
{ "id": "scope-to-decision", "from": "scope-checker", "to": "decision", "variant": "emphasis" },
|
||||
{ "id": "scope-to-proj", "from": "scope-checker", "to": "project-scope", "variant": "dashed", "fromSide": "bottom", "toSide": "top" },
|
||||
{ "id": "scope-to-vis", "from": "scope-checker", "to": "visibility-scope", "variant": "dashed", "fromSide": "bottom", "toSide": "top" },
|
||||
{ "id": "scope-to-owner", "from": "scope-checker", "to": "owner-scope", "variant": "dashed", "fromSide": "bottom", "toSide": "top" },
|
||||
{ "id": "scope-to-group", "from": "scope-checker", "to": "group-scope", "variant": "dashed", "fromSide": "bottom", "toSide": "top" }
|
||||
],
|
||||
"cards": [
|
||||
{ "dot": "emerald", "title": "Phase 1: Capability", "items": ["has_capability(memory:read)", "Checks roles[] claim", "Binary allow/deny"] },
|
||||
{ "dot": "rose", "title": "Phase 2: Scope", "items": ["Project: dev/staging/prod", "Visibility: public/private", "Owner: self-owned only", "Group: poimen-admins/devs"] },
|
||||
{ "dot": "cyan", "title": "JWT Claims", "items": ["groups: user.groups", "roles: attributes.roles", "permissions: group names"] }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "sequence",
|
||||
"meta": {
|
||||
"title": "Service-to-Service (M2M) Authentication",
|
||||
"viewBox": [1080, 520],
|
||||
"column_fit": "spread",
|
||||
"quality_profile": "showcase"
|
||||
},
|
||||
"participants": [
|
||||
{ "id": "worker", "type": "backend", "label": "Ingest Worker", "sublabel": "temporal/cron" },
|
||||
{ "id": "authentik", "type": "security", "label": "Authentik", "sublabel": "OIDC provider" },
|
||||
{ "id": "memory", "type": "backend", "label": "Memory Service", "sublabel": "poimen-memory" },
|
||||
{ "id": "jwks", "type": "security", "label": "JWKS", "sublabel": "RS256 keys" }
|
||||
],
|
||||
"segments": [
|
||||
{ "from": 150, "to": 230, "label": "Token Mint" },
|
||||
{ "from": 240, "to": 380, "label": "API + RBAC" }
|
||||
],
|
||||
"messages": [
|
||||
{ "id": "token-req", "from": "worker", "to": "authentik", "y": 165, "label": "POST /token grant_type=password", "variant": "emphasis", "note": "client_id + username + app_password + scope" },
|
||||
{ "id": "jwt-resp", "from": "authentik", "to": "worker", "y": 205, "label": "JWT {groups,roles,aud,iss}", "variant": "return", "note": "groups=[poimen-admins] roles=[memory:write]" },
|
||||
{ "id": "api-call", "from": "worker", "to": "memory", "y": 260, "label": "POST /memory/ingest", "variant": "emphasis", "note": "Authorization: Bearer <JWT>" },
|
||||
{ "id": "fetch-jwks", "from": "memory", "to": "jwks", "y": 295, "label": "GET /jwks (cached)", "variant": "dashed" },
|
||||
{ "id": "jwks-resp", "from": "jwks", "to": "memory", "y": 330, "label": "RSA pubkey (kid)", "variant": "return" },
|
||||
{ "id": "verify-ok", "from": "memory", "to": "worker", "y": 365, "label": "202 Accepted", "variant": "return", "note": "sig ✓ iss ✓ aud ✓ has_capability ✓" }
|
||||
],
|
||||
"activations": [
|
||||
{ "participant": "worker", "from": 160, "to": 371, "type": "backend" },
|
||||
{ "participant": "authentik", "from": 165, "to": 211, "type": "security" },
|
||||
{ "participant": "memory", "from": 255, "to": 371, "type": "backend" },
|
||||
{ "participant": "jwks", "from": 290, "to": 336, "type": "security" }
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "M2M Pattern",
|
||||
"items": [
|
||||
"grant_type=password (Authentik aliases client_credentials)",
|
||||
"App-password token authenticates the service account",
|
||||
"NOT client_secret — that creates nameless SA"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "rose",
|
||||
"title": "JWT Claims",
|
||||
"items": [
|
||||
"groups: from user.groups (poimen-admins)",
|
||||
"roles: from user.attributes.roles",
|
||||
"iss/aud: per-provider issuer"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"schema_version": 1,
|
||||
"diagram_type": "sequence",
|
||||
"meta": {
|
||||
"title": "User Direct Access with JWT",
|
||||
"viewBox": [1000, 640],
|
||||
"column_fit": "spread",
|
||||
"quality_profile": "showcase"
|
||||
},
|
||||
"participants": [
|
||||
{ "id": "user", "type": "external", "label": "User", "sublabel": "browser" },
|
||||
{ "id": "app", "type": "frontend", "label": "Web App", "sublabel": "poimen UI" },
|
||||
{ "id": "authentik", "type": "security", "label": "Authentik", "sublabel": "OIDC provider" },
|
||||
{ "id": "memory", "type": "backend", "label": "Memory API", "sublabel": "poimen-memory" },
|
||||
{ "id": "jwks", "type": "security", "label": "JWKS", "sublabel": "RS256 keys" }
|
||||
],
|
||||
"segments": [
|
||||
{ "from": 150, "to": 370, "label": "OIDC Login" },
|
||||
{ "from": 380, "to": 540, "label": "API + RBAC" }
|
||||
],
|
||||
"messages": [
|
||||
{ "id": "click-login", "from": "user", "to": "app", "y": 165, "label": "click Login", "variant": "default" },
|
||||
{ "id": "redirect-auth", "from": "app", "to": "authentik", "y": 200, "label": "redirect /authorize", "variant": "emphasis", "note": "scope=openid groups roles" },
|
||||
{ "id": "show-login", "from": "authentik", "to": "user", "y": 235, "label": "login form", "variant": "default" },
|
||||
{ "id": "submit-creds", "from": "user", "to": "authentik", "y": 270, "label": "username + password", "variant": "security" },
|
||||
{ "id": "auth-code", "from": "authentik", "to": "app", "y": 305, "label": "redirect ?code=xxx", "variant": "return" },
|
||||
{ "id": "exchange-token", "from": "app", "to": "authentik", "y": 340, "label": "POST /token code=xxx", "variant": "emphasis" },
|
||||
{ "id": "jwt-resp", "from": "authentik", "to": "app", "y": 375, "label": "JWT {groups,roles,sub}", "variant": "return" },
|
||||
{ "id": "query-api", "from": "app", "to": "memory", "y": 420, "label": "GET /memory/query", "variant": "emphasis", "note": "Authorization: Bearer <JWT>" },
|
||||
{ "id": "fetch-jwks", "from": "memory", "to": "jwks", "y": 455, "label": "GET /jwks (cached)", "variant": "dashed" },
|
||||
{ "id": "jwks-keys", "from": "jwks", "to": "memory", "y": 490, "label": "RSA pubkey", "variant": "return" },
|
||||
{ "id": "query-resp", "from": "memory", "to": "app", "y": 525, "label": "200 JSON results", "variant": "return", "note": "has_capability(memory:read) ✓" }
|
||||
],
|
||||
"activations": [
|
||||
{ "participant": "user", "from": 160, "to": 276, "type": "external" },
|
||||
{ "participant": "app", "from": 165, "to": 531, "type": "frontend" },
|
||||
{ "participant": "authentik", "from": 195, "to": 381, "type": "security" },
|
||||
{ "participant": "memory", "from": 415, "to": 531, "type": "backend" },
|
||||
{ "participant": "jwks", "from": 450, "to": 496, "type": "security" }
|
||||
],
|
||||
"cards": [
|
||||
{
|
||||
"dot": "emerald",
|
||||
"title": "Authorization Code Flow",
|
||||
"items": [
|
||||
"User authenticates directly with Authentik",
|
||||
"App exchanges code for JWT",
|
||||
"Scopes: openid groups roles permissions"
|
||||
]
|
||||
},
|
||||
{
|
||||
"dot": "rose",
|
||||
"title": "JWT Contents",
|
||||
"items": [
|
||||
"iss: .../application/o/poimen/",
|
||||
"aud: poimen (provider client_id)",
|
||||
"groups: user's group memberships"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
# cloudflared ingress configuration.
|
||||
#
|
||||
# This list is the tunnel's routing table: for each incoming hostname the
|
||||
# connector dials the matching origin. The final rule is a catch-all, so a
|
||||
# hostname absent from this list gets 404 from the connector — DNS alone does
|
||||
# not route a request into a service.
|
||||
#
|
||||
# Management model: this file makes the tunnel LOCALLY managed. cloudflared is
|
||||
# given a credentials file instead of a token, so Cloudflare stops pushing the
|
||||
# remotely-managed ingess list and this ConfigMap wins. Revert the Deployment to
|
||||
# `run --token` to return to the dashboard-managed list.
|
||||
#
|
||||
# Origin services self-reference the public hostname. That resolves because
|
||||
# CoreDNS rewrites each of these names to ingress-nginx
|
||||
# (terraform/files/coredns/Corefile), so the connector reaches the in-cluster
|
||||
# ingress instead of looping back out through Cloudflare.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: cloudflared-config
|
||||
namespace: cloudflared
|
||||
data:
|
||||
config.yml: |
|
||||
tunnel: 32d0bf93-e1bd-45b8-93a9-71ff9f83de80
|
||||
credentials-file: /etc/cloudflared/creds/credentials.json
|
||||
|
||||
# WARP-to-Tunnel routing. The remotely-managed config had this enabled;
|
||||
# dropping the key would break WARP access to private addresses. cloudflared
|
||||
# 2026.x has no `enabled` field — the key's presence is what enables it, and
|
||||
# `enabled: true` here makes the binary warn about an unused key.
|
||||
warp-routing: {}
|
||||
|
||||
ingress:
|
||||
- hostname: paperless.riotpiao.com
|
||||
service: https://paperless.riotpiao.com
|
||||
- hostname: img.riotpiao.com
|
||||
service: https://img.riotpiao.com
|
||||
- hostname: riotpiao.com
|
||||
service: https://riotpiao.com
|
||||
- hostname: grafana.riotpiao.com
|
||||
service: https://grafana.riotpiao.com
|
||||
- hostname: gotify.riotpiao.com
|
||||
service: https://gotify.riotpiao.com
|
||||
- hostname: igotify.riotpiao.com
|
||||
service: https://igotify.riotpiao.com
|
||||
- hostname: poimen.riotpiao.com
|
||||
service: https://poimen.riotpiao.com
|
||||
- hostname: comfyui.riotpiao.com
|
||||
service: https://comfyui.riotpiao.com
|
||||
- hostname: forgejo.riotpiao.com
|
||||
service: https://forgejo.riotpiao.com
|
||||
# ZITADEL. Without this rule the connector answers 404 for the hostname
|
||||
# even though the DNS CNAME points at the tunnel.
|
||||
- hostname: zitadel.riotpiao.com
|
||||
service: https://zitadel.riotpiao.com
|
||||
- service: http_status:404
|
||||
@@ -13,21 +13,63 @@ spec:
|
||||
labels:
|
||||
app: cloudflared
|
||||
spec:
|
||||
# Ingress-nginx forces HTTPS (force-ssl-redirect), so cloudflared must
|
||||
# dial https://<hostname> and trust homelab-ca or the TLS handshake fails
|
||||
# with "certificate signed by unknown authority". Same pattern as
|
||||
# iam values: merge the system bundle with homelab-ca.crt.
|
||||
initContainers:
|
||||
- name: merge-ca-certs
|
||||
image: debian:bookworm
|
||||
imagePullPolicy: IfNotPresent
|
||||
securityContext:
|
||||
runAsUser: 0
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- (cat /etc/ssl/certs/ca-certificates.crt 2>/dev/null; cat /homelab-ca/ca.crt) > /merged/ca-bundle.crt
|
||||
volumeMounts:
|
||||
- name: homelab-ca
|
||||
mountPath: /homelab-ca
|
||||
readOnly: true
|
||||
- name: merged-ca
|
||||
mountPath: /merged
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: cloudflared-config
|
||||
- name: credentials
|
||||
secret:
|
||||
secretName: cloudflared-credentials
|
||||
- name: homelab-ca
|
||||
configMap:
|
||||
name: homelab-ca
|
||||
- name: merged-ca
|
||||
emptyDir: {}
|
||||
containers:
|
||||
- name: cloudflared
|
||||
image: cloudflare/cloudflared:latest
|
||||
args:
|
||||
- tunnel
|
||||
- --no-autoupdate
|
||||
- --protocol
|
||||
- http2
|
||||
- --config
|
||||
- /etc/cloudflared/config.yml
|
||||
- run
|
||||
- --token
|
||||
- $(TUNNEL_TOKEN)
|
||||
env:
|
||||
- name: TUNNEL_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: cloudflared-token
|
||||
key: token
|
||||
- name: SSL_CERT_FILE
|
||||
value: /merged/ca-bundle.crt
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/cloudflared/config.yml
|
||||
subPath: config.yml
|
||||
readOnly: true
|
||||
- name: credentials
|
||||
mountPath: /etc/cloudflared/creds
|
||||
readOnly: true
|
||||
- name: merged-ca
|
||||
mountPath: /merged
|
||||
readOnly: true
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
|
||||
@@ -3,3 +3,4 @@ kind: Kustomization
|
||||
namespace: cloudflared
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- configmap.yaml
|
||||
|
||||
@@ -4,22 +4,27 @@ metadata:
|
||||
name: comfyui
|
||||
namespace: comfyui
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
# No cert-manager annotation and no tls: block. This host is covered by
|
||||
# the *.riotpiao.com wildcard, which nginx serves as its
|
||||
# default-ssl-certificate (ingress-nginx/riotpiao-com-tls).
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "0"
|
||||
# WebSocket support for ComfyUI's live preview
|
||||
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||
nginx.ingress.kubernetes.io/upstream-hash-by: "$remote_addr"
|
||||
nginx.ingress.kubernetes.io/configuration-snippet: |
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
# Single sign-on. ComfyUI has no auth of its own, so every request is handed to
|
||||
# oauth2-proxy for an allow or deny decision.
|
||||
nginx.ingress.kubernetes.io/auth-url: "http://comfyui-oauth2-proxy.comfyui.svc.cluster.local:4180/oauth2/auth"
|
||||
nginx.ingress.kubernetes.io/auth-signin: "https://comfyui.riotpiao.com/oauth2/start?rd=$scheme://$host$request_uri"
|
||||
nginx.ingress.kubernetes.io/auth-response-headers: "x-auth-request-user,x-auth-request-email"
|
||||
# No configuration-snippet. The controller rejects that annotation as risky
|
||||
# ("annotation group ConfigurationSnippet contains risky annotation"), and a
|
||||
# rejected annotation drops the WHOLE ingress from nginx.conf: the host then
|
||||
# answers 404 with no entry in the access log. ingress-nginx sets the Upgrade
|
||||
# and Connection headers itself for a websocket upgrade.
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- secretName: comfyui-tls
|
||||
hosts:
|
||||
- comfyui.riotpiao.com
|
||||
rules:
|
||||
- host: comfyui.riotpiao.com
|
||||
http:
|
||||
|
||||
@@ -4,4 +4,5 @@ kind: Kustomization
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- oauth2-proxy.yaml
|
||||
- ingress.yaml
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
# ComfyUI has no authentication of its own: comfyui.riotpiao.com is open to anyone
|
||||
# who can resolve the host. It also has no OIDC support, so ZITADEL cannot be a
|
||||
# login method inside it. oauth2-proxy is the front door instead: nginx asks it for
|
||||
# a decision on every request and it answers from a ZITADEL session.
|
||||
#
|
||||
# No API client calls this host today, so the whole host is gated. If a client ever
|
||||
# needs direct access, add a skip rule for the exact paths it uses rather than
|
||||
# opening the whole host.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
labels:
|
||||
app: comfyui-oauth2-proxy
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: comfyui-oauth2-proxy
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: comfyui-oauth2-proxy
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
seccompProfile: {type: RuntimeDefault}
|
||||
containers:
|
||||
- name: oauth2-proxy
|
||||
image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
|
||||
args:
|
||||
- --provider=oidc
|
||||
- --oidc-issuer-url=https://zitadel.riotpiao.com
|
||||
- --email-domain=*
|
||||
- --upstream=static://202
|
||||
- --reverse-proxy=true
|
||||
- --set-xauthrequest=true
|
||||
- --cookie-secure=true
|
||||
- --cookie-expire=12h
|
||||
- --cookie-refresh=1h
|
||||
- --skip-provider-button=true
|
||||
- --redirect-url=https://comfyui.riotpiao.com/oauth2/callback
|
||||
- --http-address=0.0.0.0:4180
|
||||
env:
|
||||
- name: OAUTH2_PROXY_CLIENT_ID
|
||||
valueFrom: {secretKeyRef: {name: comfyui-oauth2-proxy, key: client-id}}
|
||||
- name: OAUTH2_PROXY_CLIENT_SECRET
|
||||
valueFrom: {secretKeyRef: {name: comfyui-oauth2-proxy, key: client-secret}}
|
||||
- name: OAUTH2_PROXY_COOKIE_SECRET
|
||||
valueFrom: {secretKeyRef: {name: comfyui-oauth2-proxy, key: cookie-secret}}
|
||||
ports:
|
||||
- {name: http, containerPort: 4180}
|
||||
readinessProbe:
|
||||
httpGet: {path: /ping, port: http}
|
||||
initialDelaySeconds: 5
|
||||
livenessProbe:
|
||||
httpGet: {path: /ping, port: http}
|
||||
initialDelaySeconds: 15
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: {drop: ["ALL"]}
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {cpu: 200m, memory: 128Mi}
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
labels:
|
||||
app: comfyui-oauth2-proxy
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: comfyui-oauth2-proxy
|
||||
ports:
|
||||
- {name: http, port: 4180, targetPort: http}
|
||||
---
|
||||
# The login path must NOT be gated: a request to /oauth2/start that itself demands a
|
||||
# login redirects to /oauth2/start forever. ingress-nginx applies the annotations of
|
||||
# an ingress to the locations it defines, so a separate object without auth-url keeps
|
||||
# this path open. The earlier failure of this shape was not the split — it was the
|
||||
# configuration-snippet on the app ingress, which made the controller drop the whole
|
||||
# object and left only this one alive.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: comfyui-oauth2
|
||||
namespace: comfyui
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: comfyui.riotpiao.com
|
||||
http:
|
||||
paths:
|
||||
- path: /oauth2
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: comfyui-oauth2-proxy
|
||||
port:
|
||||
number: 4180
|
||||
@@ -0,0 +1,110 @@
|
||||
# The bridge script is mounted as a ConfigMap so no image build is needed.
|
||||
# Regenerate this block from grafana_gotify_bridge.py when the script changes.
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: grafana-gotify-bridge
|
||||
namespace: notifications
|
||||
data:
|
||||
grafana_gotify_bridge.py: |
|
||||
#!/usr/bin/env python3
|
||||
"""Translate a Grafana alert webhook into a Gotify message.
|
||||
|
||||
Grafana sends its own payload shape:
|
||||
{"receiver": "...", "status": "firing", "alerts": [{"labels": {...}, "annotations": {...}}]}
|
||||
|
||||
Gotify requires:
|
||||
{"title": "...", "message": "...", "priority": 0..10}
|
||||
|
||||
Gotify answers 400 "Field 'message' is required" to a Grafana payload, so a
|
||||
conversion step is mandatory. This service is that step and nothing else.
|
||||
Standard library only, so it needs no image build and no dependencies.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
GOTIFY_BASE = os.environ.get("GOTIFY_URL", "http://gotify.notifications.svc.cluster.local")
|
||||
GOTIFY_TOKEN = os.environ["GOTIFY_TOKEN"]
|
||||
LISTEN_PORT = int(os.environ.get("PORT", "8080"))
|
||||
|
||||
# Firing is louder than resolved. Gotify priority runs 0 to 10.
|
||||
PRIORITY = {"firing": 8, "resolved": 3}
|
||||
|
||||
|
||||
def to_gotify(payload):
|
||||
alerts = payload.get("alerts") or []
|
||||
status = (payload.get("status") or "firing").lower()
|
||||
priority = PRIORITY.get(status, 5)
|
||||
|
||||
if not alerts:
|
||||
title = payload.get("title") or "Grafana notification"
|
||||
return title, json.dumps(payload)[:2000], priority
|
||||
|
||||
first = alerts[0]
|
||||
labels = first.get("labels") or {}
|
||||
annotations = first.get("annotations") or {}
|
||||
name = labels.get("alertname", "Grafana alert")
|
||||
|
||||
title = "[%s] %s" % (status.upper(), name)
|
||||
lines = []
|
||||
summary = annotations.get("summary") or annotations.get("description")
|
||||
if summary:
|
||||
lines.append(summary)
|
||||
for key in ("severity", "namespace", "pod", "instance", "job"):
|
||||
if labels.get(key):
|
||||
lines.append("%s: %s" % (key, labels[key]))
|
||||
if len(alerts) > 1:
|
||||
lines.append("+%d more alert(s) in this notification" % (len(alerts) - 1))
|
||||
|
||||
return title, "\n".join(lines) or name, priority
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def _reply(self, code, body=b"ok"):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
self._reply(200)
|
||||
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8", "replace") or "{}")
|
||||
except ValueError:
|
||||
payload = {}
|
||||
|
||||
title, message, priority = to_gotify(payload)
|
||||
body = json.dumps({
|
||||
"title": title[:200],
|
||||
"message": message[:4000],
|
||||
"priority": priority,
|
||||
}).encode()
|
||||
|
||||
request = urllib.request.Request(
|
||||
GOTIFY_BASE.rstrip("/") + "/message",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json", "X-Gotify-Key": GOTIFY_TOKEN},
|
||||
)
|
||||
try:
|
||||
urllib.request.urlopen(request, timeout=10).read()
|
||||
except Exception as exc: # noqa: BLE001 - log and report to Grafana
|
||||
print("forward to gotify failed: %s" % exc, flush=True)
|
||||
self._reply(502, b"gotify rejected the message")
|
||||
return
|
||||
|
||||
print("forwarded: %s" % title, flush=True)
|
||||
self._reply(200)
|
||||
|
||||
def log_message(self, fmt, *args): # keep the log to one line per delivery
|
||||
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("grafana-gotify bridge listening on %d" % LISTEN_PORT, flush=True)
|
||||
HTTPServer(("0.0.0.0", LISTEN_PORT), Handler).serve_forever()
|
||||
@@ -0,0 +1,80 @@
|
||||
# Grafana sends its own JSON and Gotify rejects it, so this Deployment is the
|
||||
# translation step between them. Grafana posts to the Service, the bridge posts
|
||||
# to /message with the app token from a SOPS secret.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: grafana-gotify-bridge
|
||||
namespace: notifications
|
||||
labels:
|
||||
app: grafana-gotify-bridge
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: grafana-gotify-bridge
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: grafana-gotify-bridge
|
||||
spec:
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: bridge
|
||||
image: python:3.12-alpine
|
||||
command: ["python3", "/app/grafana_gotify_bridge.py"]
|
||||
env:
|
||||
- name: GOTIFY_URL
|
||||
value: "http://gotify.notifications.svc.cluster.local"
|
||||
- name: PORT
|
||||
value: "8080"
|
||||
- name: GOTIFY_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef: {name: grafana-gotify-bridge, key: app-token}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet: {path: /, port: http}
|
||||
initialDelaySeconds: 3
|
||||
periodSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet: {path: /, port: http}
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 20
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
readOnlyRootFilesystem: true
|
||||
capabilities:
|
||||
drop: ["ALL"]
|
||||
volumeMounts:
|
||||
- name: script
|
||||
mountPath: /app
|
||||
readOnly: true
|
||||
resources:
|
||||
requests: {cpu: 10m, memory: 32Mi}
|
||||
limits: {cpu: 200m, memory: 96Mi}
|
||||
volumes:
|
||||
- name: script
|
||||
configMap:
|
||||
name: grafana-gotify-bridge
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: grafana-gotify-bridge
|
||||
namespace: notifications
|
||||
labels:
|
||||
app: grafana-gotify-bridge
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app: grafana-gotify-bridge
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: http
|
||||
@@ -0,0 +1,101 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Translate a Grafana alert webhook into a Gotify message.
|
||||
|
||||
Grafana sends its own payload shape:
|
||||
{"receiver": "...", "status": "firing", "alerts": [{"labels": {...}, "annotations": {...}}]}
|
||||
|
||||
Gotify requires:
|
||||
{"title": "...", "message": "...", "priority": 0..10}
|
||||
|
||||
Gotify answers 400 "Field 'message' is required" to a Grafana payload, so a
|
||||
conversion step is mandatory. This service is that step and nothing else.
|
||||
Standard library only, so it needs no image build and no dependencies.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
GOTIFY_BASE = os.environ.get("GOTIFY_URL", "http://gotify.notifications.svc.cluster.local")
|
||||
GOTIFY_TOKEN = os.environ["GOTIFY_TOKEN"]
|
||||
LISTEN_PORT = int(os.environ.get("PORT", "8080"))
|
||||
|
||||
# Firing is louder than resolved. Gotify priority runs 0 to 10.
|
||||
PRIORITY = {"firing": 8, "resolved": 3}
|
||||
|
||||
|
||||
def to_gotify(payload):
|
||||
alerts = payload.get("alerts") or []
|
||||
status = (payload.get("status") or "firing").lower()
|
||||
priority = PRIORITY.get(status, 5)
|
||||
|
||||
if not alerts:
|
||||
title = payload.get("title") or "Grafana notification"
|
||||
return title, json.dumps(payload)[:2000], priority
|
||||
|
||||
first = alerts[0]
|
||||
labels = first.get("labels") or {}
|
||||
annotations = first.get("annotations") or {}
|
||||
name = labels.get("alertname", "Grafana alert")
|
||||
|
||||
title = "[%s] %s" % (status.upper(), name)
|
||||
lines = []
|
||||
summary = annotations.get("summary") or annotations.get("description")
|
||||
if summary:
|
||||
lines.append(summary)
|
||||
for key in ("severity", "namespace", "pod", "instance", "job"):
|
||||
if labels.get(key):
|
||||
lines.append("%s: %s" % (key, labels[key]))
|
||||
if len(alerts) > 1:
|
||||
lines.append("+%d more alert(s) in this notification" % (len(alerts) - 1))
|
||||
|
||||
return title, "\n".join(lines) or name, priority
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def _reply(self, code, body=b"ok"):
|
||||
self.send_response(code)
|
||||
self.send_header("Content-Type", "text/plain")
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self):
|
||||
self._reply(200)
|
||||
|
||||
def do_POST(self):
|
||||
length = int(self.headers.get("Content-Length") or 0)
|
||||
raw = self.rfile.read(length) if length else b"{}"
|
||||
try:
|
||||
payload = json.loads(raw.decode("utf-8", "replace") or "{}")
|
||||
except ValueError:
|
||||
payload = {}
|
||||
|
||||
title, message, priority = to_gotify(payload)
|
||||
body = json.dumps({
|
||||
"title": title[:200],
|
||||
"message": message[:4000],
|
||||
"priority": priority,
|
||||
}).encode()
|
||||
|
||||
request = urllib.request.Request(
|
||||
GOTIFY_BASE.rstrip("/") + "/message",
|
||||
data=body,
|
||||
headers={"Content-Type": "application/json", "X-Gotify-Key": GOTIFY_TOKEN},
|
||||
)
|
||||
try:
|
||||
urllib.request.urlopen(request, timeout=10).read()
|
||||
except Exception as exc: # noqa: BLE001 - log and report to Grafana
|
||||
print("forward to gotify failed: %s" % exc, flush=True)
|
||||
self._reply(502, b"gotify rejected the message")
|
||||
return
|
||||
|
||||
print("forwarded: %s" % title, flush=True)
|
||||
self._reply(200)
|
||||
|
||||
def log_message(self, fmt, *args): # keep the log to one line per delivery
|
||||
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("grafana-gotify bridge listening on %d" % LISTEN_PORT, flush=True)
|
||||
HTTPServer(("0.0.0.0", LISTEN_PORT), Handler).serve_forever()
|
||||
@@ -28,9 +28,17 @@ spec:
|
||||
value: "true"
|
||||
- name: ENABLE_SCALAR_UI
|
||||
value: "true"
|
||||
# GOTIFY_URLS / GOTIFY_CLIENT_TOKENS / SECNTFY_TOKENS intentionally
|
||||
# left unset — not required when Gotify is reachable via public domain
|
||||
# (gotify.riotpiao.com). iGotify iOS app registers itself at runtime.
|
||||
# The bridge authenticates with a Gotify client token, not with
|
||||
# ZITADEL. Gotify has no way to accept a forwarded identity, so the
|
||||
# token is the only credential that works server-side. The token
|
||||
# belongs to user `rock`, never expires, and is revoked by deleting
|
||||
# the client row in Gotify.
|
||||
- name: GOTIFY_URLS
|
||||
value: "https://gotify.riotpiao.com"
|
||||
- name: GOTIFY_CLIENT_TOKENS
|
||||
valueFrom:
|
||||
secretKeyRef: {name: igotify-client-token, key: client-token}
|
||||
# SECNTFY_TOKENS intentionally left unset: secntfy is not deployed.
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /app/data
|
||||
|
||||
@@ -6,6 +6,8 @@ resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
- bridge/configmap.yaml
|
||||
- bridge/deployment.yaml
|
||||
- igotify-pvc.yaml
|
||||
- igotify-deployment.yaml
|
||||
- igotify-service.yaml
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
# Homarr landing page with Authentik SSO
|
||||
|
||||
# Probe tuning (chart DOES expose these — the old PostSync patch-job was
|
||||
# unnecessary and fragile: it only patched one Deployment revision, so any later
|
||||
# rollout reverted to the chart's aggressive defaults). Homarr's first-boot icon
|
||||
# updater blocks the event loop for ~50s ("icons updater took 49553ms"), during
|
||||
# which /api/health/live can't answer within the default 10s×3 window → kubelet
|
||||
# SIGTERMs the pod → CrashLoopBackOff (247 restarts, 503 at the ingress). Give
|
||||
# liveness a wide window so the icon import can finish without a kill.
|
||||
livenessProbe:
|
||||
initialDelaySeconds: 60
|
||||
periodSeconds: 30
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 10
|
||||
readinessProbe:
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 15
|
||||
timeoutSeconds: 5
|
||||
failureThreshold: 6
|
||||
|
||||
image:
|
||||
repository: ghcr.io/homarr-labs/homarr
|
||||
tag: "latest"
|
||||
pullPolicy: Always
|
||||
|
||||
replicaCount: 1
|
||||
|
||||
# Configure SSO via environment variables
|
||||
# Chart supports these via top-level env dict (not array)
|
||||
env:
|
||||
AUTH_PROVIDERS: "oidc,credentials"
|
||||
AUTH_OIDC_ISSUER: "https://authentik.riotpiao.com/application/o/homarr/"
|
||||
# AUTH_OIDC_URI (authorize endpoint) is REQUIRED in addition to ISSUER — homarr
|
||||
# hides the "Sign in with Authentik" button entirely when it's absent (per the
|
||||
# authentik Homarr integration + homarr SSO docs). This was the missing var.
|
||||
AUTH_OIDC_URI: "https://authentik.riotpiao.com/application/o/authorize/"
|
||||
AUTH_OIDC_CLIENT_NAME: "Authentik"
|
||||
AUTH_OIDC_GROUPS_ATTRIBUTE: "groups"
|
||||
AUTH_OIDC_SCOPE_OVERWRITE: "openid email profile groups"
|
||||
AUTH_OIDC_AUTO_LOGIN: "false"
|
||||
# Link the OIDC identity to an existing homarr account with the same email.
|
||||
OAUTH_ALLOW_DANGEROUS_EMAIL_ACCOUNT_LINKING: "true"
|
||||
# The analytics cron blocked the (single-threaded) Next.js event loop for ~16s
|
||||
# per run ("callback took longer than expected"), compounding CPU pressure.
|
||||
DISABLE_ANALYTICS: "true"
|
||||
BASE_URL: "https://homarr.riotpiao.com"
|
||||
NEXTAUTH_URL: "https://homarr.riotpiao.com"
|
||||
|
||||
# Client credentials from homarr-oidc secret
|
||||
# Chart doesn't support envFrom, so we add via extraEnv
|
||||
extraEnv:
|
||||
- name: AUTH_OIDC_CLIENT_ID
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: homarr-oidc
|
||||
key: client-id
|
||||
- name: AUTH_OIDC_CLIENT_SECRET
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: homarr-oidc
|
||||
key: client-secret
|
||||
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
operator: Exists
|
||||
effect: NoSchedule
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 250m
|
||||
memory: 384Mi
|
||||
limits:
|
||||
# Next.js 16 + bundled redis + the icon-updater (28k icons) saturated the old
|
||||
# 500m limit; CPU throttling made Next.js abort with exit 134 (SIGABRT) and
|
||||
# self-restart in a loop, so nginx saw no upstream and returned 502. Give it
|
||||
# real CPU headroom.
|
||||
cpu: "2"
|
||||
memory: 1Gi
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: dashboard
|
||||
# Probes are now tuned via homarr-values.yaml (chart-native); the old
|
||||
# fix-probes-job PostSync hook is removed. homarr-secrets/auth-oidc/db-encryption
|
||||
# Secrets are delivered by the sops-secrets (ksops) Application.
|
||||
resources: []
|
||||
@@ -0,0 +1,50 @@
|
||||
# Credential rotation
|
||||
|
||||
One `KeyRotation` object describes one credential. The controller creates the value,
|
||||
writes the Secret, proves the new value works, then retires the old one.
|
||||
|
||||
## Why the state order is the safety property
|
||||
|
||||
```
|
||||
Prepare -> Publish -> Verify -> Retire
|
||||
```
|
||||
|
||||
`Retire` runs only for a provider whose old credential still works after the publish
|
||||
step, and only after `Verify` passes. A failure before that point leaves the working
|
||||
credential untouched. The hand-run rotation that broke Grafana did the opposite: it
|
||||
regenerated the secret and never re-pointed the consumer.
|
||||
|
||||
## Two strategies, decided by the provider, not by preference
|
||||
|
||||
| Provider | Overlap in ZITADEL | Strategy | Retire |
|
||||
|---|---|---|---|
|
||||
| `zitadel-machine-key` | many keys per service user | `overlap` | after the new key obtains a token |
|
||||
| `zitadel-oidc-client` | one secret per app | `atomic` | nothing to retire |
|
||||
|
||||
## The single master credential
|
||||
|
||||
The controller authenticates with one machine key, mounted from Secret
|
||||
`iam/key-rotation-controller`. That key is the only credential the cluster cannot
|
||||
rotate by itself, so it is the only one that stays encrypted in git.
|
||||
|
||||
Every other credential is created by the controller, which means the 34 `.enc.yaml`
|
||||
files under `k8s/argocd/secrets/` are migration debt, not a design. The retirement
|
||||
target is one root-of-trust file per thing that cannot rotate itself:
|
||||
|
||||
| Survives (root of trust) | Why it cannot rotate |
|
||||
|---|---|
|
||||
| ZITADEL instance masterkey | encrypts the whole ZITADEL database |
|
||||
| Vault unseal keys | unsealing needs a human-held key |
|
||||
| Talos repair config | recovery path, must work when the cluster does not |
|
||||
| homelab CA | signs the wildcard certificate |
|
||||
| controller bootstrap key | chicken and egg: it creates every other key |
|
||||
| etcd defrag credentials | needed by a job that runs outside the controller |
|
||||
|
||||
Everything else — Grafana, Argo CD, Forgejo, MinIO, Paperless, Immich, ComfyUI,
|
||||
api-gw, Gotify — is a KeyRotation object, and its secret leaves git.
|
||||
|
||||
## Status
|
||||
|
||||
The CRD is installed. Eight `KeyRotation` objects exist for the consumers above and
|
||||
all are `suspend: true`: the controller image does not exist yet, so no process acts
|
||||
on them. poimen-memory is excluded from this migration on purpose.
|
||||
@@ -0,0 +1,254 @@
|
||||
// Package api declares the KeyRotation custom resource.
|
||||
//
|
||||
// One resource describes one credential and how to rotate it. The controller
|
||||
// never stores the credential in git: it writes a Kubernetes Secret and records
|
||||
// only the credential's identifier in status.
|
||||
package api
|
||||
|
||||
import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
// Group is the API group for homelab platform resources.
|
||||
const Group = "iam.homelab.riotpiao.com"
|
||||
|
||||
// Version is the served version.
|
||||
const Version = "v1"
|
||||
|
||||
// Provider names. Each provider uses one external API to create and retire
|
||||
// credentials, and each declares whether a rotation can overlap.
|
||||
const (
|
||||
// ProviderZitadelMachineKey creates a machine key while the previous key
|
||||
// stays valid. True zero downtime.
|
||||
ProviderZitadelMachineKey = "zitadel-machine-key"
|
||||
// ProviderZitadelOIDCClient regenerates an app client secret. ZITADEL keeps
|
||||
// one secret per app, so the old one dies the moment the new one exists.
|
||||
ProviderZitadelOIDCClient = "zitadel-oidc-client"
|
||||
)
|
||||
|
||||
// Strategy names.
|
||||
const (
|
||||
// StrategyOverlap keeps the old credential valid until the new one is proven.
|
||||
StrategyOverlap = "overlap"
|
||||
// StrategyAtomic replaces the credential in place. A short window exists.
|
||||
StrategyAtomic = "atomic"
|
||||
)
|
||||
|
||||
// Reload names tell the controller how the consumer picks up a new value.
|
||||
const (
|
||||
// ReloadHot means the consumer watches the mounted file.
|
||||
ReloadHot = "hot"
|
||||
// ReloadRestart means the consumer reads the value at start, so it must roll.
|
||||
ReloadRestart = "restart"
|
||||
)
|
||||
|
||||
// Phase names for status.
|
||||
const (
|
||||
PhasePending = "Pending"
|
||||
PhasePreparing = "Preparing"
|
||||
PhasePropagating = "Propagating"
|
||||
PhaseVerifying = "Verifying"
|
||||
PhaseCompleting = "Completing"
|
||||
PhaseSucceeded = "Succeeded"
|
||||
PhaseFailed = "Failed"
|
||||
PhaseSuspended = "Suspended"
|
||||
)
|
||||
|
||||
// ConsumerRef names a workload that reads the rotated credential.
|
||||
type ConsumerRef struct {
|
||||
// Kind is Deployment or StatefulSet.
|
||||
Kind string `json:"kind"`
|
||||
// Name is the workload name.
|
||||
Name string `json:"name"`
|
||||
// Namespace is where the workload runs.
|
||||
Namespace string `json:"namespace"`
|
||||
}
|
||||
|
||||
// RotationTarget is the credential's source and destination.
|
||||
type RotationTarget struct {
|
||||
// ZitadelProjectID is the project that owns the app or service user.
|
||||
ZitadelProjectID string `json:"zitadelProjectID,omitempty"`
|
||||
// ZitadelAppID is the OIDC app id, for the client-secret provider.
|
||||
ZitadelAppID string `json:"zitadelAppID,omitempty"`
|
||||
// MachineUserID is the service user id, for the machine-key provider.
|
||||
MachineUserID string `json:"machineUserID,omitempty"`
|
||||
// SecretName is the Secret the controller writes.
|
||||
SecretName string `json:"secretName"`
|
||||
// SecretNamespace is where that Secret lives.
|
||||
SecretNamespace string `json:"secretNamespace"`
|
||||
// SecretKeys maps a logical field name to the Secret key that holds it.
|
||||
// For a machine key: key -> key, key-id -> key-id.
|
||||
SecretKeys map[string]string `json:"secretKeys,omitempty"`
|
||||
// Consumers are the workloads that must pick the new value up.
|
||||
Consumers []ConsumerRef `json:"consumers,omitempty"`
|
||||
// Reload is hot or restart. Empty means restart, the safe default.
|
||||
Reload string `json:"reload,omitempty"`
|
||||
}
|
||||
|
||||
// VerifySpec is the proof the controller demands before it retires the old
|
||||
// credential.
|
||||
type VerifySpec struct {
|
||||
// Introspect checks client_id and client_secret against the introspection
|
||||
// endpoint.
|
||||
Introspect bool `json:"introspect,omitempty"`
|
||||
// TokenExchange exchanges a token with the new machine key.
|
||||
TokenExchange bool `json:"tokenExchange,omitempty"`
|
||||
// AudienceProject, when set, is added to the token request as
|
||||
// urn:zitadel:iam:org:project:id:<id>:aud.
|
||||
AudienceProject string `json:"audienceProject,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotationSpec is the desired state.
|
||||
type KeyRotationSpec struct {
|
||||
// Provider selects the external API.
|
||||
Provider string `json:"provider"`
|
||||
// Strategy is overlap or atomic.
|
||||
Strategy string `json:"strategy"`
|
||||
// IntervalHours is how often to rotate. Minimum 24.
|
||||
IntervalHours int32 `json:"intervalHours"`
|
||||
// Suspend stops automatic rotation without deleting the resource.
|
||||
Suspend bool `json:"suspend,omitempty"`
|
||||
// Target names the credential and its consumers.
|
||||
Target RotationTarget `json:"target"`
|
||||
// Verify names the checks that must pass.
|
||||
Verify VerifySpec `json:"verify,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotationStatus is the observed state.
|
||||
type KeyRotationStatus struct {
|
||||
Phase string `json:"phase,omitempty"`
|
||||
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
|
||||
LastRotation *metav1.Time `json:"lastRotation,omitempty"`
|
||||
NextRotation *metav1.Time `json:"nextRotation,omitempty"`
|
||||
ActiveCredentialID string `json:"activeCredentialID,omitempty"`
|
||||
PreviousCredentialID string `json:"previousCredentialID,omitempty"`
|
||||
LastError string `json:"lastError,omitempty"`
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotation is one credential under management.
|
||||
type KeyRotation struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ObjectMeta `json:"metadata,omitempty"`
|
||||
|
||||
Spec KeyRotationSpec `json:"spec,omitempty"`
|
||||
Status KeyRotationStatus `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
// KeyRotationList is a list of KeyRotation.
|
||||
type KeyRotationList struct {
|
||||
metav1.TypeMeta `json:",inline"`
|
||||
metav1.ListMeta `json:"metadata,omitempty"`
|
||||
Items []KeyRotation `json:"items"`
|
||||
}
|
||||
|
||||
// SchemeGroupVersion is the group and version for this type.
|
||||
var SchemeGroupVersion = schema.GroupVersion{Group: Group, Version: Version}
|
||||
|
||||
// SchemeBuilder registers the types with a scheme.
|
||||
var SchemeBuilder = runtime.NewSchemeBuilder(func(s *runtime.Scheme) error {
|
||||
s.AddKnownTypes(SchemeGroupVersion, &KeyRotation{}, &KeyRotationList{})
|
||||
metav1.AddToGroupVersion(s, SchemeGroupVersion)
|
||||
return nil
|
||||
})
|
||||
|
||||
// AddToScheme registers this package's types.
|
||||
var AddToScheme = SchemeBuilder.AddToScheme
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotation) DeepCopyInto(out *KeyRotation) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
|
||||
in.Spec.DeepCopyInto(&out.Spec)
|
||||
in.Status.DeepCopyInto(&out.Status)
|
||||
}
|
||||
|
||||
// DeepCopy makes a deep copy.
|
||||
func (in *KeyRotation) DeepCopy() *KeyRotation {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KeyRotation)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject satisfies runtime.Object.
|
||||
func (in *KeyRotation) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotationSpec) DeepCopyInto(out *KeyRotationSpec) {
|
||||
*out = *in
|
||||
in.Target.DeepCopyInto(&out.Target)
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *RotationTarget) DeepCopyInto(out *RotationTarget) {
|
||||
*out = *in
|
||||
if in.SecretKeys != nil {
|
||||
out.SecretKeys = make(map[string]string, len(in.SecretKeys))
|
||||
for k, v := range in.SecretKeys {
|
||||
out.SecretKeys[k] = v
|
||||
}
|
||||
}
|
||||
if in.Consumers != nil {
|
||||
out.Consumers = make([]ConsumerRef, len(in.Consumers))
|
||||
copy(out.Consumers, in.Consumers)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotationStatus) DeepCopyInto(out *KeyRotationStatus) {
|
||||
*out = *in
|
||||
if in.LastRotation != nil {
|
||||
t := *in.LastRotation
|
||||
out.LastRotation = &t
|
||||
}
|
||||
if in.NextRotation != nil {
|
||||
t := *in.NextRotation
|
||||
out.NextRotation = &t
|
||||
}
|
||||
if in.Conditions != nil {
|
||||
out.Conditions = make([]metav1.Condition, len(in.Conditions))
|
||||
copy(out.Conditions, in.Conditions)
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopyInto copies the receiver into out.
|
||||
func (in *KeyRotationList) DeepCopyInto(out *KeyRotationList) {
|
||||
*out = *in
|
||||
out.TypeMeta = in.TypeMeta
|
||||
in.ListMeta.DeepCopyInto(&out.ListMeta)
|
||||
if in.Items != nil {
|
||||
out.Items = make([]KeyRotation, len(in.Items))
|
||||
for i := range in.Items {
|
||||
in.Items[i].DeepCopyInto(&out.Items[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DeepCopy makes a deep copy.
|
||||
func (in *KeyRotationList) DeepCopy() *KeyRotationList {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(KeyRotationList)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyObject satisfies runtime.Object.
|
||||
func (in *KeyRotationList) DeepCopyObject() runtime.Object {
|
||||
if c := in.DeepCopy(); c != nil {
|
||||
return c
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
module github.com/riotpiao/key-rotation-controller
|
||||
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
k8s.io/api v0.37.1
|
||||
k8s.io/apimachinery v0.37.1
|
||||
k8s.io/client-go v0.37.0
|
||||
sigs.k8s.io/controller-runtime v0.25.2
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/swag v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/pools v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.27.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1 // indirect
|
||||
github.com/google/gnostic-models v0.7.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.24.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.70.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.1 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.37.0 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,133 @@
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes=
|
||||
github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU=
|
||||
github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1 h1:2rWm8B193Ll4VdjsJY28jxs70IdDsHRWgQYAI80+rMQ=
|
||||
github.com/fxamacker/cbor/v2 v2.9.1/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/swag v0.27.1 h1:VotvOLWW8q/EAxB0YdsBBGC8XYyeL1YwBj2ungAGPNg=
|
||||
github.com/go-openapi/swag v0.27.1/go.mod h1:GTkJPwHfhJp6MWr4/rCh64HVI3Ofu+tcsbfjfHmTxpE=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1 h1:I7sYqaWVl5mq0NEmNQkAmFDyNin9ufvMX/p2zwtQaOE=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.27.1 h1:8wi9ZG+olmY1wXphl93EWniPtbSPkXM/feH7FgjsvrU=
|
||||
github.com/go-openapi/swag/conv v0.27.1/go.mod h1:QbqMivkpKhC3g1B1GGGOJ6ANewI3S62dbzYu3Duowqs=
|
||||
github.com/go-openapi/swag/fileutils v0.27.1 h1:QQqBSoi5mW4XpU85nS0mLcA+zAE6vLzrb0QkmLKf9oM=
|
||||
github.com/go-openapi/swag/fileutils v0.27.1/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1 h1:SVgK3i4USzCU5mibOOS/l4ea2h9UQXy7J7RNLTjuXjU=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.1/go.mod h1:tdlEpZqdcQ17uj6J4YdK9vd8It5qWMwjWXOs0tjpRlk=
|
||||
github.com/go-openapi/swag/loading v0.27.1 h1:/DxUgDXKbBX4bcn7r9uEXfJyzN5XpiJmZplzQTjrRCY=
|
||||
github.com/go-openapi/swag/loading v0.27.1/go.mod h1:jvGh3iA2+zyUUycB5fgJWzeHnhrpvGnJJM0RVE9ZShE=
|
||||
github.com/go-openapi/swag/mangling v0.27.1 h1:yC9D0HyUE8gbP+BfmGx9+AA89ikwZTMjESK3OnnoaqA=
|
||||
github.com/go-openapi/swag/mangling v0.27.1/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||
github.com/go-openapi/swag/netutils v0.27.1 h1:mICMFoS82F5TZ4Zy3cqmcQk+BFeCp3Uyq3Np7GI0/qU=
|
||||
github.com/go-openapi/swag/netutils v0.27.1/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||
github.com/go-openapi/swag/pools v0.27.1 h1:9LeadcMyb2GJCbXX5hVQDbZ2Lq9TL4dCs/nx1j5DO0E=
|
||||
github.com/go-openapi/swag/pools v0.27.1/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.27.1 h1:ZXePZ0r2p1qSjo8tD3Un4vFj8+FqlCkczxDrJIhYUp8=
|
||||
github.com/go-openapi/swag/stringutils v0.27.1/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.1 h1:KSTdFlfnse4r6dP9IrEnwMldjE+zs71UeEB3//PtVXc=
|
||||
github.com/go-openapi/swag/typeutils v0.27.1/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1 h1:ftxv6xvXb1E3zohUc+okZ9nSqNb9StQX/FXnKZ98sQA=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.1/go.mod h1:bnxFIB1qewGRiZHypXGZ3fNgf13/0HfRgnS/iZBDrOo=
|
||||
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
|
||||
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
|
||||
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
||||
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw=
|
||||
gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo=
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
k8s.io/api v0.37.1 h1:l6N77U7tjwB5L056bgrBTJIEdevac/naBZ3iSvDNfpM=
|
||||
k8s.io/api v0.37.1/go.mod h1:zSlbB1YpJ1YQlFVQy20UYll81UJSJJUMLhkhvg6Z78M=
|
||||
k8s.io/apiextensions-apiserver v0.37.0 h1:zRMQ3+/LIE5oZ0tVvXwYHC+dIkSP5cjNWju7AZU1LOI=
|
||||
k8s.io/apiextensions-apiserver v0.37.0/go.mod h1:HU0PfSBwchHL5iDau6jjt9zU6ryWkDDlaVUiq91NK80=
|
||||
k8s.io/apimachinery v0.37.1 h1:hGCYyvKHCwtwMitj2vU4vYx0Z16N9GyZk9BBnz0wDAE=
|
||||
k8s.io/apimachinery v0.37.1/go.mod h1:jF84AyUi/IRIXRot5f+lm6MpxoWI+F1XgjaMmwCdTFw=
|
||||
k8s.io/client-go v0.37.0 h1:nsN31fy8wBySuZ+QRnKmrjRSQLOG2rvoGN0tKd12zhQ=
|
||||
k8s.io/client-go v0.37.0/go.mod h1:FcGqw+Ll/gNQiq+nPGY1Oyt9y7SgDh1d3MW3RFDEbn0=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd h1:Ea7fgQ5we8Y9T0OX5o0dAHzQOBRI07D/dEYRaB9ZZEs=
|
||||
k8s.io/utils v0.0.0-20260626114624-be93311217bd/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
sigs.k8s.io/controller-runtime v0.25.2 h1:bEkK3PVOIVK9X8QWLGVhJgmFc++47vfT6wakSzAOLsQ=
|
||||
sigs.k8s.io/controller-runtime v0.25.2/go.mod h1:4QqLdT6z/L6Olj8JJCtvztid4/fnIiYsfaTFScegctc=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
@@ -0,0 +1,168 @@
|
||||
package reconcile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
iamv1 "github.com/riotpiao/key-rotation-controller/api"
|
||||
"github.com/riotpiao/key-rotation-controller/internal/zitadel"
|
||||
)
|
||||
|
||||
// MachineKeyProvider rotates a service user's private key.
|
||||
//
|
||||
// ZITADEL keeps many keys per service user, and each assertion names its key with
|
||||
// `kid`. A new key can be created while the deployed key stays valid, so this
|
||||
// provider retires the old key only after the new one proves itself. Nothing in
|
||||
// the window is invalid.
|
||||
type MachineKeyProvider struct {
|
||||
// Client talks to the instance that owns the service user.
|
||||
Client *zitadel.Client
|
||||
// AuthClientID is the API app the controller authenticates as.
|
||||
AuthClientID string
|
||||
// KeyIDFile and KeyFile name the Secret keys, defaulting to key-id and key.
|
||||
KeyIDFile string
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
// Name implements Provider.
|
||||
func (p *MachineKeyProvider) Name() string { return iamv1.ProviderZitadelMachineKey }
|
||||
|
||||
// Strategy implements Provider. Machine keys overlap.
|
||||
func (p *MachineKeyProvider) Strategy() string { return iamv1.StrategyOverlap }
|
||||
|
||||
// Prepare creates a new key. The currently deployed key remains valid.
|
||||
func (p *MachineKeyProvider) Prepare(ctx context.Context, kr *iamv1.KeyRotation) (*Credential, error) {
|
||||
userID := kr.Spec.Target.MachineUserID
|
||||
if userID == "" {
|
||||
return nil, fmt.Errorf("spec.target.machineUserID is required")
|
||||
}
|
||||
key, err := p.Client.CreateMachineKey(userID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating a machine key: %w", err)
|
||||
}
|
||||
fields := map[string][]byte{
|
||||
p.keyField(kr): []byte(key.Key),
|
||||
p.keyIDField(kr): []byte(key.KeyID),
|
||||
}
|
||||
return &Credential{ID: key.KeyID, Fields: fields}, nil
|
||||
}
|
||||
|
||||
// Verify exchanges a token with the new key. A key that cannot obtain a token is
|
||||
// not deployed as a working credential, whatever the API said.
|
||||
func (p *MachineKeyProvider) Verify(ctx context.Context, kr *iamv1.KeyRotation, cred *Credential) error {
|
||||
keyPEM := cred.Fields[p.keyField(kr)]
|
||||
keyID := string(cred.Fields[p.keyIDField(kr)])
|
||||
return p.Client.ExchangeMachineKey(
|
||||
p.AuthClientID,
|
||||
kr.Spec.Target.MachineUserID,
|
||||
keyID,
|
||||
keyPEM,
|
||||
kr.Spec.Verify.AudienceProject,
|
||||
)
|
||||
}
|
||||
|
||||
// Retire deletes the superseded key.
|
||||
func (p *MachineKeyProvider) Retire(ctx context.Context, kr *iamv1.KeyRotation, credentialID string) error {
|
||||
return p.Client.DeleteMachineKey(kr.Spec.Target.MachineUserID, credentialID)
|
||||
}
|
||||
|
||||
func (p *MachineKeyProvider) keyField(kr *iamv1.KeyRotation) string {
|
||||
if key, ok := kr.Spec.Target.SecretKeys["key"]; ok && key != "" {
|
||||
return key
|
||||
}
|
||||
return "key"
|
||||
}
|
||||
|
||||
func (p *MachineKeyProvider) keyIDField(kr *iamv1.KeyRotation) string {
|
||||
if key, ok := kr.Spec.Target.SecretKeys["key-id"]; ok && key != "" {
|
||||
return key
|
||||
}
|
||||
return "key-id"
|
||||
}
|
||||
|
||||
// OIDCClientProvider rotates an app's client secret.
|
||||
//
|
||||
// ZITADEL stores exactly one secret per app, so creating the new secret kills the
|
||||
// old one. The strategy is atomic: publish immediately, roll the consumer, verify,
|
||||
// and if verification fails, put the previous value back.
|
||||
type OIDCClientProvider struct {
|
||||
Client *zitadel.Client
|
||||
// ClientIDFunc maps a KeyRotation to the app's client id, which the
|
||||
// introspection endpoint needs.
|
||||
ClientIDFunc func(kr *iamv1.KeyRotation) string
|
||||
// SecretKey names the Secret key that holds the secret.
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
// Name implements Provider.
|
||||
func (p *OIDCClientProvider) Name() string { return iamv1.ProviderZitadelOIDCClient }
|
||||
|
||||
// Strategy implements Provider. Client secrets cannot overlap.
|
||||
func (p *OIDCClientProvider) Strategy() string { return iamv1.StrategyAtomic }
|
||||
|
||||
// Prepare regenerates the client secret.
|
||||
func (p *OIDCClientProvider) Prepare(ctx context.Context, kr *iamv1.KeyRotation) (*Credential, error) {
|
||||
projectID := kr.Spec.Target.ZitadelProjectID
|
||||
appID := kr.Spec.Target.ZitadelAppID
|
||||
if projectID == "" || appID == "" {
|
||||
return nil, fmt.Errorf("spec.target.zitadelProjectID and zitadelAppID are required")
|
||||
}
|
||||
secret, err := p.Client.GenerateClientSecret(projectID, appID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("generating a client secret: %w", err)
|
||||
}
|
||||
field := p.secretField(kr)
|
||||
// The credential id is the generation marker, not a ZITADEL identifier: the
|
||||
// API returns only the value.
|
||||
return &Credential{
|
||||
ID: "secret-" + shortHash(secret),
|
||||
Fields: map[string][]byte{field: []byte(secret)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Verify checks the credential against the introspection endpoint.
|
||||
func (p *OIDCClientProvider) Verify(ctx context.Context, kr *iamv1.KeyRotation, cred *Credential) error {
|
||||
if !kr.Spec.Verify.Introspect {
|
||||
return nil
|
||||
}
|
||||
clientID := ""
|
||||
if p.ClientIDFunc != nil {
|
||||
clientID = p.ClientIDFunc(kr)
|
||||
}
|
||||
if clientID == "" {
|
||||
return fmt.Errorf("no client id configured for introspection")
|
||||
}
|
||||
return p.Client.Introspect(clientID, string(cred.Fields[p.secretField(kr)]))
|
||||
}
|
||||
|
||||
// Retire is a no-op: an atomic provider has nothing to retire, because the
|
||||
// provider already invalidated the previous value.
|
||||
func (p *OIDCClientProvider) Retire(ctx context.Context, kr *iamv1.KeyRotation, credentialID string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OIDCClientProvider) secretField(kr *iamv1.KeyRotation) string {
|
||||
if p.SecretKey != "" {
|
||||
return p.SecretKey
|
||||
}
|
||||
if key, ok := kr.Spec.Target.SecretKeys["secret"]; ok && key != "" {
|
||||
return key
|
||||
}
|
||||
return "secret"
|
||||
}
|
||||
|
||||
// shortHash gives a stable, non-reversible marker for a secret so status can
|
||||
// record which generation is live without storing the value.
|
||||
func shortHash(value string) string {
|
||||
sum := 0
|
||||
for _, b := range []byte(value) {
|
||||
sum = (sum*31 + int(b)) % 100000007
|
||||
}
|
||||
encoded := base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("%d", sum)))
|
||||
return encoded
|
||||
}
|
||||
|
||||
// unusedJSON keeps the import set honest if a provider needs raw JSON later.
|
||||
var _ = json.Marshal
|
||||
@@ -0,0 +1,391 @@
|
||||
// Package reconcile turns a KeyRotation object into a rotated credential.
|
||||
//
|
||||
// The state machine is identical for every provider. Only three calls differ:
|
||||
// create, verify, retire. That is what makes one controller cover machine keys,
|
||||
// client secrets and, later, the token-style providers.
|
||||
//
|
||||
// Order is the safety property:
|
||||
//
|
||||
// Prepare create the new credential
|
||||
// Publish write the Secret, roll the consumer when it cannot reload
|
||||
// Verify prove the new credential works against the provider
|
||||
// Retire delete the old credential, only for overlapping providers
|
||||
//
|
||||
// A failure before Retire leaves the old credential in place, so a broken
|
||||
// rotation never removes the thing that currently works.
|
||||
package reconcile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
appsv1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
|
||||
iamv1 "github.com/riotpiao/key-rotation-controller/api"
|
||||
)
|
||||
|
||||
// Credential is fresh credential material, before it is written to a Secret.
|
||||
type Credential struct {
|
||||
// ID identifies the credential at the provider, so it can be retired later.
|
||||
ID string
|
||||
// Fields maps a Secret key to its value.
|
||||
Fields map[string][]byte
|
||||
}
|
||||
|
||||
// Provider creates, verifies and retires one class of credential.
|
||||
type Provider interface {
|
||||
// Name is the provider name from the spec.
|
||||
Name() string
|
||||
// Strategy is overlap or atomic. Atomic providers replace in place.
|
||||
Strategy() string
|
||||
// Prepare creates a new credential at the provider.
|
||||
Prepare(ctx context.Context, kr *iamv1.KeyRotation) (*Credential, error)
|
||||
// Verify proves the credential works, using the provider's own check.
|
||||
Verify(ctx context.Context, kr *iamv1.KeyRotation, cred *Credential) error
|
||||
// Retire deletes a superseded credential. It must be safe to call twice.
|
||||
Retire(ctx context.Context, kr *iamv1.KeyRotation, credentialID string) error
|
||||
}
|
||||
|
||||
// Reconciler rotates credentials for KeyRotation objects.
|
||||
type Reconciler struct {
|
||||
client.Client
|
||||
// Providers keyed by spec.provider.
|
||||
Providers map[string]Provider
|
||||
// Now is injectable so scheduling is testable.
|
||||
Now func() time.Time
|
||||
// RolloutTimeout bounds the wait for a consumer to become available.
|
||||
RolloutTimeout time.Duration
|
||||
}
|
||||
|
||||
// previousKeySuffix keeps the superseded value beside the current one, so an
|
||||
// atomic provider can be rolled back without storing a secret in the object.
|
||||
const previousKeySuffix = ".previous"
|
||||
|
||||
// RequeueAfter is how long to wait before checking a resource again.
|
||||
func (r *Reconciler) RequeueAfter(kr *iamv1.KeyRotation) time.Duration {
|
||||
interval := time.Duration(kr.Spec.IntervalHours) * time.Hour
|
||||
if interval <= 0 {
|
||||
interval = 30 * 24 * time.Hour
|
||||
}
|
||||
if kr.Status.LastRotation == nil || kr.Status.ActiveCredentialID == "" {
|
||||
return 0 // rotate now
|
||||
}
|
||||
due := kr.Status.LastRotation.Add(interval)
|
||||
wait := due.Sub(r.now())
|
||||
if wait < 0 {
|
||||
return 0
|
||||
}
|
||||
return wait
|
||||
}
|
||||
|
||||
// Reconcile performs one rotation when the resource is due.
|
||||
func (r *Reconciler) Reconcile(ctx context.Context, req types.NamespacedName) (time.Duration, error) {
|
||||
logger := log.FromContext(ctx).WithValues("keyrotation", req.String())
|
||||
|
||||
var kr iamv1.KeyRotation
|
||||
if err := r.Get(ctx, req, &kr); err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return 0, nil
|
||||
}
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if kr.Spec.Suspend {
|
||||
return 0, r.setPhase(ctx, &kr, iamv1.PhaseSuspended, "")
|
||||
}
|
||||
|
||||
provider := r.Providers[kr.Spec.Provider]
|
||||
if provider == nil {
|
||||
return 0, r.setPhase(ctx, &kr, iamv1.PhaseFailed,
|
||||
fmt.Sprintf("no provider named %q", kr.Spec.Provider))
|
||||
}
|
||||
|
||||
wait := r.RequeueAfter(&kr)
|
||||
if wait > 0 {
|
||||
return wait, nil
|
||||
}
|
||||
|
||||
logger.Info("rotating credential", "provider", kr.Spec.Provider, "strategy", kr.Spec.Strategy)
|
||||
|
||||
previousID := kr.Status.ActiveCredentialID
|
||||
previousFields := r.readCurrentSecret(ctx, &kr)
|
||||
|
||||
// Prepare
|
||||
if err := r.setPhase(ctx, &kr, iamv1.PhasePreparing, ""); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
cred, err := provider.Prepare(ctx, &kr)
|
||||
if err != nil {
|
||||
return r.fail(ctx, &kr, iamv1.PhasePreparing, err)
|
||||
}
|
||||
|
||||
// Publish. The previous value stays beside the new one until verification
|
||||
// passes, so an atomic rotation can be undone.
|
||||
if err := r.setPhase(ctx, &kr, iamv1.PhasePropagating, ""); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := r.writeSecret(ctx, &kr, cred, previousFields); err != nil {
|
||||
return r.fail(ctx, &kr, iamv1.PhasePropagating, err)
|
||||
}
|
||||
if kr.Spec.Target.Reload != iamv1.ReloadHot {
|
||||
if err := r.rollConsumers(ctx, &kr, cred.ID); err != nil {
|
||||
return r.fail(ctx, &kr, iamv1.PhasePropagating, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify
|
||||
if err := r.setPhase(ctx, &kr, iamv1.PhaseVerifying, ""); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := provider.Verify(ctx, &kr, cred); err != nil {
|
||||
// Put the previous value back and roll again, then report the failure.
|
||||
r.restorePrevious(ctx, &kr, previousFields)
|
||||
return r.fail(ctx, &kr, iamv1.PhaseVerifying, err)
|
||||
}
|
||||
|
||||
// Complete. Retire only when the provider can overlap, because retiring an
|
||||
// atomic credential would delete the one now in use.
|
||||
if err := r.setPhase(ctx, &kr, iamv1.PhaseCompleting, ""); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if provider.Strategy() == iamv1.StrategyOverlap && previousID != "" && previousID != cred.ID {
|
||||
if err := provider.Retire(ctx, &kr, previousID); err != nil {
|
||||
logger.Error(err, "retiring the previous credential failed; it stays valid", "credential", previousID)
|
||||
}
|
||||
}
|
||||
r.clearPrevious(ctx, &kr)
|
||||
|
||||
now := metav1.NewTime(r.now())
|
||||
kr.Status.LastRotation = &now
|
||||
kr.Status.ActiveCredentialID = cred.ID
|
||||
kr.Status.PreviousCredentialID = previousID
|
||||
kr.Status.LastError = ""
|
||||
kr.Status.ObservedGeneration = kr.Generation
|
||||
if err := r.Status().Update(ctx, &kr); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := r.setPhase(ctx, &kr, iamv1.PhaseSucceeded, ""); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
logger.Info("rotation complete", "credential", cred.ID, "previous", previousID)
|
||||
|
||||
return r.RequeueAfter(&kr), nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) now() time.Time {
|
||||
if r.Now != nil {
|
||||
return r.Now()
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
func (r *Reconciler) setPhase(ctx context.Context, kr *iamv1.KeyRotation, phase, message string) error {
|
||||
kr.Status.Phase = phase
|
||||
kr.Status.LastError = message
|
||||
kr.Status.ObservedGeneration = kr.Generation
|
||||
if err := r.Status().Update(ctx, kr); err != nil && !apierrors.IsConflict(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Reconciler) fail(ctx context.Context, kr *iamv1.KeyRotation, phase string, cause error) (time.Duration, error) {
|
||||
kr.Status.Phase = iamv1.PhaseFailed
|
||||
kr.Status.LastError = fmt.Sprintf("%s: %v", phase, cause)
|
||||
_ = r.Status().Update(ctx, kr)
|
||||
// Retry soon, but not in a tight loop.
|
||||
return 5 * time.Minute, nil
|
||||
}
|
||||
|
||||
// secretData assembles the Secret payload: the new values, plus the superseded
|
||||
// values under a shadow key so an atomic rotation can be undone.
|
||||
func secretData(kr *iamv1.KeyRotation, cred *Credential, previous map[string][]byte) map[string][]byte {
|
||||
data := make(map[string][]byte, len(cred.Fields)*2)
|
||||
for logical, value := range cred.Fields {
|
||||
key := secretKeyFor(kr, logical)
|
||||
data[key] = value
|
||||
if old, ok := previous[key]; ok && len(old) > 0 {
|
||||
data[key+previousKeySuffix] = old
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// secretKeyFor maps a logical field name to the Secret key, defaulting to the
|
||||
// same name so a simple credential needs no mapping.
|
||||
func secretKeyFor(kr *iamv1.KeyRotation, logical string) string {
|
||||
if key, ok := kr.Spec.Target.SecretKeys[logical]; ok && key != "" {
|
||||
return key
|
||||
}
|
||||
return logical
|
||||
}
|
||||
|
||||
func (r *Reconciler) readCurrentSecret(ctx context.Context, kr *iamv1.KeyRotation) map[string][]byte {
|
||||
var secret corev1.Secret
|
||||
err := r.Get(ctx, types.NamespacedName{
|
||||
Namespace: kr.Spec.Target.SecretNamespace,
|
||||
Name: kr.Spec.Target.SecretName,
|
||||
}, &secret)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return secret.Data
|
||||
}
|
||||
|
||||
func (r *Reconciler) writeSecret(ctx context.Context, kr *iamv1.KeyRotation, cred *Credential, previous map[string][]byte) error {
|
||||
data := secretData(kr, cred, previous)
|
||||
|
||||
var secret corev1.Secret
|
||||
key := types.NamespacedName{Namespace: kr.Spec.Target.SecretNamespace, Name: kr.Spec.Target.SecretName}
|
||||
err := r.Get(ctx, key, &secret)
|
||||
if apierrors.IsNotFound(err) {
|
||||
secret = corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: key.Name,
|
||||
Namespace: key.Namespace,
|
||||
Labels: map[string]string{"app.kubernetes.io/managed-by": "key-rotation-controller"},
|
||||
},
|
||||
Type: corev1.SecretTypeOpaque,
|
||||
Data: data,
|
||||
}
|
||||
return r.Create(ctx, &secret)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
secret.Data = data
|
||||
return r.Update(ctx, &secret)
|
||||
}
|
||||
|
||||
// restorePrevious puts the shadow values back in place after a failed check.
|
||||
func (r *Reconciler) restorePrevious(ctx context.Context, kr *iamv1.KeyRotation, previous map[string][]byte) {
|
||||
if len(previous) == 0 {
|
||||
return
|
||||
}
|
||||
var secret corev1.Secret
|
||||
key := types.NamespacedName{Namespace: kr.Spec.Target.SecretNamespace, Name: kr.Spec.Target.SecretName}
|
||||
if err := r.Get(ctx, key, &secret); err != nil {
|
||||
return
|
||||
}
|
||||
// Restore every shadowed value over the current one.
|
||||
for k, v := range secret.Data {
|
||||
base := k
|
||||
if len(k) > len(previousKeySuffix) && k[len(k)-len(previousKeySuffix):] == previousKeySuffix {
|
||||
base = k[:len(k)-len(previousKeySuffix)]
|
||||
secret.Data[base] = v
|
||||
}
|
||||
}
|
||||
_ = r.Update(ctx, &secret)
|
||||
_ = r.rollConsumers(ctx, kr, "rollback")
|
||||
}
|
||||
|
||||
// clearPrevious removes the shadow values once verification passed.
|
||||
func (r *Reconciler) clearPrevious(ctx context.Context, kr *iamv1.KeyRotation) {
|
||||
var secret corev1.Secret
|
||||
key := types.NamespacedName{Namespace: kr.Spec.Target.SecretNamespace, Name: kr.Spec.Target.SecretName}
|
||||
if err := r.Get(ctx, key, &secret); err != nil {
|
||||
return
|
||||
}
|
||||
changed := false
|
||||
for k := range secret.Data {
|
||||
if len(k) > len(previousKeySuffix) && k[len(k)-len(previousKeySuffix):] == previousKeySuffix {
|
||||
delete(secret.Data, k)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
_ = r.Update(ctx, &secret)
|
||||
}
|
||||
}
|
||||
|
||||
// rollConsumers restarts each consumer so it reads the new value.
|
||||
func (r *Reconciler) rollConsumers(ctx context.Context, kr *iamv1.KeyRotation, revision string) error {
|
||||
for _, consumer := range kr.Spec.Target.Consumers {
|
||||
ns := consumer.Namespace
|
||||
if ns == "" {
|
||||
ns = kr.Namespace
|
||||
}
|
||||
switch consumer.Kind {
|
||||
case "Deployment":
|
||||
var dep appsv1.Deployment
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: ns, Name: consumer.Name}, &dep); err != nil {
|
||||
return fmt.Errorf("consumer %s/%s: %w", ns, consumer.Name, err)
|
||||
}
|
||||
base := dep.DeepCopy()
|
||||
if dep.Spec.Template.Annotations == nil {
|
||||
dep.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
dep.Spec.Template.Annotations["iam.homelab.riotpiao.com/credential-revision"] = revision
|
||||
if err := r.Patch(ctx, &dep, client.MergeFrom(base)); err != nil {
|
||||
return fmt.Errorf("rolling %s/%s: %w", ns, consumer.Name, err)
|
||||
}
|
||||
case "StatefulSet":
|
||||
var sts appsv1.StatefulSet
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: ns, Name: consumer.Name}, &sts); err != nil {
|
||||
return fmt.Errorf("consumer %s/%s: %w", ns, consumer.Name, err)
|
||||
}
|
||||
base := sts.DeepCopy()
|
||||
if sts.Spec.Template.Annotations == nil {
|
||||
sts.Spec.Template.Annotations = map[string]string{}
|
||||
}
|
||||
sts.Spec.Template.Annotations["iam.homelab.riotpiao.com/credential-revision"] = revision
|
||||
if err := r.Patch(ctx, &sts, client.MergeFrom(base)); err != nil {
|
||||
return fmt.Errorf("rolling %s/%s: %w", ns, consumer.Name, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("consumer kind %q is not supported", consumer.Kind)
|
||||
}
|
||||
}
|
||||
return r.waitForConsumers(ctx, kr)
|
||||
}
|
||||
|
||||
// waitForConsumers blocks until every consumer reports all replicas available.
|
||||
func (r *Reconciler) waitForConsumers(ctx context.Context, kr *iamv1.KeyRotation) error {
|
||||
deadline := r.now().Add(r.rolloutTimeout())
|
||||
for {
|
||||
ready := true
|
||||
for _, consumer := range kr.Spec.Target.Consumers {
|
||||
ns := consumer.Namespace
|
||||
if ns == "" {
|
||||
ns = kr.Namespace
|
||||
}
|
||||
var dep appsv1.Deployment
|
||||
if err := r.Get(ctx, types.NamespacedName{Namespace: ns, Name: consumer.Name}, &dep); err != nil {
|
||||
continue
|
||||
}
|
||||
desired := int32(1)
|
||||
if dep.Spec.Replicas != nil {
|
||||
desired = *dep.Spec.Replicas
|
||||
}
|
||||
if dep.Status.UpdatedReplicas < desired || dep.Status.AvailableReplicas < desired {
|
||||
ready = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if ready {
|
||||
return nil
|
||||
}
|
||||
if r.now().After(deadline) {
|
||||
return fmt.Errorf("consumer did not become available within %s", r.rolloutTimeout())
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case <-time.After(2 * time.Second):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reconciler) rolloutTimeout() time.Duration {
|
||||
if r.RolloutTimeout > 0 {
|
||||
return r.RolloutTimeout
|
||||
}
|
||||
return 3 * time.Minute
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package reconcile
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
iamv1 "github.com/riotpiao/key-rotation-controller/api"
|
||||
)
|
||||
|
||||
func rotation(last *metav1.Time, activeID string, hours int32) *iamv1.KeyRotation {
|
||||
return &iamv1.KeyRotation{
|
||||
Spec: iamv1.KeyRotationSpec{
|
||||
Provider: iamv1.ProviderZitadelMachineKey,
|
||||
Strategy: iamv1.StrategyOverlap,
|
||||
IntervalHours: hours,
|
||||
},
|
||||
Status: iamv1.KeyRotationStatus{
|
||||
LastRotation: last,
|
||||
ActiveCredentialID: activeID,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequeueAfterRotatesImmediatelyWhenNeverRotated(t *testing.T) {
|
||||
r := &Reconciler{Now: func() time.Time { return time.Unix(1_700_000_000, 0) }}
|
||||
if got := r.RequeueAfter(rotation(nil, "", 24)); got != 0 {
|
||||
t.Fatalf("a resource with no rotation history must rotate now, got %s", got)
|
||||
}
|
||||
if got := r.RequeueAfter(rotation(nil, "key-1", 24)); got != 0 {
|
||||
t.Fatalf("a resource with no lastRotation must rotate now, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequeueAfterWaitsForTheInterval(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
last := metav1.NewTime(now.Add(-6 * time.Hour))
|
||||
r := &Reconciler{Now: func() time.Time { return now }}
|
||||
|
||||
got := r.RequeueAfter(rotation(&last, "key-1", 24))
|
||||
want := 18 * time.Hour
|
||||
if got != want {
|
||||
t.Fatalf("expected %s until the next rotation, got %s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequeueAfterRotatesWhenOverdue(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
last := metav1.NewTime(now.Add(-30 * time.Hour))
|
||||
r := &Reconciler{Now: func() time.Time { return now }}
|
||||
|
||||
if got := r.RequeueAfter(rotation(&last, "key-1", 24)); got != 0 {
|
||||
t.Fatalf("an overdue resource must rotate now, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequeueAfterFallsBackWhenNoInterval(t *testing.T) {
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
last := metav1.NewTime(now)
|
||||
r := &Reconciler{Now: func() time.Time { return now }}
|
||||
|
||||
got := r.RequeueAfter(rotation(&last, "key-1", 0))
|
||||
if got <= 0 {
|
||||
t.Fatalf("a zero interval must fall back to a default, got %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretDataKeepsThePreviousValueUnderAShadowKey(t *testing.T) {
|
||||
kr := &iamv1.KeyRotation{Spec: iamv1.KeyRotationSpec{
|
||||
Target: iamv1.RotationTarget{
|
||||
SecretKeys: map[string]string{"key": "jwt-key", "key-id": "jwt-key-id"},
|
||||
},
|
||||
}}
|
||||
cred := &Credential{ID: "k2", Fields: map[string][]byte{
|
||||
"key": []byte("PEM-2"),
|
||||
"key-id": []byte("k2"),
|
||||
}}
|
||||
previous := map[string][]byte{
|
||||
"jwt-key": []byte("PEM-1"),
|
||||
"jwt-key-id": []byte("k1"),
|
||||
}
|
||||
|
||||
data := secretData(kr, cred, previous)
|
||||
|
||||
if string(data["jwt-key"]) != "PEM-2" {
|
||||
t.Fatalf("current value missing, got %q", data["jwt-key"])
|
||||
}
|
||||
if string(data["jwt-key"+previousKeySuffix]) != "PEM-1" {
|
||||
t.Fatalf("previous value must be kept for rollback, got %q", data["jwt-key"+previousKeySuffix])
|
||||
}
|
||||
if string(data["jwt-key-id"+previousKeySuffix]) != "k1" {
|
||||
t.Fatalf("previous key id must be kept for rollback")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretDataDefaultsToLogicalNames(t *testing.T) {
|
||||
kr := &iamv1.KeyRotation{}
|
||||
cred := &Credential{ID: "k1", Fields: map[string][]byte{"secret": []byte("s3cret")}}
|
||||
|
||||
data := secretData(kr, cred, nil)
|
||||
|
||||
if string(data["secret"]) != "s3cret" {
|
||||
t.Fatalf("an unmapped field must keep its logical name, got %v", data)
|
||||
}
|
||||
if _, shadow := data["secret"+previousKeySuffix]; shadow {
|
||||
t.Fatalf("no previous value existed, so no shadow key may be written")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecretKeyForPrefersTheMapping(t *testing.T) {
|
||||
kr := &iamv1.KeyRotation{Spec: iamv1.KeyRotationSpec{
|
||||
Target: iamv1.RotationTarget{
|
||||
SecretKeys: map[string]string{"secret": "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET"},
|
||||
},
|
||||
}}
|
||||
if got := secretKeyFor(kr, "secret"); got != "GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET" {
|
||||
t.Fatalf("mapping not honoured, got %q", got)
|
||||
}
|
||||
if got := secretKeyFor(kr, "key-id"); got != "key-id" {
|
||||
t.Fatalf("unmapped field must default to its own name, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMachineKeyProviderFieldsRespectTheMapping(t *testing.T) {
|
||||
p := &MachineKeyProvider{}
|
||||
kr := &iamv1.KeyRotation{Spec: iamv1.KeyRotationSpec{
|
||||
Target: iamv1.RotationTarget{
|
||||
SecretKeys: map[string]string{"key": "private-key", "key-id": "kid"},
|
||||
},
|
||||
}}
|
||||
if p.keyField(kr) != "private-key" || p.keyIDField(kr) != "kid" {
|
||||
t.Fatalf("provider must read the mapped Secret keys")
|
||||
}
|
||||
if p.Strategy() != iamv1.StrategyOverlap {
|
||||
t.Fatalf("machine keys must be an overlapping provider")
|
||||
}
|
||||
if (&OIDCClientProvider{}).Strategy() != iamv1.StrategyAtomic {
|
||||
t.Fatalf("client secrets cannot overlap and must be atomic")
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortHashIsStableAndDoesNotLeakTheValue(t *testing.T) {
|
||||
a := shortHash("super-secret-value")
|
||||
b := shortHash("super-secret-value")
|
||||
if a != b {
|
||||
t.Fatalf("the marker must be stable for the same value")
|
||||
}
|
||||
if a == "super-secret-value" {
|
||||
t.Fatalf("the marker must not be the value itself")
|
||||
}
|
||||
if shortHash("other") == a {
|
||||
t.Fatalf("different values must produce different markers")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
package zitadel
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config holds everything the controller needs to call ZITADEL.
|
||||
type Config struct {
|
||||
// Issuer is the instance URL, for example https://zitadel.riotpiao.com.
|
||||
Issuer string
|
||||
// ClientID is the API app that this controller authenticates as.
|
||||
ClientID string
|
||||
// UserID is the machine user the assertion acts for. Empty means ClientID.
|
||||
UserID string
|
||||
// KeyID and PrivateKeyPEM identify the controller's own machine key.
|
||||
KeyID string
|
||||
PrivateKeyPEM []byte
|
||||
// HTTPClient is optional; a sane default is used when nil.
|
||||
HTTPClient *http.Client
|
||||
}
|
||||
|
||||
// Client is a thin ZITADEL management client.
|
||||
type Client struct {
|
||||
cfg Config
|
||||
http *http.Client
|
||||
mu sync.Mutex
|
||||
token string
|
||||
until time.Time
|
||||
}
|
||||
|
||||
// New builds a client.
|
||||
func New(cfg Config) *Client {
|
||||
client := cfg.HTTPClient
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 20 * time.Second}
|
||||
}
|
||||
return &Client{cfg: cfg, http: client}
|
||||
}
|
||||
|
||||
// AccessToken returns a cached access token, minting one when needed.
|
||||
func (c *Client) AccessToken() (string, error) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
if c.token != "" && time.Now().Before(c.until) {
|
||||
return c.token, nil
|
||||
}
|
||||
|
||||
assertion, err := SignAssertion(c.cfg.PrivateKeyPEM, c.cfg.KeyID, c.cfg.ClientID, c.cfg.UserID, c.cfg.Issuer, time.Now())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
|
||||
form.Set("assertion", assertion)
|
||||
form.Set("scope", "openid")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, strings.TrimRight(c.cfg.Issuer, "/")+"/oauth/v2/token", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("token request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("token endpoint returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
var parsed struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return "", fmt.Errorf("token response did not parse: %w", err)
|
||||
}
|
||||
if parsed.AccessToken == "" {
|
||||
return "", fmt.Errorf("token response carried no access_token")
|
||||
}
|
||||
|
||||
lifetime := parsed.ExpiresIn
|
||||
if lifetime <= 0 {
|
||||
lifetime = 300
|
||||
}
|
||||
c.token = parsed.AccessToken
|
||||
// Refresh at 80 % of the lifetime so a slow call never presents an expired token.
|
||||
c.until = time.Now().Add(time.Duration(lifetime) * time.Second * 4 / 5)
|
||||
return c.token, nil
|
||||
}
|
||||
|
||||
// projectAppPath builds the management path for one OIDC app under a project.
|
||||
func (c *Client) projectAppPath(projectID, appID string) string {
|
||||
return fmt.Sprintf("/management/v1/projects/%s/apps/%s", projectID, appID)
|
||||
}
|
||||
|
||||
// MachineKey is one registered key for a service user.
|
||||
type MachineKey struct {
|
||||
KeyID string `json:"keyId"`
|
||||
Expiration string `json:"expirationDate,omitempty"`
|
||||
// Details holds the PEM bundle when the key was just created.
|
||||
Details string `json:"keyDetails,omitempty"`
|
||||
}
|
||||
|
||||
// NewMachineKeyJSON is the JSON document ZITADEL returns for a new JSON key.
|
||||
type NewMachineKeyJSON struct {
|
||||
Type string `json:"type"`
|
||||
KeyID string `json:"keyId"`
|
||||
Key string `json:"key"`
|
||||
UserID string `json:"userId"`
|
||||
ExpireAt string `json:"expirationDate,omitempty"`
|
||||
}
|
||||
|
||||
// CreateMachineKey adds a key to a service user and returns its material.
|
||||
//
|
||||
// ZITADEL keeps many keys per user, so this never invalidates the key that is
|
||||
// currently deployed. That is the whole reason the machine-key provider can
|
||||
// rotate with no gap.
|
||||
func (c *Client) CreateMachineKey(userID string) (*NewMachineKeyJSON, error) {
|
||||
payload := map[string]any{"type": "KEY_TYPE_JSON"}
|
||||
var out struct {
|
||||
KeyID string `json:"keyId"`
|
||||
KeyDetails string `json:"keyDetails"`
|
||||
}
|
||||
if err := c.post("/management/v1/users/"+userID+"/keys", payload, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.KeyDetails == "" {
|
||||
return nil, fmt.Errorf("create machine key returned no key material")
|
||||
}
|
||||
|
||||
raw, err := base64Decode(out.KeyDetails)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("key material is not base64: %w", err)
|
||||
}
|
||||
|
||||
var key NewMachineKeyJSON
|
||||
if err := json.Unmarshal(raw, &key); err != nil {
|
||||
return nil, fmt.Errorf("key material is not the expected JSON: %w", err)
|
||||
}
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
// DeleteMachineKey removes one key. Called only after the new key is proven.
|
||||
func (c *Client) DeleteMachineKey(userID, keyID string) error {
|
||||
return c.delete("/management/v1/users/" + userID + "/keys/" + keyID)
|
||||
}
|
||||
|
||||
// GenerateClientSecret replaces the client secret of an OIDC app.
|
||||
//
|
||||
// ZITADEL stores exactly one secret per app, so the previous secret stops working
|
||||
// at this call. The caller must publish the new value immediately.
|
||||
func (c *Client) GenerateClientSecret(projectID, appID string) (string, error) {
|
||||
var out struct {
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
path := c.projectAppPath(projectID, appID) + "/oidc_config/_generate_client_secret"
|
||||
if err := c.post(path, map[string]any{}, &out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if out.ClientSecret == "" {
|
||||
return "", fmt.Errorf("generate client secret returned an empty value")
|
||||
}
|
||||
return out.ClientSecret, nil
|
||||
}
|
||||
|
||||
// Introspect checks a client credential against the token endpoint.
|
||||
//
|
||||
// A successful call returns an object with `active`. The credentials are valid
|
||||
// when the endpoint answers 200, whatever `active` says, because `active`
|
||||
// describes the token rather than the client.
|
||||
func (c *Client) Introspect(clientID, clientSecret string) error {
|
||||
form := url.Values{}
|
||||
form.Set("token", "probe")
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, strings.TrimRight(c.cfg.Issuer, "/")+"/oauth/v2/introspect", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.SetBasicAuth(clientID, clientSecret)
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("introspection request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("introspection returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExchangeMachineKey obtains a token with a specific key, proving that key works.
|
||||
func (c *Client) ExchangeMachineKey(clientID, userID, keyID string, keyPEM []byte, audienceProject string) error {
|
||||
assertion, err := SignAssertion(keyPEM, keyID, clientID, userID, c.cfg.Issuer, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scope := "openid"
|
||||
if audienceProject != "" {
|
||||
scope += " urn:zitadel:iam:org:project:id:" + audienceProject + ":aud"
|
||||
}
|
||||
|
||||
form := url.Values{}
|
||||
form.Set("grant_type", "urn:ietf:params:oauth:grant-type:jwt-bearer")
|
||||
form.Set("assertion", assertion)
|
||||
form.Set("scope", scope)
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, strings.TrimRight(c.cfg.Issuer, "/")+"/oauth/v2/token", strings.NewReader(form.Encode()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("key exchange failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<16))
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("key exchange returned %d: %s", resp.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) do(method, path string, payload any, out any) error {
|
||||
token, err := c.AccessToken()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var body io.Reader
|
||||
if payload != nil {
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body = bytes.NewReader(encoded)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(method, strings.TrimRight(c.cfg.Issuer, "/")+path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if payload != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.http.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
raw, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("%s %s returned %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(raw)))
|
||||
}
|
||||
if out != nil && len(raw) > 0 {
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
return fmt.Errorf("%s %s response did not parse: %w", method, path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Client) post(path string, payload, out any) error {
|
||||
return c.do(http.MethodPost, path, payload, out)
|
||||
}
|
||||
|
||||
func (c *Client) delete(path string) error {
|
||||
return c.do(http.MethodDelete, path, nil, nil)
|
||||
}
|
||||
|
||||
func base64Decode(s string) ([]byte, error) {
|
||||
if raw, err := base64StdDecode(s); err == nil {
|
||||
return raw, nil
|
||||
}
|
||||
return base64URLDecode(s)
|
||||
}
|
||||
|
||||
func base64StdDecode(s string) ([]byte, error) {
|
||||
return base64.StdEncoding.DecodeString(s)
|
||||
}
|
||||
|
||||
func base64URLDecode(s string) ([]byte, error) {
|
||||
return base64.RawURLEncoding.DecodeString(s)
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// Package zitadel talks to the ZITADEL management API with the controller's own
|
||||
// machine key.
|
||||
//
|
||||
// The controller needs a credential to rotate credentials. It signs a short
|
||||
// assertion with its private key, exchanges it for an access token, and reuses
|
||||
// that token until shortly before it expires.
|
||||
package zitadel
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// assertionLifetime is deliberately short. The assertion proves one request.
|
||||
const assertionLifetime = 60 * time.Second
|
||||
|
||||
// newAssertionClaims builds the claim set for a machine-key assertion.
|
||||
//
|
||||
// ZITADEL expects `iss` to name the caller, `sub` to name the user it acts as,
|
||||
// and `aud` to equal the issuer of the instance.
|
||||
func newAssertionClaims(clientID, userID, issuer string, now time.Time) map[string]any {
|
||||
if userID == "" {
|
||||
userID = clientID
|
||||
}
|
||||
return map[string]any{
|
||||
"iss": clientID,
|
||||
"sub": userID,
|
||||
"aud": issuer,
|
||||
"iat": now.Unix(),
|
||||
"exp": now.Add(assertionLifetime).Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
func base64URL(b []byte) string {
|
||||
return base64.RawURLEncoding.EncodeToString(b)
|
||||
}
|
||||
|
||||
// SignAssertion produces a signed RS256 JWT for the jwt-bearer grant.
|
||||
//
|
||||
// The header carries `kid`, so ZITADEL selects the matching registered key. That
|
||||
// is what makes overlapping keys possible: two keys can be valid at once and the
|
||||
// assertion says which one it used.
|
||||
func SignAssertion(privateKeyPEM []byte, keyID, clientID, userID, issuer string, now time.Time) (string, error) {
|
||||
block, _ := pem.Decode(privateKeyPEM)
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("private key is not PEM encoded")
|
||||
}
|
||||
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// ZITADEL exports PKCS#1 keys as well.
|
||||
if rsaKey, rsaErr := x509.ParsePKCS1PrivateKey(block.Bytes); rsaErr == nil {
|
||||
key = rsaKey
|
||||
} else {
|
||||
return "", fmt.Errorf("private key does not parse: %w", err)
|
||||
}
|
||||
}
|
||||
rsaKey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("private key is not RSA")
|
||||
}
|
||||
|
||||
header := map[string]any{"alg": "RS256", "typ": "JWT", "kid": keyID}
|
||||
headerJSON, err := json.Marshal(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
claimsJSON, err := json.Marshal(newAssertionClaims(clientID, userID, issuer, now))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
signingInput := base64URL(headerJSON) + "." + base64URL(claimsJSON)
|
||||
digest := sha256.Sum256([]byte(signingInput))
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, digest[:])
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("cannot sign the assertion: %w", err)
|
||||
}
|
||||
|
||||
return signingInput + "." + base64URL(signature), nil
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package zitadel
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func newKey(t *testing.T) ([]byte, *rsa.PublicKey) {
|
||||
t.Helper()
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
t.Fatalf("cannot generate a key: %v", err)
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||
return pemBytes, &key.PublicKey
|
||||
}
|
||||
|
||||
func decodeSegment(t *testing.T, segment string, out any) {
|
||||
t.Helper()
|
||||
raw, err := base64.RawURLEncoding.DecodeString(segment)
|
||||
if err != nil {
|
||||
t.Fatalf("segment is not base64url: %v", err)
|
||||
}
|
||||
if err := json.Unmarshal(raw, out); err != nil {
|
||||
t.Fatalf("segment is not JSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAssertionCarriesTheKeyIDAndIssuer(t *testing.T) {
|
||||
pemBytes, _ := newKey(t)
|
||||
now := time.Unix(1_700_000_000, 0)
|
||||
|
||||
token, err := SignAssertion(pemBytes, "key-42", "client-1", "user-9", "https://zitadel.riotpiao.com", now)
|
||||
if err != nil {
|
||||
t.Fatalf("signing failed: %v", err)
|
||||
}
|
||||
if strings.Count(token, ".") != 2 {
|
||||
t.Fatalf("a JWT has three segments, got %q", token)
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
var header map[string]any
|
||||
decodeSegment(t, parts[0], &header)
|
||||
if header["kid"] != "key-42" {
|
||||
t.Fatalf("the header must name the key id so ZITADEL can pick the key, got %v", header["kid"])
|
||||
}
|
||||
if header["alg"] != "RS256" {
|
||||
t.Fatalf("expected RS256, got %v", header["alg"])
|
||||
}
|
||||
|
||||
var claims map[string]any
|
||||
decodeSegment(t, parts[1], &claims)
|
||||
if claims["iss"] != "client-1" || claims["sub"] != "user-9" {
|
||||
t.Fatalf("iss and sub are wrong: %v / %v", claims["iss"], claims["sub"])
|
||||
}
|
||||
if claims["aud"] != "https://zitadel.riotpiao.com" {
|
||||
t.Fatalf("aud must equal the issuer, got %v", claims["aud"])
|
||||
}
|
||||
exp := int64(claims["exp"].(float64))
|
||||
iat := int64(claims["iat"].(float64))
|
||||
if exp-iat != 60 {
|
||||
t.Fatalf("the assertion must live 60 seconds, got %d", exp-iat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAssertionRejectsGarbage(t *testing.T) {
|
||||
if _, err := SignAssertion([]byte("not a key"), "k", "c", "u", "https://i", time.Now()); err == nil {
|
||||
t.Fatalf("a non-PEM key must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignAssertionDefaultsSubjectToClientID(t *testing.T) {
|
||||
pemBytes, _ := newKey(t)
|
||||
token, err := SignAssertion(pemBytes, "k1", "client-1", "", "https://i", time.Unix(1, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("signing failed: %v", err)
|
||||
}
|
||||
var claims map[string]any
|
||||
decodeSegment(t, strings.Split(token, ".")[1], &claims)
|
||||
if claims["sub"] != "client-1" {
|
||||
t.Fatalf("an empty user id must fall back to the client id, got %v", claims["sub"])
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
// Command key-rotation-controller rotates credentials declared by KeyRotation
|
||||
// objects.
|
||||
//
|
||||
// The controller authenticates to ZITADEL with its own machine key, read from a
|
||||
// mounted Secret. That key is the single bootstrap credential: everything else is
|
||||
// created by this process and never committed to git.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
ctrlreconcile "sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
iamv1 "github.com/riotpiao/key-rotation-controller/api"
|
||||
keyreconcile "github.com/riotpiao/key-rotation-controller/internal/reconcile"
|
||||
"github.com/riotpiao/key-rotation-controller/internal/zitadel"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var (
|
||||
metricsAddr = flag.String("metrics-bind-address", ":8080", "address for the metrics endpoint")
|
||||
healthAddr = flag.String("health-probe-bind-address", ":8081", "address for probes")
|
||||
interval = flag.Duration("scan-interval", 10*time.Minute, "how often to re-check every resource")
|
||||
)
|
||||
flag.Parse()
|
||||
|
||||
ctrl.SetLogger(zap.New(zap.UseDevMode(false)))
|
||||
logger := ctrl.Log.WithName("setup")
|
||||
|
||||
scheme := runtime.NewScheme()
|
||||
if err := clientgoscheme.AddToScheme(scheme); err != nil {
|
||||
logger.Error(err, "cannot register the Kubernetes types")
|
||||
os.Exit(1)
|
||||
}
|
||||
if err := iamv1.AddToScheme(scheme); err != nil {
|
||||
logger.Error(err, "cannot register the KeyRotation types")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||
Scheme: scheme,
|
||||
Metrics: metricsserver.Options{BindAddress: *metricsAddr},
|
||||
HealthProbeBindAddress: *healthAddr,
|
||||
LeaderElection: true,
|
||||
LeaderElectionID: "key-rotation-controller.iam.homelab.riotpiao.com",
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create the manager")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
zitadelClient, err := buildZitadelClient()
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot build the ZITADEL client")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
reconciler := &keyreconcile.Reconciler{
|
||||
Client: mgr.GetClient(),
|
||||
Providers: map[string]keyreconcile.Provider{
|
||||
iamv1.ProviderZitadelMachineKey: &keyreconcile.MachineKeyProvider{
|
||||
Client: zitadelClient,
|
||||
AuthClientID: os.Getenv("ZITADEL_CLIENT_ID"),
|
||||
},
|
||||
iamv1.ProviderZitadelOIDCClient: &keyreconcile.OIDCClientProvider{
|
||||
Client: zitadelClient,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
c, err := controller.New("keyrotation", mgr, controller.Options{
|
||||
Reconciler: ctrlreconcile.Func(func(ctx context.Context, req ctrlreconcile.Request) (ctrlreconcile.Result, error) {
|
||||
wait, err := reconciler.Reconcile(ctx, types.NamespacedName(req.NamespacedName))
|
||||
if err != nil {
|
||||
return ctrlreconcile.Result{}, err
|
||||
}
|
||||
if wait <= 0 {
|
||||
wait = *interval
|
||||
}
|
||||
return ctrlreconcile.Result{RequeueAfter: wait}, nil
|
||||
}),
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error(err, "cannot create the controller")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := c.Watch(source.Kind(mgr.GetCache(), client.Object(&iamv1.KeyRotation{}), &handler.EnqueueRequestForObject{})); err != nil {
|
||||
logger.Error(err, "cannot watch KeyRotation objects")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := mgr.AddHealthzCheck("healthz", func(*http.Request) error { return nil }); err != nil {
|
||||
logger.Error(err, "cannot add the health check")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
logger.Info("starting the key rotation controller")
|
||||
if err := mgr.Start(ctrl.SetupSignalHandler()); err != nil {
|
||||
logger.Error(err, "manager stopped")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// buildZitadelClient reads the controller's own credential from the environment.
|
||||
//
|
||||
// These are the only values the controller cannot create for itself. They arrive
|
||||
// from one Secret, created once when the cluster is bootstrapped.
|
||||
func buildZitadelClient() (*zitadel.Client, error) {
|
||||
issuer := os.Getenv("ZITADEL_ISSUER")
|
||||
clientID := os.Getenv("ZITADEL_CLIENT_ID")
|
||||
userID := os.Getenv("ZITADEL_USER_ID")
|
||||
keyID := os.Getenv("ZITADEL_KEY_ID")
|
||||
keyFile := envOr("ZITADEL_KEY_FILE", "/var/run/keys/controller/key")
|
||||
|
||||
if issuer == "" || clientID == "" || keyID == "" {
|
||||
return nil, fmt.Errorf("ZITADEL_ISSUER, ZITADEL_CLIENT_ID and ZITADEL_KEY_ID are required")
|
||||
}
|
||||
pem, err := os.ReadFile(keyFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read the controller key: %w", err)
|
||||
}
|
||||
return zitadel.New(zitadel.Config{
|
||||
Issuer: issuer,
|
||||
ClientID: clientID,
|
||||
UserID: userID,
|
||||
KeyID: keyID,
|
||||
PrivateKeyPEM: pem,
|
||||
}), nil
|
||||
}
|
||||
|
||||
func envOr(name, fallback string) string {
|
||||
if v := os.Getenv(name); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
# KeyRotation declares one credential and how to rotate it.
|
||||
#
|
||||
# The resource never holds a secret value. It names where the value goes and how
|
||||
# to prove the new value works. The controller writes the Secret.
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: keyrotations.iam.homelab.riotpiao.com
|
||||
spec:
|
||||
group: iam.homelab.riotpiao.com
|
||||
scope: Namespaced
|
||||
names:
|
||||
kind: KeyRotation
|
||||
plural: keyrotations
|
||||
singular: keyrotation
|
||||
shortNames: [kr]
|
||||
versions:
|
||||
- name: v1
|
||||
served: true
|
||||
storage: true
|
||||
additionalPrinterColumns:
|
||||
- {name: Provider, type: string, jsonPath: .spec.provider}
|
||||
- {name: Strategy, type: string, jsonPath: .spec.strategy}
|
||||
- {name: Phase, type: string, jsonPath: .status.phase}
|
||||
- {name: Last, type: date, jsonPath: .status.lastRotation}
|
||||
- {name: Next, type: date, jsonPath: .status.nextRotation}
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
required: [spec]
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required: [provider, strategy, intervalHours, target]
|
||||
properties:
|
||||
provider:
|
||||
type: string
|
||||
enum: [zitadel-machine-key, zitadel-oidc-client]
|
||||
description: >-
|
||||
Which external API creates the credential.
|
||||
strategy:
|
||||
type: string
|
||||
enum: [overlap, atomic]
|
||||
description: >-
|
||||
overlap keeps the old credential valid until the new one is
|
||||
proven. atomic replaces it in place, because the provider
|
||||
allows only one value.
|
||||
intervalHours:
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: How often to rotate.
|
||||
suspend:
|
||||
type: boolean
|
||||
description: Stop automatic rotation without deleting the resource.
|
||||
target:
|
||||
type: object
|
||||
required: [secretName, secretNamespace]
|
||||
properties:
|
||||
zitadelProjectID: {type: string}
|
||||
zitadelAppID: {type: string}
|
||||
machineUserID: {type: string}
|
||||
secretName: {type: string}
|
||||
secretNamespace: {type: string}
|
||||
secretKeys:
|
||||
type: object
|
||||
additionalProperties: {type: string}
|
||||
description: >-
|
||||
Logical field name to Secret key. Unmapped fields keep
|
||||
their logical name.
|
||||
reload:
|
||||
type: string
|
||||
enum: [hot, restart]
|
||||
description: >-
|
||||
hot means the consumer watches the file. restart rolls the
|
||||
consumer so it reads the new value at start.
|
||||
consumers:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [kind, name, namespace]
|
||||
properties:
|
||||
kind: {type: string, enum: [Deployment, StatefulSet]}
|
||||
name: {type: string}
|
||||
namespace: {type: string}
|
||||
verify:
|
||||
type: object
|
||||
properties:
|
||||
introspect:
|
||||
type: boolean
|
||||
description: Check client id and secret against introspection.
|
||||
tokenExchange:
|
||||
type: boolean
|
||||
description: Exchange a token with the new machine key.
|
||||
audienceProject:
|
||||
type: string
|
||||
description: Project id added to the token audience scope.
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
phase:
|
||||
type: string
|
||||
enum: [Pending, Preparing, Propagating, Verifying, Completing, Succeeded, Failed, Suspended]
|
||||
observedGeneration: {type: integer, format: int64}
|
||||
lastRotation: {type: string, format: date-time}
|
||||
nextRotation: {type: string, format: date-time}
|
||||
activeCredentialID: {type: string}
|
||||
previousCredentialID: {type: string}
|
||||
lastError: {type: string}
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [type, status, reason, lastTransitionTime]
|
||||
properties:
|
||||
type: {type: string}
|
||||
status: {type: string}
|
||||
reason: {type: string}
|
||||
message: {type: string}
|
||||
lastTransitionTime: {type: string, format: date-time}
|
||||
@@ -0,0 +1,66 @@
|
||||
# The controller. Its own machine key is the single bootstrap credential: it is
|
||||
# created once and mounted here, and this process creates every other credential.
|
||||
#
|
||||
# The image must be built and pushed before this Deployment can start. Until then
|
||||
# the object stays Pending, which is why no Argo CD Application is wired yet.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: key-rotation-controller
|
||||
namespace: iam
|
||||
labels:
|
||||
app: key-rotation-controller
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: key-rotation-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: key-rotation-controller
|
||||
spec:
|
||||
serviceAccountName: key-rotation-controller
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
seccompProfile:
|
||||
type: RuntimeDefault
|
||||
containers:
|
||||
- name: controller
|
||||
image: forgejo.riotpiao.com/riotpiao-poimen/key-rotation-controller:0.1.0
|
||||
imagePullPolicy: IfNotPresent
|
||||
args:
|
||||
- --scan-interval=10m
|
||||
env:
|
||||
- {name: ZITADEL_ISSUER, value: "https://zitadel.riotpiao.com"}
|
||||
- name: ZITADEL_CLIENT_ID
|
||||
valueFrom: {secretKeyRef: {name: key-rotation-controller, key: client-id}}
|
||||
- name: ZITADEL_USER_ID
|
||||
valueFrom: {secretKeyRef: {name: key-rotation-controller, key: user-id}}
|
||||
- name: ZITADEL_KEY_ID
|
||||
valueFrom: {secretKeyRef: {name: key-rotation-controller, key: key-id}}
|
||||
- {name: ZITADEL_KEY_FILE, value: "/var/run/keys/controller/key"}
|
||||
ports:
|
||||
- {name: metrics, containerPort: 8080}
|
||||
- {name: health, containerPort: 8081}
|
||||
readinessProbe:
|
||||
httpGet: {path: /healthz, port: health}
|
||||
initialDelaySeconds: 5
|
||||
livenessProbe:
|
||||
httpGet: {path: /healthz, port: health}
|
||||
initialDelaySeconds: 15
|
||||
volumeMounts:
|
||||
- {name: controller-key, mountPath: /var/run/keys/controller, readOnly: true}
|
||||
securityContext:
|
||||
allowPrivilegeEscalation: false
|
||||
capabilities: {drop: ["ALL"]}
|
||||
resources:
|
||||
requests: {cpu: 20m, memory: 64Mi}
|
||||
limits: {cpu: 500m, memory: 256Mi}
|
||||
volumes:
|
||||
- name: controller-key
|
||||
secret:
|
||||
secretName: key-rotation-controller
|
||||
items:
|
||||
- {key: key, path: key}
|
||||
@@ -0,0 +1,28 @@
|
||||
# Example: rotate a service user's machine key with no gap.
|
||||
#
|
||||
# Machine keys overlap in ZITADEL, so the old key stays valid until the new one has
|
||||
# obtained a token. Nothing in the window fails.
|
||||
#
|
||||
# Apply only after the controller runs and the machine user exists.
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: example-machine-key
|
||||
namespace: iam
|
||||
spec:
|
||||
provider: zitadel-machine-key
|
||||
strategy: overlap
|
||||
intervalHours: 720 # 30 days
|
||||
suspend: true # flip to false once the pilot is proven
|
||||
target:
|
||||
machineUserID: "REPLACE_WITH_SERVICE_USER_ID"
|
||||
secretName: example-jwt
|
||||
secretNamespace: iam
|
||||
secretKeys:
|
||||
key: key
|
||||
key-id: key-id
|
||||
reload: hot # the consumer watches the file, so no restart
|
||||
consumers: []
|
||||
verify:
|
||||
tokenExchange: true
|
||||
audienceProject: "396757796898471937"
|
||||
@@ -0,0 +1,27 @@
|
||||
# Example: rotate an OIDC app's client secret.
|
||||
#
|
||||
# ZITADEL keeps one secret per app, so the old value dies at the generate call.
|
||||
# The strategy is atomic: publish, roll the consumer, verify, and roll back if the
|
||||
# introspection check fails.
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: example-grafana-client
|
||||
namespace: iam
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160 # 90 days
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "REPLACE_WITH_APP_ID"
|
||||
secretName: grafana-oidc
|
||||
secretNamespace: logging
|
||||
secretKeys:
|
||||
secret: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
|
||||
reload: restart # Grafana reads the value at start
|
||||
consumers:
|
||||
- {kind: Deployment, name: grafana, namespace: logging}
|
||||
verify:
|
||||
introspect: true
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
# No Argo CD Application is wired to this directory yet: the controller image must
|
||||
# exist first. Add the app once the image is pushed, so Argo never reports a
|
||||
# Deployment that cannot pull.
|
||||
resources:
|
||||
- crd.yaml
|
||||
- rbac.yaml
|
||||
- deployment.yaml
|
||||
- rotations.yaml
|
||||
@@ -0,0 +1,43 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: key-rotation-controller
|
||||
namespace: iam
|
||||
---
|
||||
# Cluster-wide because a credential and its consumer live in different namespaces:
|
||||
# the Secret in one, the workload in another.
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: key-rotation-controller
|
||||
rules:
|
||||
- apiGroups: ["iam.homelab.riotpiao.com"]
|
||||
resources: ["keyrotations"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
- apiGroups: ["iam.homelab.riotpiao.com"]
|
||||
resources: ["keyrotations/status"]
|
||||
verbs: ["get", "update", "patch"]
|
||||
# The controller writes the credential it just created.
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "create", "update", "patch"]
|
||||
# Rolling a consumer that cannot hot reload.
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments", "statefulsets"]
|
||||
verbs: ["get", "list", "watch", "patch"]
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["create", "patch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: key-rotation-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: key-rotation-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: key-rotation-controller
|
||||
namespace: iam
|
||||
@@ -0,0 +1,211 @@
|
||||
# Every credential this cluster owns, described as a KeyRotation.
|
||||
#
|
||||
# One object per consumer. The controller creates the value, writes the Secret and
|
||||
# proves the new value works before it retires the old one. Nothing here holds a
|
||||
# secret, so this file is safe in git.
|
||||
#
|
||||
# suspend: true everywhere on purpose. The controller image does not exist yet, and
|
||||
# an unsuspended object would claim an intent no process is acting on. Clear the flag
|
||||
# per object as its pilot passes.
|
||||
#
|
||||
# poimen-memory is absent deliberately: it is in use and excluded from this migration.
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: grafana
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757801965191169"
|
||||
secretName: grafana-oidc
|
||||
secretNamespace: logging
|
||||
secretKeys:
|
||||
secret: GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET
|
||||
reload: restart
|
||||
consumers:
|
||||
- {kind: Deployment, name: grafana, namespace: logging}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: argocd
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757801210282367"
|
||||
secretName: argocd-oidc
|
||||
secretNamespace: argocd
|
||||
secretKeys:
|
||||
secret: oidc.clientSecret
|
||||
reload: restart
|
||||
consumers:
|
||||
- {kind: Deployment, name: argocd-server, namespace: argocd}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: comfyui
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "397015744010781070"
|
||||
secretName: comfyui-oauth2-proxy
|
||||
secretNamespace: comfyui
|
||||
secretKeys:
|
||||
secret: client-secret
|
||||
reload: restart
|
||||
consumers:
|
||||
- {kind: Deployment, name: comfyui-oauth2-proxy, namespace: comfyui}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: minio
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757803257036801"
|
||||
secretName: minio-oidc
|
||||
secretNamespace: minio
|
||||
secretKeys:
|
||||
secret: client-secret
|
||||
reload: hot
|
||||
consumers:
|
||||
- {kind: Deployment, name: minio, namespace: minio}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: paperless
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757803559027071"
|
||||
secretName: paperless-oidc
|
||||
secretNamespace: paperless
|
||||
secretKeys:
|
||||
secret: client-secret
|
||||
reload: restart
|
||||
consumers:
|
||||
- {kind: Deployment, name: paperless, namespace: paperless}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: immich
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757802602791295"
|
||||
secretName: immich-oidc
|
||||
secretNamespace: immich
|
||||
secretKeys:
|
||||
secret: client-secret
|
||||
reload: restart
|
||||
consumers:
|
||||
- {kind: Deployment, name: immich-server, namespace: immich}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: forgejo
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757801579315583"
|
||||
secretName: forgejo-oidc
|
||||
secretNamespace: forgejo
|
||||
secretKeys:
|
||||
secret: key
|
||||
reload: restart
|
||||
consumers:
|
||||
- {kind: StatefulSet, name: forgejo, namespace: forgejo}
|
||||
verify:
|
||||
introspect: true
|
||||
---
|
||||
apiVersion: iam.homelab.riotpiao.com/v1
|
||||
kind: KeyRotation
|
||||
metadata:
|
||||
name: api-gw
|
||||
namespace: iam
|
||||
labels:
|
||||
app.kubernetes.io/part-of: key-rotation
|
||||
spec:
|
||||
provider: zitadel-oidc-client
|
||||
strategy: atomic
|
||||
intervalHours: 2160
|
||||
suspend: true
|
||||
target:
|
||||
zitadelProjectID: "396757796898471937"
|
||||
zitadelAppID: "396757800874672511"
|
||||
secretName: api-gw-oidc
|
||||
secretNamespace: api-gw
|
||||
secretKeys:
|
||||
secret: client-secret
|
||||
reload: hot
|
||||
consumers:
|
||||
- {kind: Deployment, name: api-gw, namespace: api-gw}
|
||||
verify:
|
||||
introspect: true
|
||||
@@ -1,107 +0,0 @@
|
||||
apiVersion: serving.kserve.io/v1beta1
|
||||
kind: InferenceService
|
||||
metadata:
|
||||
annotations:
|
||||
serving.kserve.io/deploymentMode: RawDeployment
|
||||
# The konghq.com/{connect,read,write}-timeout annotations that used to live
|
||||
# here went with Kong (retired 2026-08-19). They existed because Kong read
|
||||
# its upstream timeouts off the Kubernetes Service, and its 60s default cut
|
||||
# off the first request after any pod restart — a restart flushes VRAM and
|
||||
# reloading ornith:35b takes longer than that. OLLAMA_KEEP_ALIVE=-1 hid the
|
||||
# problem in steady state.
|
||||
#
|
||||
# The equivalent budget now belongs to the Go gateway's per-route timeout
|
||||
# config in rock/homelab-frontend, not to an annotation on this object.
|
||||
labels:
|
||||
app.kubernetes.io/name: llm-ornith
|
||||
app.kubernetes.io/part-of: llm-serving
|
||||
name: ornith
|
||||
namespace: llm-serving
|
||||
spec:
|
||||
predictor:
|
||||
containers:
|
||||
- command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- 'set -e
|
||||
|
||||
ollama serve &
|
||||
|
||||
SERVE_PID=$!
|
||||
|
||||
until ollama list >/dev/null 2>&1; do sleep 2; done
|
||||
|
||||
ollama pull ornith:35b
|
||||
|
||||
ollama run ornith:35b "ok" >/dev/null 2>&1 || true
|
||||
|
||||
wait $SERVE_PID
|
||||
|
||||
'
|
||||
env:
|
||||
- name: OLLAMA_HOST
|
||||
value: 0.0.0.0:8080
|
||||
- name: OLLAMA_MODELS
|
||||
value: /mnt/models/ollama
|
||||
- name: OLLAMA_CONTEXT_LENGTH
|
||||
value: '65536'
|
||||
- name: OLLAMA_KEEP_ALIVE
|
||||
value: '-1'
|
||||
- name: OLLAMA_NUM_PARALLEL
|
||||
value: '1'
|
||||
- name: OLLAMA_MAX_LOADED_MODELS
|
||||
value: '1'
|
||||
image: ollama/ollama:0.32.9@sha256:1685741456770df6e3cceb2a945a5f75e020f658d1701509668d6f4688f1dd3f
|
||||
name: kserve-container
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- ollama ps 2>/dev/null | grep -q ornith
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
limits:
|
||||
cpu: '16'
|
||||
memory: 16Gi
|
||||
nvidia.com/gpu: '1'
|
||||
requests:
|
||||
cpu: '8'
|
||||
memory: 8Gi
|
||||
nvidia.com/gpu: '1'
|
||||
startupProbe:
|
||||
exec:
|
||||
command:
|
||||
- /bin/sh
|
||||
- -c
|
||||
- ollama ps 2>/dev/null | grep -q ornith
|
||||
failureThreshold: 120
|
||||
periodSeconds: 15
|
||||
volumeMounts:
|
||||
- mountPath: /mnt/models
|
||||
name: models
|
||||
podMetadata:
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/metrics"
|
||||
labels:
|
||||
app.kubernetes.io/name: llm-ornith
|
||||
app.kubernetes.io/part-of: llm-serving
|
||||
deploymentStrategy:
|
||||
type: Recreate
|
||||
# 1 replica -- ornith:35b only. qwen2.5:3b moved to CPU on cp-2.
|
||||
# Frees 1 GPU for ComfyUI.
|
||||
maxReplicas: 1
|
||||
minReplicas: 1
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: worker-1
|
||||
runtimeClassName: nvidia
|
||||
volumes:
|
||||
- name: models
|
||||
persistentVolumeClaim:
|
||||
claimName: llm-models
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
apiVersion: serving.kserve.io/v1beta1
|
||||
kind: InferenceService
|
||||
metadata:
|
||||
annotations:
|
||||
serving.kserve.io/deploymentMode: RawDeployment
|
||||
labels:
|
||||
app.kubernetes.io/name: llm-reasoning
|
||||
app.kubernetes.io/part-of: llm-serving
|
||||
name: reasoning
|
||||
namespace: llm-serving
|
||||
spec:
|
||||
predictor:
|
||||
containers:
|
||||
- args:
|
||||
# bnb-4bit retired: no int4 tensor cores on sm70/V100, dequant-then-
|
||||
# matmul is two slow kernel launches instead of one fused int4 GEMM,
|
||||
# decode crawled at 2.5-10 tok/s regardless of TP/PP. Switched to
|
||||
# JunHowie/Qwen3-32B-GPTQ-Int4 -- same dense Qwen3-32B weights, same
|
||||
# hermes/qwen3 parser stack (no narration-bug risk, same as before),
|
||||
# only the quant format changes. Plain (non-Marlin) GPTQ kernel is
|
||||
# confirmed Volta-compatible; Marlin needs sm80+ and vLLM would try
|
||||
# to auto-upgrade to it, so --quantization is pinned explicitly to
|
||||
# `gptq` to force the plain kernel. Verified checkpoint size: 19.34GB
|
||||
# (summed from the real safetensors index, not bits-per-param math).
|
||||
# max-model-len=131072 is Qwen3-32B's real ceiling (config.json YaRN:
|
||||
# factor=4.0, original_max_position_embeddings=32768) -- 200k was
|
||||
# asked for but exceeds this architecturally regardless of VRAM.
|
||||
# KV cache math: 256KB/token total (64 layers, 8 KV heads, 128
|
||||
# head_dim, fp16), PP=2 splits both weights and KV load ~evenly, so
|
||||
# each GPU carries ~9.67GB weights + ~128KB/token KV. At
|
||||
# gpu-memory-utilization=0.90 (28.8GB/GPU usable), that leaves
|
||||
# ~19.1GB/GPU for KV cache -> ~156k tokens/GPU capacity, comfortably
|
||||
# above the 131072 target with room to spare -- the old
|
||||
# OffloadingConnector CPU-DRAM spillover (tuned for the previous
|
||||
# model's much smaller 16384 context) is no longer needed and is
|
||||
# dropped. Staying on PP=2 and vLLM 0.11.0 (no version bump needed,
|
||||
# this checkpoint only requires vllm>=0.9.2) -- plain GPTQ has no
|
||||
# TP>1 restriction unlike bnb, so tensor-parallel-size=2 is worth
|
||||
# trying later, but not risking a parallelism-strategy change in the
|
||||
# same rollout as the quant+context-length change.
|
||||
# This GPTQ requant's own config.json ships max_position_embeddings=
|
||||
# 40960 and rope_scaling=None -- confirmed directly (curl'd the raw
|
||||
# config.json), the base Qwen3-32B repo's YaRN block did NOT carry
|
||||
# over during quantization. Re-applying it explicitly here restores
|
||||
# the same math the base model documents (32768 * 4.0 = 131072);
|
||||
# without this, --max-model-len=131072 fails ModelConfig validation
|
||||
# against the checkpoint's own (unscaled) 40960 ceiling.
|
||||
- --model=JunHowie/Qwen3-32B-GPTQ-Int4
|
||||
- --served-model-name=reasoning
|
||||
- --quantization=gptq
|
||||
- --dtype=float16
|
||||
- --kv-cache-dtype=auto
|
||||
- --rope-scaling={"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":32768}
|
||||
- --tensor-parallel-size=1
|
||||
- --pipeline-parallel-size=2
|
||||
- --max-model-len=131072
|
||||
- --gpu-memory-utilization=0.90
|
||||
- --max-num-seqs=4
|
||||
- --enable-chunked-prefill
|
||||
- --enable-prefix-caching
|
||||
# qwen3 is vLLM's dedicated reasoning parser for this family's <think>
|
||||
# blocks.
|
||||
- --reasoning-parser=qwen3
|
||||
# hermes is the documented tool-call parser for general (non-Coder)
|
||||
# Qwen3 models -- native chat template support, not narrated text.
|
||||
- --enable-auto-tool-choice
|
||||
- --tool-call-parser=hermes
|
||||
- --host=0.0.0.0
|
||||
- --port=8080
|
||||
env:
|
||||
- name: VLLM_USE_FLASHINFER_SAMPLER
|
||||
value: '0'
|
||||
- name: VLLM_ATTENTION_BACKEND
|
||||
value: TRITON_ATTN
|
||||
- name: HF_HOME
|
||||
value: /mnt/models
|
||||
image: vllm/vllm-openai:v0.11.0@sha256:014a95f21c9edf6abe0aea6b07353f96baa4ec291c427bb1176dc7c93a85845c
|
||||
name: kserve-container
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
protocol: TCP
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
periodSeconds: 10
|
||||
resources:
|
||||
limits:
|
||||
cpu: '16'
|
||||
memory: 36Gi
|
||||
nvidia.com/gpu: '2'
|
||||
requests:
|
||||
cpu: '8'
|
||||
memory: 12Gi
|
||||
nvidia.com/gpu: '2'
|
||||
startupProbe:
|
||||
failureThreshold: 80
|
||||
httpGet:
|
||||
path: /health
|
||||
port: 8080
|
||||
periodSeconds: 15
|
||||
volumeMounts:
|
||||
- mountPath: /mnt/models
|
||||
name: models
|
||||
- mountPath: /dev/shm
|
||||
name: shm
|
||||
podMetadata:
|
||||
annotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/port: "8080"
|
||||
prometheus.io/path: "/metrics"
|
||||
labels:
|
||||
app.kubernetes.io/name: llm-reasoning
|
||||
app.kubernetes.io/part-of: llm-serving
|
||||
deploymentStrategy:
|
||||
type: Recreate
|
||||
maxReplicas: 1
|
||||
minReplicas: 1
|
||||
nodeSelector:
|
||||
kubernetes.io/hostname: worker-1
|
||||
runtimeClassName: nvidia
|
||||
volumes:
|
||||
- name: models
|
||||
persistentVolumeClaim:
|
||||
claimName: llm-models
|
||||
- emptyDir:
|
||||
medium: Memory
|
||||
sizeLimit: 2Gi
|
||||
name: shm
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
data:
|
||||
KMSVC_KAFKA_BROKERS: {{ .Values.env.kafkaBrokers | quote }}
|
||||
KMSVC_REDIS_ADDR: {{ .Values.env.redisAddr | quote }}
|
||||
KMSVC_AUTHENTIK_ISSUER_URL: {{ .Values.env.authentikIssuerURL | quote }}
|
||||
KMSVC_AUTHENTIK_AUDIENCE: {{ .Values.env.authentikAudience | quote }}
|
||||
KMSVC_ZITADEL_ISSUER_URL: {{ .Values.env.zitadelIssuerURL | quote }}
|
||||
KMSVC_ZITADEL_AUDIENCE: {{ .Values.env.zitadelAudience | quote }}
|
||||
KMSVC_GRPC_LISTEN_ADDR: ":{{ .Values.grpcPort }}"
|
||||
KMSVC_HTTP_LISTEN_ADDR: ":{{ .Values.httpPort }}"
|
||||
|
||||
@@ -12,8 +12,8 @@ httpPort: 8080
|
||||
env:
|
||||
kafkaBrokers: "kmsvc-kafka-bootstrap.sqs.svc.cluster.local:9092"
|
||||
redisAddr: "kmsvc-redis-master.sqs.svc.cluster.local:6379"
|
||||
authentikIssuerURL: ""
|
||||
authentikAudience: ""
|
||||
zitadelIssuerURL: ""
|
||||
zitadelAudience: ""
|
||||
|
||||
resources:
|
||||
requests:
|
||||
|
||||
@@ -12,6 +12,19 @@ rules:
|
||||
- apiGroups: ["kmsvc.io"]
|
||||
resources: ["queues"]
|
||||
verbs: ["get", "list", "watch", "update", "patch"]
|
||||
# The operator watches TemporalWorker objects and creates Deployments for
|
||||
# them. Without these two rules the controller logs
|
||||
# "cannot list resource temporalworkers/deployments ... at the cluster scope"
|
||||
# and never reconciles.
|
||||
- apiGroups: ["kmsvc.io"]
|
||||
resources: ["temporalworkers"]
|
||||
verbs: ["get", "list", "watch", "update", "patch"]
|
||||
- apiGroups: ["kmsvc.io"]
|
||||
resources: ["temporalworkers/status"]
|
||||
verbs: ["get", "update", "patch"]
|
||||
- apiGroups: ["apps"]
|
||||
resources: ["deployments"]
|
||||
verbs: ["get", "list", "watch", "create", "update", "patch"]
|
||||
- apiGroups: ["kmsvc.io"]
|
||||
resources: ["queues/status"]
|
||||
verbs: ["get", "update", "patch"]
|
||||
|
||||
@@ -48,7 +48,8 @@ spec:
|
||||
mountPath: /backup
|
||||
containers:
|
||||
- name: mc-mirror
|
||||
image: quay.io/minio/mc:latest
|
||||
# bitnamilegacy/minio-client:2025.7.21-debian-12-r3 replaces the dead quay.io/minio/mc image
|
||||
image: docker.io/bitnamilegacy/minio-client:2025.7.21-debian-12-r3@sha256:73bd39f7899a0cef12b8dd5df13aa93a3ed1aaa44236542442e9ac76819ac158
|
||||
env:
|
||||
- name: ACCESS_KEY
|
||||
valueFrom:
|
||||
|
||||
@@ -49,9 +49,9 @@ spec:
|
||||
echo "[init] Fetching JWT token from Authentik for paperless-ai-agent..."
|
||||
|
||||
# Get JWT token via client_credentials grant
|
||||
TOKEN_RESPONSE=$(curl -s -X POST https://authentik.riotpiao.com/application/o/token/ \
|
||||
TOKEN_RESPONSE=$(curl -s -X POST https://zitadel.riotpiao.com/oauth/v2/token \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id=paperless-ai-agent" \
|
||||
-d "client_id=396757803559027071" \
|
||||
-d "client_secret=29skAhE0tBzWn0qrPvmPTQMva0joFjcCr7msDQr_VXQ" \
|
||||
-d "scope=openid llm:inference" 2>/dev/null)
|
||||
|
||||
|
||||
@@ -9,6 +9,14 @@ metadata:
|
||||
app.kubernetes.io/name: paperless-ai
|
||||
spec:
|
||||
replicas: 1
|
||||
# Recreate, not RollingUpdate: paperless-ai-data is a single RWO PVC. With
|
||||
# RollingUpdate the new pod tries to mount the PVC while the old pod still
|
||||
# holds it -> "Multi-Attach error" / stuck Init:0/1 forever, rollout wedged
|
||||
# at "1 desired, 1 ready, 1 updated, 1 unavailable". Recreate terminates the
|
||||
# old pod first so the PVC detaches before the new one starts. Same pattern
|
||||
# as the Forgejo gitea Deployment.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: paperless-ai
|
||||
@@ -43,9 +51,9 @@ spec:
|
||||
|
||||
# Get JWT token via client_credentials grant
|
||||
# Client secret sourced from environment (injected from paperless-ai-config Secret)
|
||||
TOKEN_RESPONSE=$(curl -s -X POST https://authentik.riotpiao.com/application/o/token/ \
|
||||
TOKEN_RESPONSE=$(curl -s -X POST https://zitadel.riotpiao.com/oauth/v2/token \
|
||||
-d "grant_type=client_credentials" \
|
||||
-d "client_id=paperless-ai-agent" \
|
||||
-d "client_id=396757803559027071" \
|
||||
-d "client_secret=${LLM_AUTH_CLIENT_SECRET}" \
|
||||
-d "scope=openid llm:inference" 2>/dev/null)
|
||||
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: secretrotations.homelab.riotpiao.com
|
||||
spec:
|
||||
group: homelab.riotpiao.com
|
||||
names:
|
||||
kind: SecretRotation
|
||||
plural: secretrotations
|
||||
scope: Namespaced
|
||||
versions:
|
||||
- name: v1
|
||||
served: true
|
||||
storage: true
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
metadata:
|
||||
type: object
|
||||
spec:
|
||||
type: object
|
||||
required:
|
||||
- provider
|
||||
- rotationInterval
|
||||
properties:
|
||||
# External system: authentik | forgejo | minio | vault
|
||||
provider:
|
||||
type: string
|
||||
enum: [authentik, forgejo, minio, vault]
|
||||
|
||||
# How often to rotate (hours)
|
||||
rotationInterval:
|
||||
type: integer
|
||||
minimum: 24
|
||||
|
||||
# Application ID in external system
|
||||
appId:
|
||||
type: string
|
||||
|
||||
# k8s Secret to update (name, namespace, key)
|
||||
secretRef:
|
||||
type: object
|
||||
required: [name, namespace]
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
namespace:
|
||||
type: string
|
||||
key:
|
||||
type: string
|
||||
description: "Secret key to update (e.g., MINIO_IDENTITY_OPENID_CLIENT_SECRET)"
|
||||
|
||||
# Path to git file that holds the secret (for .enc.yaml files)
|
||||
gitPath:
|
||||
type: string
|
||||
description: "Path in homelab repo to .enc.yaml file"
|
||||
|
||||
# Ansible template values to substitute
|
||||
templateValues:
|
||||
type: object
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
lastRotationTime:
|
||||
type: string
|
||||
format: date-time
|
||||
nextRotationTime:
|
||||
type: string
|
||||
format: date-time
|
||||
lastRotationStatus:
|
||||
type: string
|
||||
enum: [Success, Failed, Pending]
|
||||
lastRotationError:
|
||||
type: string
|
||||
lastCommitHash:
|
||||
type: string
|
||||
|
||||
---
|
||||
# Example usage:
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: minio-oidc
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: authentik
|
||||
rotationInterval: 2160 # 90 days in hours
|
||||
appId: minio
|
||||
secretRef:
|
||||
name: minio-oidc
|
||||
namespace: storage
|
||||
key: MINIO_IDENTITY_OPENID_CLIENT_SECRET
|
||||
gitPath: k8s/argocd/secrets/minio-oidc.enc.yaml
|
||||
|
||||
---
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: portfolio-agent-oidc
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: authentik
|
||||
rotationInterval: 2160
|
||||
appId: portfolio-agent
|
||||
secretRef:
|
||||
name: portfolio-agent-oidc
|
||||
namespace: portfolio
|
||||
key: CLIENT_SECRET
|
||||
gitPath: k8s/argocd/secrets/portfolio-agent-oidc.enc.yaml
|
||||
|
||||
---
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: forgejo-registry-token
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: forgejo
|
||||
rotationInterval: 2160
|
||||
appId: rock/riotpiao.com
|
||||
secretRef:
|
||||
name: forgejo-registry-secret
|
||||
namespace: kube-system
|
||||
key: REGISTRY_TOKEN
|
||||
gitPath: k8s/argocd/secrets/forgejo-registry-secret.enc.yaml
|
||||
|
||||
---
|
||||
apiVersion: homelab.riotpiao.com/v1
|
||||
kind: SecretRotation
|
||||
metadata:
|
||||
name: minio-root-credentials
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
provider: minio
|
||||
rotationInterval: 4320 # 180 days in hours
|
||||
appId: root
|
||||
secretRef:
|
||||
name: minio-creds
|
||||
namespace: storage
|
||||
gitPath: k8s/argocd/secrets/minio-secrets.enc.yaml
|
||||
@@ -1,92 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
namespace: secret-rotation
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: secret-rotation-controller
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: secret-rotation-controller
|
||||
spec:
|
||||
serviceAccountName: secret-rotation-controller
|
||||
containers:
|
||||
- name: controller
|
||||
image: secret-rotation-controller:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
env:
|
||||
# SOPS reads age key from this file
|
||||
- name: SOPS_AGE_KEY_FILE
|
||||
value: /etc/sops/age/private-key.txt
|
||||
|
||||
# Vault auth (token in projected volume)
|
||||
- name: VAULT_ADDR
|
||||
value: http://vault.vault.svc.cluster.local:8200
|
||||
- name: VAULT_TOKEN_FILE
|
||||
value: /var/run/secrets/vault/token
|
||||
|
||||
# Authentik
|
||||
- name: AUTHENTIK_URL
|
||||
value: http://authentik-server.iam.svc.cluster.local
|
||||
- name: AUTHENTIK_BOOTSTRAP_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: authentik-bootstrap
|
||||
key: token
|
||||
|
||||
# Git
|
||||
- name: GIT_REPO
|
||||
value: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
- name: GIT_AUTHOR_EMAIL
|
||||
value: [email protected]
|
||||
- name: GIT_AUTHOR_NAME
|
||||
value: Secret Rotation Controller
|
||||
- name: FORGEJO_TOKEN
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: forgejo-registry-secret
|
||||
key: REGISTRY_TOKEN
|
||||
|
||||
volumeMounts:
|
||||
# Age key from ExternalSecret (synced from Vault)
|
||||
- name: age-key
|
||||
mountPath: /etc/sops/age
|
||||
readOnly: true
|
||||
|
||||
# Vault auth token (projected)
|
||||
- name: vault-token
|
||||
mountPath: /var/run/secrets/vault
|
||||
readOnly: true
|
||||
|
||||
# Temp working dir
|
||||
- name: tmp
|
||||
mountPath: /tmp
|
||||
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 256Mi
|
||||
limits:
|
||||
cpu: 500m
|
||||
memory: 512Mi
|
||||
|
||||
volumes:
|
||||
- name: age-key
|
||||
secret:
|
||||
secretName: sops-age-key
|
||||
defaultMode: 0400
|
||||
|
||||
- name: vault-token
|
||||
projected:
|
||||
sources:
|
||||
- serviceAccountToken:
|
||||
path: token
|
||||
audience: vault
|
||||
expirationSeconds: 3600
|
||||
|
||||
- name: tmp
|
||||
emptyDir: {}
|
||||
@@ -1,15 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
namespace: secret-rotation
|
||||
|
||||
resources:
|
||||
- rbac.yaml
|
||||
- crd.yaml
|
||||
- external-secret.yaml
|
||||
- deployment.yaml
|
||||
|
||||
commonLabels:
|
||||
app.kubernetes.io/name: secret-rotation-controller
|
||||
app.kubernetes.io/component: automation
|
||||
managed-by: argocd
|
||||
@@ -1,53 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
namespace: secret-rotation
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
rules:
|
||||
# Read SecretRotation CRDs
|
||||
- apiGroups: ["homelab.riotpiao.com"]
|
||||
resources: ["secretrotations"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
|
||||
# Update status
|
||||
- apiGroups: ["homelab.riotpiao.com"]
|
||||
resources: ["secretrotations/status"]
|
||||
verbs: ["get", "patch", "update"]
|
||||
|
||||
# Read k8s secrets that will be rotated
|
||||
- apiGroups: [""]
|
||||
resources: ["secrets"]
|
||||
verbs: ["get", "list"]
|
||||
|
||||
# For recording events
|
||||
- apiGroups: [""]
|
||||
resources: ["events"]
|
||||
verbs: ["create", "patch"]
|
||||
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: secret-rotation-controller
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: secret-rotation-controller
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: secret-rotation-controller
|
||||
namespace: secret-rotation
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: secret-rotation
|
||||
labels:
|
||||
kubernetes.io/metadata.name: secret-rotation
|
||||
@@ -1,115 +0,0 @@
|
||||
# macOS VM (Docker-OSX) hosting the BlueBubbles server.
|
||||
#
|
||||
# ── Why a VM and not a container ────────────────────────────────────────────
|
||||
# Containers share the host kernel. macOS binaries are Mach-O and need XNU plus
|
||||
# Cocoa/IOKit, which a Linux kernel cannot provide, so no macOS container exists
|
||||
# or can exist. Docker-OSX is QEMU running a macOS guest, packaged in a
|
||||
# container — a VM in a box, not a macOS container.
|
||||
#
|
||||
# ── Why this works on worker-2 ──────────────────────────────────────────────
|
||||
# Verified on the existing hardware: amd64, `vmx` (Intel VT-x) present, and
|
||||
# /dev/kvm exists on Talos nodes (KVM is compiled into Talos' kernel, not a
|
||||
# module). Bare metal, so no nested virtualisation needed.
|
||||
#
|
||||
# ── Read this before relying on it ──────────────────────────────────────────
|
||||
# 1. Setup is INTERACTIVE. First boot runs the macOS installer: connect over
|
||||
# VNC (:5999), erase the disk in Disk Utility, install, create a user, sign
|
||||
# into iMessage, THEN install BlueBubbles inside the guest. This manifest
|
||||
# only provides the machine; it does not provision macOS.
|
||||
# 2. iMessage activation on non-Apple hardware is a coin flip. BlueBubbles'
|
||||
# own guidance: "test sending an iMessage to yourself. If it does not
|
||||
# succeed, it's likely best to restart from the beginning."
|
||||
# 3. Apple's macOS licence permits virtualisation only on Apple hardware. This
|
||||
# is a Hackintosh. Use a throwaway Apple ID, not a primary one.
|
||||
# 4. BlueBubbles labels this path "not for beginners", "no guarantees or
|
||||
# warranty".
|
||||
#
|
||||
# Private API (reactions, typing indicators, edit/unsend) needs SIP disabled
|
||||
# inside the guest and is NOT required for plain send/receive. Skip it.
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: macos-bluebubbles
|
||||
labels:
|
||||
app.kubernetes.io/name: macos-bluebubbles
|
||||
app.kubernetes.io/part-of: sms
|
||||
spec:
|
||||
replicas: 1
|
||||
# Recreate: the qcow2 disk is RWO and a second pod must never attach it
|
||||
# concurrently — two QEMU processes on one image corrupts it.
|
||||
strategy:
|
||||
type: Recreate
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: macos-bluebubbles
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: macos-bluebubbles
|
||||
app.kubernetes.io/part-of: sms
|
||||
spec:
|
||||
# Dedicated node. The taint keeps everything else off worker-2; this
|
||||
# toleration is what lets the VM on. Both halves are required.
|
||||
nodeSelector:
|
||||
workload: imessage
|
||||
tolerations:
|
||||
- key: workload
|
||||
operator: Equal
|
||||
value: imessage
|
||||
effect: NoSchedule
|
||||
containers:
|
||||
- name: macos
|
||||
image: sickcodes/docker-osx:latest@sha256:3a3c82c79bc4e73531f819ccdfa4053b3084efd7c1f645678dbf8b4b3a24369c
|
||||
# QEMU needs /dev/kvm; Talos enforces `baseline` cluster-wide, so this
|
||||
# only schedules because the sms namespace is labelled privileged.
|
||||
securityContext:
|
||||
privileged: true
|
||||
env:
|
||||
# Generates a unique serial / board-serial / UUID / MAC and persists
|
||||
# them to bootdisk.qcow2. This synthetic identity is what iMessage
|
||||
# activates against — it must stay stable across restarts, which is
|
||||
# why the PVC matters.
|
||||
- name: GENERATE_UNIQUE
|
||||
value: "true"
|
||||
# Identity is only plausible if it matches a real product line.
|
||||
- name: DEVICE_MODEL
|
||||
value: "iMacPro1,1"
|
||||
- name: RAM
|
||||
value: "12"
|
||||
- name: CORES
|
||||
value: "6"
|
||||
- name: EXTRA
|
||||
# Expose the BlueBubbles server port from the guest to the pod.
|
||||
# Guest :1234 (BlueBubbles default) -> pod :1234.
|
||||
value: "-device virtio-net-pci,netdev=net0 -netdev user,id=net0,hostfwd=tcp::1234-:1234"
|
||||
ports:
|
||||
- name: vnc
|
||||
containerPort: 5999
|
||||
protocol: TCP
|
||||
- name: bluebubbles
|
||||
containerPort: 1234
|
||||
protocol: TCP
|
||||
resources:
|
||||
requests:
|
||||
cpu: "6"
|
||||
memory: 14Gi
|
||||
limits:
|
||||
cpu: "12"
|
||||
memory: 20Gi
|
||||
volumeMounts:
|
||||
- name: macos-disk
|
||||
mountPath: /home/arch/OSX-KVM/disk
|
||||
- name: kvm
|
||||
mountPath: /dev/kvm
|
||||
# No readiness probe on purpose. The guest takes many minutes to boot,
|
||||
# and until macOS + BlueBubbles are installed BY HAND there is nothing
|
||||
# listening on 1234. A probe here would crash-loop the pod through the
|
||||
# entire interactive install.
|
||||
volumes:
|
||||
- name: macos-disk
|
||||
persistentVolumeClaim:
|
||||
claimName: macos-disk
|
||||
- name: kvm
|
||||
hostPath:
|
||||
path: /dev/kvm
|
||||
type: CharDevice
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
namespace: sms
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- storageclass.yaml
|
||||
- pvc-macos.yaml
|
||||
- deployment-macos.yaml
|
||||
- service.yaml
|
||||
- networkpolicy.yaml
|
||||
@@ -1,19 +0,0 @@
|
||||
# iMessage delivery for the cluster.
|
||||
#
|
||||
# BlueBubbles' server is a macOS Electron app paired with an Objective-C helper
|
||||
# that hooks Messages.app private APIs — it cannot be containerised on Linux,
|
||||
# because containers share the host kernel and macOS needs XNU + Cocoa. The only
|
||||
# way to run it on Talos is a full macOS VM under QEMU/KVM (Docker-OSX), which
|
||||
# needs a privileged pod with /dev/kvm.
|
||||
#
|
||||
# Hence privileged PodSecurity: the cluster default from the Talos controlplane
|
||||
# is `enforce: baseline`, which forbids privileged containers and host devices.
|
||||
# Scope is limited to this namespace.
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: sms
|
||||
labels:
|
||||
pod-security.kubernetes.io/enforce: privileged
|
||||
pod-security.kubernetes.io/audit: privileged
|
||||
pod-security.kubernetes.io/warn: privileged
|
||||
@@ -1,22 +0,0 @@
|
||||
# Default-deny. This namespace runs a privileged QEMU VM signed into an Apple
|
||||
# ID and exposes an unauthenticated VNC console; nothing should reach it except
|
||||
# opted-in clients.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
name: sms-default-deny
|
||||
spec:
|
||||
podSelector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/part-of: sms
|
||||
policyTypes:
|
||||
- Ingress
|
||||
ingress:
|
||||
- from:
|
||||
- namespaceSelector: {}
|
||||
podSelector:
|
||||
matchLabels:
|
||||
sms-client: "true"
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 1234
|
||||
@@ -1,25 +0,0 @@
|
||||
# Persistent macOS disk image + generated hardware identity (bootdisk.qcow2).
|
||||
#
|
||||
# This volume is NOT disposable: it holds the VM's serial number, board serial,
|
||||
# UUID and MAC, which together form the identity iMessage was activated against.
|
||||
# Losing it means re-running activation, which is the least reliable step of the
|
||||
# whole setup.
|
||||
#
|
||||
# Docker-OSX documents 128GB minimum for the guest image; 200Gi leaves room for
|
||||
# the installer, the base system, and qcow2 growth.
|
||||
#
|
||||
# ⚠️ Single replica (see storageclass.yaml — capacity and IO both rule out 3).
|
||||
# Losing worker-2's disk therefore means losing the activated identity and
|
||||
# redoing iMessage activation. Once the guest is installed and activated, take
|
||||
# a Longhorn snapshot/backup of this volume; that is the only redundancy here.
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: macos-disk
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
storageClassName: longhorn-imessage-local
|
||||
resources:
|
||||
requests:
|
||||
storage: 200Gi
|
||||
@@ -1,31 +0,0 @@
|
||||
# VNC is how you drive the interactive macOS install. Deliberately ClusterIP —
|
||||
# it is an unauthenticated console onto a machine holding a live Apple ID
|
||||
# session. Reach it with `kubectl port-forward`, never an Ingress.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: macos-vnc
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: macos-bluebubbles
|
||||
ports:
|
||||
- name: vnc
|
||||
port: 5999
|
||||
targetPort: vnc
|
||||
---
|
||||
# The BlueBubbles REST API, once installed inside the guest. This is the stable
|
||||
# name cluster services use, so callers never depend on the pod IP or on whether
|
||||
# the backend is this VM or a real Mac mini later.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: bluebubbles
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: macos-bluebubbles
|
||||
ports:
|
||||
- name: http
|
||||
port: 1234
|
||||
targetPort: bluebubbles
|
||||
@@ -1,32 +0,0 @@
|
||||
# Dedicated StorageClass for the macOS VM disk.
|
||||
#
|
||||
# The default `longhorn` class does not work here, for two independent reasons:
|
||||
#
|
||||
# 1. Replica count. Default is 3, and Longhorn schedules against
|
||||
# storageMaximum - storageScheduled with over-provisioning at 100%. Free
|
||||
# space is cp-1 146Gi / cp-2 8Gi / cp-3 146Gi / worker-1 292Gi, so a 200Gi
|
||||
# volume has only one node that can hold even a single replica — a 3-replica
|
||||
# volume fails outright with ReplicaSchedulingFailure.
|
||||
# 2. Binding mode. `Immediate` provisions the volume the moment the PVC is
|
||||
# created, before any pod is scheduled. Combined with strict-local that
|
||||
# pins the data to an arbitrary node, not the one the VM runs on.
|
||||
#
|
||||
# So: one replica, kept local to the VM, bound only once the pod has a node.
|
||||
apiVersion: storage.k8s.io/v1
|
||||
kind: StorageClass
|
||||
metadata:
|
||||
name: longhorn-imessage-local
|
||||
provisioner: driver.longhorn.io
|
||||
allowVolumeExpansion: true
|
||||
reclaimPolicy: Delete
|
||||
# The pod is pinned to worker-2 by nodeSelector; wait for it to be scheduled so
|
||||
# the replica is placed on that node and not somewhere else.
|
||||
volumeBindingMode: WaitForFirstConsumer
|
||||
parameters:
|
||||
# A qcow2 backing a live VM is latency-sensitive and rewritten constantly.
|
||||
# Serving it over the network from another node's disk would be the single
|
||||
# worst thing for guest responsiveness, so force it local.
|
||||
numberOfReplicas: "1"
|
||||
dataLocality: "strict-local"
|
||||
staleReplicaTimeout: "30"
|
||||
fsType: "ext4"
|
||||
@@ -1,33 +0,0 @@
|
||||
# ArgoCD Image Updater - auto-updates Application images from registry
|
||||
# Watches forgejo.riotpiao.com for new image tags and updates Applications
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: argocd-image-updater
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
spec:
|
||||
project: homelab
|
||||
revisionHistoryLimit: 3
|
||||
sources:
|
||||
- repoURL: https://argoproj.github.io/argo-helm
|
||||
chart: argocd-image-updater
|
||||
targetRevision: "0.11.2"
|
||||
helm:
|
||||
valueFiles:
|
||||
- $values/k8s/infra/argocd-image-updater/values.yaml
|
||||
- repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: argocd
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=false
|
||||
@@ -1,32 +0,0 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: secret-rotation
|
||||
namespace: argocd
|
||||
labels:
|
||||
app.kubernetes.io/name: secret-rotation
|
||||
spec:
|
||||
project: homelab
|
||||
|
||||
sources:
|
||||
- repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
path: k8s/apps/secret-rotation-controller
|
||||
targetRevision: main
|
||||
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: secret-rotation
|
||||
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
- RespectIgnoreDifferences=true
|
||||
retry:
|
||||
limit: 5
|
||||
backoff:
|
||||
duration: 5s
|
||||
factor: 2
|
||||
maxDuration: 3m
|
||||
@@ -249,3 +249,26 @@ spec:
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: local-path-provisioner
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "1"
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
path: k8s/infra/local-path
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: local-path-storage
|
||||
# No CreateNamespace: the Namespace object (with its privileged PSA label,
|
||||
# which the helper Pods need for their hostPath) is part of the manifest.
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
|
||||
@@ -30,22 +30,45 @@ spec:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
# ZITADEL — the Authentik replacement. Coexistence, not cutover: Authentik keeps
|
||||
# serving in the same `iam` namespace until the last consumer moves, then this
|
||||
# becomes the only IdP. Migration plan (outside git):
|
||||
# ~/workplace/homelab-doc/ZITADEL_MIGRATION.html
|
||||
#
|
||||
# Wave 3 ordering: sops-secrets (wave 0) creates the `zitadel-masterkey` Secret,
|
||||
# databases (wave 2) creates the `zitadel-db` CNPG cluster and its
|
||||
# `zitadel-db-app` DSN secret. Both must exist before the init job starts.
|
||||
#
|
||||
# automated sync is deliberately absent for the first sync — same reason as
|
||||
# forgejo. The chart ships `pre-install,pre-upgrade` Helm hooks (zitadel-init,
|
||||
# zitadel-setup), which ArgoCD maps to PreSync and re-runs on every sync, manual
|
||||
# or automated. A human reads the first sync's hook output before any repeat can
|
||||
# run. The init job runs `init zitadel` only (CNPG owns the database and the app
|
||||
# user), and the setup job creates the first org and the `iam-admin` machine key
|
||||
# that Terraform consumes. Read the SECOND-SYNC HAZARD block at the top of
|
||||
# k8s/infra/iam/zitadel-values.yaml before enabling automated sync.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: authentik
|
||||
name: zitadel
|
||||
namespace: argocd
|
||||
finalizers:
|
||||
- resources-finalizer.argocd.argoproj.io
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "3"
|
||||
spec:
|
||||
project: homelab
|
||||
revisionHistoryLimit: 3
|
||||
sources:
|
||||
- repoURL: https://charts.goauthentik.io
|
||||
chart: authentik
|
||||
targetRevision: "*"
|
||||
- repoURL: https://charts.zitadel.com
|
||||
chart: zitadel
|
||||
# Pinned: the chart trails the upstream app release, and the verified
|
||||
# masterkey/passwap facts were read at v4.19.x. A floating tag would
|
||||
# change the app version without a diff anyone reviews.
|
||||
targetRevision: 10.4.0
|
||||
helm:
|
||||
valueFiles:
|
||||
- $values/k8s/infra/iam/authentik-values.yaml
|
||||
- $values/k8s/infra/iam/zitadel-values.yaml
|
||||
- repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
@@ -53,15 +76,50 @@ spec:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: iam
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
# Raw IAM manifests: key-rotation cronjob + authentik migration job.
|
||||
# ZITADEL core objects (ServiceAccount, Role, RoleBinding, ConfigMap).
|
||||
#
|
||||
# The chart declares these as pre-install/pre-upgrade Helm hooks, and ArgoCD
|
||||
# prunes hook resources when a sync ends. That deleted the ServiceAccount and
|
||||
# ConfigMap the ZITADEL Deployment references, so new pods failed to start with
|
||||
# `serviceaccount "zitadel" not found`. This Application owns hook-free copies,
|
||||
# which makes them ordinary desired state that survives every sync.
|
||||
#
|
||||
# Wave 2: before the zatadel chart itself, so the ServiceAccount exists by the
|
||||
# time its Deployment is created.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: iam-jobs
|
||||
name: zitadel-core
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "2"
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
path: k8s/infra/zitadel-core
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: iam
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: false
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
# Raw IAM support manifests: the SMTP -> Gotify notification relay that ZITADEL
|
||||
# points at, so verification and password-reset codes reach the phone. Wave 3
|
||||
# alongside ZITADEL itself; the Secret it reads is created by sops-secrets at
|
||||
# wave 0.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: iam-support
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "3"
|
||||
@@ -152,13 +210,8 @@ metadata:
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "3"
|
||||
argocd-image-updater.argoproj.io/image-list: runner=forgejo.riotpiao.com/rock/forgejo-runner-golang
|
||||
argocd-image-updater.argoproj.io/runner.update-strategy: newest-build
|
||||
argocd-image-updater.argoproj.io/runner.allow-tags: regexp:^[0-9a-f]{7}$|^latest$|^v[0-9]+$
|
||||
argocd-image-updater.argoproj.io/runner.helm.image-name: runner.image.repository
|
||||
argocd-image-updater.argoproj.io/runner.helm.image-tag: runner.image.tag
|
||||
argocd-image-updater.argoproj.io/write-back-method: git
|
||||
argocd-image-updater.argoproj.io/git-branch: main
|
||||
# Image promotion: Forgejo package webhook → api-gw.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
@@ -180,13 +233,8 @@ metadata:
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "3"
|
||||
argocd-image-updater.argoproj.io/image-list: runner=forgejo.riotpiao.com/rock/forgejo-runner-node
|
||||
argocd-image-updater.argoproj.io/runner.update-strategy: newest-build
|
||||
argocd-image-updater.argoproj.io/runner.allow-tags: regexp:^[0-9a-f]{7}$|^latest$|^v[0-9]+$
|
||||
argocd-image-updater.argoproj.io/runner.helm.image-name: runner.image.repository
|
||||
argocd-image-updater.argoproj.io/runner.helm.image-tag: runner.image.tag
|
||||
argocd-image-updater.argoproj.io/write-back-method: git
|
||||
argocd-image-updater.argoproj.io/git-branch: main
|
||||
# Image promotion: Forgejo package webhook → api-gw.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
@@ -211,13 +259,8 @@ metadata:
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "3"
|
||||
argocd-image-updater.argoproj.io/image-list: runner=forgejo.riotpiao.com/rock/forgejo-runner-rust
|
||||
argocd-image-updater.argoproj.io/runner.update-strategy: newest-build
|
||||
argocd-image-updater.argoproj.io/runner.allow-tags: regexp:^[0-9a-f]{7}$|^latest$|^v[0-9]+$
|
||||
argocd-image-updater.argoproj.io/runner.helm.image-name: runner.image.repository
|
||||
argocd-image-updater.argoproj.io/runner.helm.image-tag: runner.image.tag
|
||||
argocd-image-updater.argoproj.io/write-back-method: git
|
||||
argocd-image-updater.argoproj.io/git-branch: main
|
||||
# Image promotion: Forgejo package webhook → api-gw.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Wave 2 — dedicated per-app CNPG Postgres clusters (authentik-db → ns iam,
|
||||
# Wave 2 — dedicated per-app CNPG Postgres clusters (zitadel-db → ns iam,
|
||||
# temporal-db + visibility → ns temporal). ONE App, ONE folder (k8s/infra/databases).
|
||||
# CNPG operator is Phase-0 bootstrap; these Cluster CRs are GitOps — no circular
|
||||
# dep (they run after ArgoCD is up, before their apps at w3/w8). CNPG generates
|
||||
|
||||
@@ -36,11 +36,9 @@ metadata:
|
||||
app.kubernetes.io/component: gateway
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "7"
|
||||
# ArgoCD Image Updater - auto-update on new image push
|
||||
argocd-image-updater.argoproj.io/image-list: gw=forgejo.riotpiao.com/rock/api-gateway
|
||||
argocd-image-updater.argoproj.io/gw.update-strategy: digest
|
||||
argocd-image-updater.argoproj.io/gw.allow-tags: regexp:^latest$
|
||||
argocd-image-updater.argoproj.io/write-back-method: argocd
|
||||
# Image promotion: Forgejo package webhook → api-gw /v1/webhooks/forgejo
|
||||
# → patches this Application's kustomize.images via k8s API.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
revisionHistoryLimit: 3
|
||||
|
||||
@@ -110,37 +110,6 @@ spec:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
# iMessage/SMS delivery. Raw manifests: a privileged macOS VM (Docker-OSX)
|
||||
# running the BlueBubbles server, plus its dedicated local StorageClass.
|
||||
#
|
||||
# Pinned to worker-2 via nodeSelector `workload: imessage` + a matching
|
||||
# toleration for that node's taint. Until worker-2 is provisioned this app
|
||||
# syncs everything except the pod, which stays Pending — that is expected.
|
||||
#
|
||||
# No CreateNamespace: namespace.yaml carries `pod-security: privileged`, which
|
||||
# the VM needs (/dev/kvm, privileged), and an ArgoCD-created namespace would
|
||||
# not have those labels.
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: sms
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "8"
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
path: k8s/apps/sms
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: sms
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
---
|
||||
# Document management. Raw manifests (no Helm): postgres is the dedicated
|
||||
# paperless-db CNPG cluster in k8s/infra/databases (wave 2), redis is
|
||||
# in-cluster only (no PVC), media lives on the cp-3 USB HDD (see
|
||||
@@ -202,40 +171,6 @@ spec:
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
# Consolidated: homarr + homarr-patches → homarr
|
||||
# Helm chart + values + PostSync hook patch (fix-probes-job.yaml)
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: homarr
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "8"
|
||||
spec:
|
||||
project: homelab
|
||||
sources:
|
||||
- repoURL: https://homarr-labs.github.io/charts
|
||||
chart: homarr
|
||||
targetRevision: "*"
|
||||
helm:
|
||||
valueFiles:
|
||||
- $values/k8s/apps/homarr/homarr-values.yaml
|
||||
- repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
ref: values
|
||||
- repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
path: k8s/apps/homarr # PostSync hook: fix-probes-job.yaml
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: dashboard
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
syncOptions:
|
||||
- CreateNamespace=true
|
||||
---
|
||||
# Portfolio site at riotpiao.com - static Next.js site from rock/riotpiao.com repo.
|
||||
# Points directly to infra/portfolio/base (bypassing repo's own argocd-apps.yaml
|
||||
# which has wrong URLs). Image built by Forgejo Actions on rock/portfolio repo.
|
||||
@@ -246,11 +181,8 @@ metadata:
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "8"
|
||||
# ArgoCD Image Updater - auto-update on new image push
|
||||
argocd-image-updater.argoproj.io/image-list: app=forgejo.riotpiao.com/rock/portfolio
|
||||
argocd-image-updater.argoproj.io/app.update-strategy: digest
|
||||
argocd-image-updater.argoproj.io/app.allow-tags: regexp:^latest$
|
||||
argocd-image-updater.argoproj.io/write-back-method: argocd
|
||||
# Image promotion: Forgejo package webhook → api-gw.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
|
||||
@@ -27,15 +27,10 @@ metadata:
|
||||
app.kubernetes.io/part-of: poimen
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "7"
|
||||
# ── Image Updater ─────────────────────────────────────────────────────────
|
||||
argocd-image-updater.argoproj.io/image-list: "workflows=forgejo.riotpiao.com/riotpiao-poimen/poimen-workflows,memory=forgejo.riotpiao.com/riotpiao-poimen/poimen-memory,frontend=forgejo.riotpiao.com/riotpiao-poimen/poimen-frontend"
|
||||
argocd-image-updater.argoproj.io/workflows.update-strategy: digest
|
||||
argocd-image-updater.argoproj.io/workflows.allow-tags: regexp:^latest$
|
||||
argocd-image-updater.argoproj.io/memory.update-strategy: digest
|
||||
argocd-image-updater.argoproj.io/memory.allow-tags: regexp:^latest$
|
||||
argocd-image-updater.argoproj.io/frontend.update-strategy: digest
|
||||
argocd-image-updater.argoproj.io/frontend.allow-tags: regexp:^latest$
|
||||
argocd-image-updater.argoproj.io/write-back-method: argocd
|
||||
# ── Image promotion ──────────────────────────────────────────────────────
|
||||
# Forgejo package webhook → api-gw patches kustomize.images per source.
|
||||
# Source index mapping: 0=workflows, 1=memory, 3=frontend.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
# Multi-source: each repo owns its own k8s manifests.
|
||||
|
||||
@@ -5,11 +5,9 @@ metadata:
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "7"
|
||||
argocd-image-updater.argoproj.io/image-list: |
|
||||
controller=ghcr.io/riotpiao-poimen/poimen-toolkit-controller
|
||||
argocd-image-updater.argoproj.io/controller.update-strategy: digest
|
||||
argocd-image-updater.argoproj.io/controller.allow-tags: regexp:^latest$
|
||||
argocd-image-updater.argoproj.io/write-back-method: argocd
|
||||
# Image promotion: Forgejo package webhook → api-gw.
|
||||
# Tracks: poimen-toolkit-controller, pi-worker.
|
||||
# Replaced ArgoCD Image Updater (removed 2026-09-10).
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
apiVersion: argoproj.io/v1alpha1
|
||||
kind: Application
|
||||
metadata:
|
||||
name: talos-repair
|
||||
namespace: argocd
|
||||
annotations:
|
||||
argocd.argoproj.io/sync-wave: "6"
|
||||
spec:
|
||||
project: homelab
|
||||
source:
|
||||
repoURL: https://forgejo.riotpiao.com/riotpiao-poimen/homelab.git
|
||||
targetRevision: main
|
||||
path: k8s/infra/talos-repair
|
||||
destination:
|
||||
server: https://kubernetes.default.svc
|
||||
namespace: cicd
|
||||
syncPolicy:
|
||||
automated:
|
||||
prune: true
|
||||
selfHeal: true
|
||||
@@ -34,10 +34,9 @@ spec:
|
||||
- https://prometheus-community.github.io/helm-charts
|
||||
- https://grafana.github.io/helm-charts
|
||||
- https://helm.releases.hashicorp.com
|
||||
- https://charts.goauthentik.io
|
||||
- https://charts.zitadel.com
|
||||
- https://strimzi.io/charts/
|
||||
- https://charts.bitnami.com/bitnami
|
||||
- https://homarr-labs.github.io/charts
|
||||
- https://go.temporal.io/helm-charts
|
||||
- https://portainer.github.io/k8s/
|
||||
# Substrate charts (cert-manager, ingress-nginx, reloader) — app-of-apps owned
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
apiVersion: ENC[AES256_GCM,data:Fc0=,iv:UFqMFeZTJZtFMoNeusjAoSI60U9StZS4IXw+n8JQCRM=,tag:uUvY4+jcjiSJHs0HDS3/7A==,type:str]
|
||||
kind: ENC[AES256_GCM,data:NUw86IEw,iv:gh4Vqn6uteYnYIFcXT/QVxRPKm+G+j2aIVuV71nZ4k4=,tag:nZA/x75Kx4+CoSx66BsvkQ==,type:str]
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:qex150pm0eGwYiiI05y9LUs=,iv:5Tk001T796c4HFikoh1ZVB6ierBfDqOUkFs5ilkr+V8=,tag:LiDAiCH7w6aRS41VKIlIKg==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:nI91,iv:c1hBWGMkVb+vg+MKNzty8CFfXxQabXEtT+EpiuQZNvo=,tag:BsGtJDbBadDdTa687+Sdag==,type:str]
|
||||
type: ENC[AES256_GCM,data:3rXawlSD,iv:FUtLK4KP7v/SBF08ZcU6siIO1qfjlJirf2x5PyzAjuA=,tag:mSIQ+Y43+wDfZGRbQ6a0Og==,type:str]
|
||||
stringData:
|
||||
AUTHENTIK_SECRET_KEY: ENC[AES256_GCM,data:i5tVOWnN9a/cfhs6gckveRxUpiN7Ie227R/74kOIz6lQ5US2hkf03W5qSGCrzheL3DB0i8+d0iJYsApjn/wCiN7ndZZVFko3W5jDfVPybxc=,iv:djF1G9JHT5B7ziTokA/UuPpG+tG2LvFU4uMAR0x4M6o=,tag:vHOd7jLYkM8Evnr0QUVrCw==,type:str]
|
||||
AUTHENTIK_BOOTSTRAP_PASSWORD: ENC[AES256_GCM,data:PjvYtH03CzQbTXEqt/Z18+u1zJdok7DLwDn51FUZXDA=,iv:pSwSm8hB0/Q8uOAcOZ1rTqq7BF782EglG19GGeghm1E=,tag:qGCQFWCrwsWBOiQ07ySMUQ==,type:str]
|
||||
AUTHENTIK_BOOTSTRAP_TOKEN: ENC[AES256_GCM,data:1tnekGo0GIemJCMRenjN0yNeSiiMl/kky8ms84TsqBLaQ0lQzPxjq9qtgNJwoAsufxrznrkDVKjeOEHNYcA82A==,iv:NNFWSLMILoWccA9ihy1OArSS1X3OySB7PIHdXZdyzlg=,tag:t2w+KyReOXftTrU9pBQDnQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBSZGRKWm9PTkNSOENtREgx
|
||||
M2V3Z0xxOXRhRGVRclo3SzBKRkc0VmtCY0NnClJPSGJRcUlzWmNFeTZ1bmorNEV4
|
||||
U3l3TzVlMTlrWkJnQzBtUTJtcUwrM3MKLS0tIEJrMWZrbllYYUZKN1V4MVMzcXAv
|
||||
eUtheHlNMHpsTEJXZ1FTMFVxbWloa0kKLRkE1Du+4gdOLerOl9y0mZw+8fqfECQY
|
||||
uP64Q+9BSuPYNiTLZvYDzmdoy+KTS7i7C9rf+iU96R/crbQLamV9Ew==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T20:37:07Z"
|
||||
mac: ENC[AES256_GCM,data:b3pDx340d1iZxsPPXzACckoLBrW+XjX1qlGrWEEI30j2Ao0hewTRZJyHbdI97A6b/hqzBN/4tjqqkdqm8goNhUQweMLTw86X6sALuE0aLS9wK53qjqih8fQwQBbgLDtjFXtW26h+smIm7aB5ZSDvU2le44HDXuWd83sWtCavNq0=,iv:ZMDsoKpygFJrYpXy2dA0IlxAi0jt24rxF72eUzgQs7k=,tag:WWE7G7+c9JK6fF1U9V8igg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: cloudflared-credentials
|
||||
namespace: cloudflared
|
||||
type: Opaque
|
||||
stringData:
|
||||
credentials.json: ENC[AES256_GCM,data:LnJVGuxJAOwDYbLgX00OHICDDldZgwgvGafOIwKoJKnjg3FjFMQYThNCOL/H+6tctuKixJNcAckjg70tmCKWxNq9I35XX/YSqob6Qv4fywv32hsNIAe44BU035MGZ5zmc6bZZ8wAgTJZw6Th4TzY7taFBCRdj2fW4T+qVfq7strn68G4ufVMK6UXlOkEDLjR2Vxj32lqSLdbdMma96XB+265hwL6IVfaugG662mcGhPwgHk=,iv:NFmyHN5m2mJUBgn1qgmJZ2YgkFxBZyp7+gU1Ng6B/7M=,tag:/66JtHK5Yu7pRxB0b0eifQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBsaDVycUE4RFpoRjdpTlpX
|
||||
dGpzN3hwRFQ2cmtXVXVzWnZJOXJEZXZRWEhNCkJwbm9QV0lGcGtNRTJqbStrc3l1
|
||||
Q2M4bE1WbzJpTXZQWjJyTGpFS0JYcVUKLS0tIDBvU2U5aEtVTTZUaE85eEkyYitG
|
||||
bGtSVkhKNnRuNlZWMWh6Z1M4VXM1WU0KdusvtM85V0xrO+uJFUY+UH0tuwdF0GiW
|
||||
79J4Rlxk8ne25psKsvwjLYLxZ53xjFHCC4FCHTNkYwTKCDEKjw46Lg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-26T02:45:04Z"
|
||||
mac: ENC[AES256_GCM,data:KF7KwXCnNLAg1YI3u0lLI/ho4sITHAq09ac2WO4TK3TCthkuivb89YjhuuJKs0NFhjn6aRLqmZavUqcXH2HujiryqbLNVexOShQM0wSzgCzmWdbi0UzpgJ4KcQA3PufDtsQz4HHLXx396EbZrM3Ie3WSu/PvRm5iWOxzTGCGc9Q=,iv:Eo920oXXD1FLZVJXywZ6C+LRL+qZjmnZXv0+Oa/GMb8=,tag:6LuPVSet4WjvrvGUAxaJlw==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,30 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: comfyui-oauth2-proxy
|
||||
namespace: comfyui
|
||||
type: Opaque
|
||||
data:
|
||||
client-id: ENC[AES256_GCM,data:jFLgcrh1tiAU0pijGxy+/mFsFHzI5zSi,iv:7GYL4xtPnzyZWtEA0egPHhzvpXGevkIH3Cvr+oD9hKI=,tag:UeDTIKlfcYnOOOJiPNgN0g==,type:str]
|
||||
client-secret: ENC[AES256_GCM,data:9QFu5a2EkEMjc/B0moPIkMEChrN0Qh5BQtAzFcvW4liKxzVAPbTKgQOJeND+NQjn5ZoE89kqqRyqNJF3bT3H1pWoDFngk9J2X7BN/9r6Qyubias0Jukydw==,iv:xf2kT0fbnJat3L3xxUYm3ht0YvHptU/RIdt9Eh0D6xU=,tag:8iknaQ2kKP1OZ/1jRCa+1g==,type:str]
|
||||
cookie-secret: ENC[AES256_GCM,data:Qq+l4go/CFtfsZrgYUcTj9bB4sreu2GEqJnu15H6fxIlD7IgnmrodPoIVNpWcdm6LSYabGjm2jraN6RP,iv:EOoDHgvIydzHCC5POohjPq3Vj2YTpzhIhvdG2FzKKzc=,tag:iDXqzFU1Z79bzpG4I4xfww==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBxOUtCUDduMVU5U1J4WXZX
|
||||
Q1VzaFRVdXlHaEZpeVJNOWprdDhUOEd6Z1hzCi9QZEpXN3NDSENzUE9RTGdpb3Va
|
||||
eVNHVTFLbXpMU0x5UWlFLzZnVlllWE0KLS0tIDZldVNqR1loNHlXZWovNWZ4WjFD
|
||||
dVErenJDM3haSStxZEVWZUtBbDBIZ1UK9x6Gy4drjfUz2goAaI+oeUDWdNHQFAJl
|
||||
JGI+X81r/6Krg5N8QDa/mE96E2M2G0yAO5FS1YbpCKOITUEyheacnQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-10-27T21:17:08Z"
|
||||
mac: ENC[AES256_GCM,data:tTGmWFidEWTWHgKPV7b5ABOJwwlP1yQDK5Jd60c9ko6IsmP2GBBPCth7kJgPXuPcw+NHbJ7yt7bcVto7GkPdtY9daJmjPq0Gt16ZO4Xi4hCk22JVKqi/KJcKD/KB0r/VO9w093/Who5CcYgqifextfca4CuxS5m5dcOn4kRJCfE=,iv:0fDvvQr9wp8IrD16u88Ml4HsPEeMhd8R3ljQJoIpw3E=,tag:AanM7C6vbaBBmus1u1GRoA==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: etcd-defrag-creds
|
||||
namespace: kube-system
|
||||
labels:
|
||||
app.kubernetes.io/name: etcd-defrag
|
||||
type: Opaque
|
||||
stringData:
|
||||
ca.crt: ENC[AES256_GCM,data:vso/Ouz5QRoRF2CFj9TsGYhoGkX8oGJIeiFCoS92AAQbyyGPTiZ/fPNFM03p+TpeKDb+Z+Fyo/HXuDeT5Fee5kjPnUHXf2hiUBmJLyHnPMp5i57dRzTC8X4VaNeV/1MmP2WpkWwieWOGBxJVIIrgeQhQexxmIzSlQ5Io9dVOH8F6iyakwdf7qfLFGdoer3rfrXadSOJtKRpLneFJDL2OmR1042LZ8KtNuRRXI/qrdpgr2bnyxFuCRc+tk5QY73mpwwWfKGn6FW/rVdjiEftkC+AvTUItx7RkYMrz0PlTYPUOLustEF2gIgWheYZhE5ebZDurVSG//sudjASliYJItwgSk3G5mZihOSDA7Jcus+U8p1iHQ9CQsey3CSAMmuSGRFmq42kI8135kjtBYrzVswHjX1/51qPMyA2TIeWlRJ7AJlHihG317gT1vPeM1ULl3PkuCxaBLCtjotZBzKLJ/6zXr1ig77Baz71YOVB0D0MgFvzIdao+mdxS8WLOZdiXOCwbI5Y2eo7wJHG4BnpKEdx1rjPkv7DI2NNEoXA7e5FvJWinyAxOy6YGH4Fv6dTlEVhI3Nl/1X+CeafL6Hk8YoqA04Hm+/dJCZi7SsF0r6zfPNNsZLivM8DN18h8KYM3eVY3CwtBldE7atz+oprQ6K/yGUbblHRQwuG3sH3AHrCKLPiUrMIi0jWc0IpdPZRZFL7SnMULfV05QgcvGySOJCuQU+/OYzrRs7umOjwpqvi9PyJYVXBliiYfJutvmfpFAhQj,iv:KnW9LYNzxZ6r3Q6LTeOrgSLLPgiJlibVp6YmTEKBkjE=,tag:KTL1nOWdNWDA4dLVaA4//g==,type:str]
|
||||
client.crt: ENC[AES256_GCM,data:IEmZe505SwkW3U5vmRtFxR6PpuSFGWEHflUZwyhSODzYG1zLM8olVWDtH+yUVI54kLXxTtcpMkKrTBjpAxHG8zUTLPdTCVzPMq6U20jH4PYFMzoldnSgpRl/eiADUmE4I32CBbPdM5nkPvjVEYMmRafEGyJctjG6Shhh/sUO1yE9A+h1R7o3UCRPttWTozSKN4dPi75v1TSAOLyXqvQYl5Ea9iKW47ea7Q+XTcoKfMPXsTJp44kiZnVW0VT/7xNydR7AVWJJzYltY5380j0Ho+7PfNd+QqmdodkaZPS4b0PaoinMiZ40JnChGVhdS91j3IkUlU1g/+G2iVnPR9KB07xqRubqiASxIvpSaSCeYl3rR7+dXYX18VpWcWnJ6XoREoqxJiqmMQWYSROYDTd4G/zs4Usuh5r0wIodmCjud+g89mmXFtH0PbsMCVvZDUswjVfeT2PUBm036TXlxF/1aAy2GDqycbPgJgDLyw9tdiQUZe2iA+Z/Wx80qgQjmmqCaw77cDergLFj9KnByl3Tinwfm+IWFeQiDW+fwSzz+/vrpl55BkSVkVhw7r2Jd6zyg4VtLVXbxYhzdNz695N+fjkZHuPE5tCwr4WCzAg3vSS9hA7IHo8k93WO8s6+KlyjuwTdjPIYZghebO4ktwdqvhvuqxv8Nzwpc5J7Z2H2IChjXg==,iv:l8BwxxEfHfYrm+mM5UE6Yj42DeeU9y23jjIE1KjIiME=,tag:HwRT6/myIsxc84BYDj3K6w==,type:str]
|
||||
client.key: ENC[AES256_GCM,data:QD/Glf3lFslBEdEoJ3CNrYzlw2HLCvGSeVuJLtxnFne2aJOuw0sk/e0h/kSAxvG4dZ+LIWJLLbqTjKHOAhkvS/ZOA7GSea4ubNkvTkw8+JBRuYwb2D2iB8kZhlER/CpiGkYJukjkgGP1CzHdvBY1M88pW9w6rH74MD3jY47qExIaNduQj5vkUQZ8ajqkd79l0nk98ZTawfPG4AFcEtAIKAPIuubT2GZ3qqNLjOI7agbtRv8xI44hhb2wj5J9xdl1GrbOWwG5EOo+hLNrcAAOCcWBpNTUCKBy7mrk2ID8jX3HsFWN76DDlQzkhcphMhlM7g==,iv:49HGJAEgllEkB3YSiZLmbWs/vlbFbY52kiKesdGVC/Y=,tag:g8YZ9at3V2l6RFRfvO2lAQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB4Z0h0R2dhbDdRcFJydE9o
|
||||
R0hWeCtwVzluZWZlR0VtOXcxdW9FbklOQXlzCmo3eXNyNlFjejNETkJiVVRVWFhr
|
||||
a2pORnhIQTI0UUk2N3dWMDZJcEhYU1UKLS0tIEduaHlmMHQza1A0TGVZb0JrbTYz
|
||||
MWJwQjcrdkkzQXhOTmhycERsM0FtNVEKG+gBRcvUsyfRzuW999EQNzyDh2/5fY6p
|
||||
8ab2xvTOJPiMUBTBuQZsA+iDImytUO/02ROQsbEZkALijp1nqrsV/g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-07T05:37:26Z"
|
||||
mac: ENC[AES256_GCM,data:vu6ltV0jYQNGV4eqIcqAp9p2TJ8ULBX5JBELUgm/nA9GX+I6Pv3fyjpcHRSWY0HbLvCwgXd22CG4IYcj4HJFtQGZR7OcQA5WM5plZH3RCPdvPNix/798vkETfpJxorQjlHoq4nlr3dEmUCbzdtE07qV2W1M6GJr4C9xKvB947bQ=,iv:aA9mjZCFAoLEhGqkl6NfyBZ3x3vWGfLYYAEe8jXuA4U=,tag:Drdiji33ckDd8ildpvSEXw==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -1,23 +0,0 @@
|
||||
apiVersion: ENC[AES256_GCM,data:ECE=,iv:bISz4HovH++X7DW1Qj8Cw0L6+EvPB0+68hh7tfyW5C0=,tag:w+SjD7MsfeIuSf62n+Zl7Q==,type:str]
|
||||
kind: ENC[AES256_GCM,data:AxXaV3Mb,iv:xfJb354Rrjz3zLctW2i6hl40yh9EsfIqKxDvmn8jqnU=,tag:1xQ/RR3KhPLKCTWCYjebJg==,type:str]
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:gdQ4OoMYKUPEbsUeA5OI4i1xnh8=,iv:IA2kaCyqQmkYpLolvcTF4aleh+yd/ImXJMhRMvpGCgo=,tag:8qyYYw4EhBKKPzEmpepAeQ==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:/mOSyXWmRg==,iv:cpeHUTlMqlJzJttGtuR3DoiMtvVqFmDS0/5Tl7K7c2M=,tag:q4GxWgWn2wQJxJHqnq4WQA==,type:str]
|
||||
type: ENC[AES256_GCM,data:92OcrsuV,iv:Z1XBy6iZ6unGrK4/SSdDa58pbPL3022gU/f1AOM4uvc=,tag:cd4wWRq/ohm6BJ/Eo6HOIw==,type:str]
|
||||
stringData:
|
||||
REGISTRY_PAT: ENC[AES256_GCM,data:zDKruOXhzsIcFZTTp4r4r9rwHih9uf1cCp06KZ6eIXSIZDvR6aQY4A==,iv:5Vch5Z7xn1KkRxrgOs2p/M58nd6WhtcuUhoZDCi8LSY=,tag:14SAZIWGOxJMqLBtOorjYQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBIaG5QVUYyNjhvaENPcUFy
|
||||
K3RSSVM2N2hTeFhNK0M5YWZmODhzZU1HaHlFCmNGZWdFcFEvanVwcXpGSmVvRHVx
|
||||
aG45c2lBY1RuSTYwbDZTVk1QeHNWOTQKLS0tIDg4TnVMNjNtaU1VQk5zQjUvU2hM
|
||||
R245ZVdqc0ZWWVhJb3dOZ3lpU3JjUlUKawSg09ZPq8FKx5tvOVZZ+K4yh7eTQsUp
|
||||
be8mWUpS0+eEmNqh35BwU3HrETMQFA6a1kjVp30JOMtqa5rbYlzF7w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-23T23:14:53Z"
|
||||
mac: ENC[AES256_GCM,data:yzi6woUaUCi6w9pG/eKnU7k/VfZgoXg/tW8p9joG8p7iabLWMdlXlx23m/CItw/NE0zeGWA5iZPPFZXOit2vN36VzK3kQNoFW6QbhvYLZi+78C/RWIdcpZkxB/tPIRG0vq8Q1SA+rwGjG/0xeAFh+R7k+YBTMd2XWBH3P7EI5T4=,iv:kf6MDaAVrtDvPIEjHMMLxXDSDRC3I1GpsPeJFUYppiw=,tag:NukVbGLa9EoMYmRsa4nBtA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
type: Opaque
|
||||
metadata:
|
||||
name: forgejo-registry-token
|
||||
namespace: cicd
|
||||
data:
|
||||
username: ENC[AES256_GCM,data:oRNecs0a7rs=,iv:/5EFdNqpSjoxe7XDmAEDMd6lXd0SMsRiMmBM9SIwDHY=,tag:Wi+ggoP+9/vgz9rHrxB9nA==,type:str]
|
||||
password: ENC[AES256_GCM,data:HbmwnWk3ST5SDWXwIhu+M5f/FOfFbJHaG0Q/IWrO63HRXmJTmLMpbjtWIXC9IMFoHeDZ1MNAV8A=,iv:ztJZgpaS//yYCiyr6/ubs/UiXkbGMnokvy4Pe45jQMk=,tag:WMpawc/SVAV4mxsCd6XqAA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjQmhxN2E3ZnNTdkR6U2c1
|
||||
ZStsQktkeWUwMEdmdFIyMkRYRVk1ZGppWlZ3CmUzR2JubWw2VjdWUGt6S2lpNmQ3
|
||||
cDdSWjJiYzRIWS9UVERpVDFJenFnaDAKLS0tIGhDSmNMSjZwcDVPR3U0dUcwNk9Q
|
||||
WmdGS1lyS3dkSXIvOFhyRklGRll2UlUK8NGts6a6Fv10c6gxIIPqIIqfgzQ3EEyk
|
||||
SFrMThwbdaOqLY4AMeDrg+k0Y2t1Xj/XOvdNbRCRictDwZUsptvO7A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-06T02:01:24Z"
|
||||
mac: ENC[AES256_GCM,data:imebczVzG9YWd42ER4a+w+Bi3dxbN0HFm/1igxzJt6Z0HG/q6i0s59swpGVruk9dQJWrlO8KKqX739ATp2KGC5szrEuRFvPemF594XFCg6ykW+ZeP5VtTvpJg2nHv7/Pu+71ljKKi4gqOJ4SJDn8owAbSN+su6PiT323wax930A=,iv:iG/G2+qQkvbBU95w/GG2PrbAHDOaamuvjqMDBxSW6zI=,tag:wy9Vn72HBDHqCyzTcV15/Q==,type:str]
|
||||
version: 3.13.2
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: grafana-gotify-bridge
|
||||
namespace: notifications
|
||||
type: Opaque
|
||||
data:
|
||||
app-token: ENC[AES256_GCM,data:MnkKT0/ubJctmLTFXjdCZjNlz+KgQLsPwIKCmPF2weEtMw6ieRfb3+B7OUlIiDqpktdPaJA/znsMOYPXT2aXDqyKquA=,iv:vcAGkgfTheoViZ7OzJJhbNf0boIbRBZLr60ieW5DhLk=,tag:iBDJEjGScxHopkNExh7jDw==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBYYWZYNFBHbVhRSkZoa3dH
|
||||
dWp3WllyVmRPcExPTndZUlFsTG1YMHN6WW1jCjRiS1M2U0tIM0tCdGhJOUQ0c2JW
|
||||
bXNTTExDeVVmcExuNStldFdhTHMrVTgKLS0tIDN3d3JKSEtIUUFhVHROOVBUaUNO
|
||||
UUVGcVVkOVJmN294dE5DS1R1UkxiZzgKx0MEkIrFsO2dQwjY2410Nj0Nyy3QgC3L
|
||||
T844m4WT+mxFwB69/y0luGkn4aXMS3xrHHWUC+vjAkM4Unp+MKhhrg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-10-27T19:35:38Z"
|
||||
mac: ENC[AES256_GCM,data:Gf5jnvWP+jBRc8fvQHyrnHnTDaaUTcXQgoOqm7Drax5FpBYEHa36pdKaUq+XR6VBd7ch6Ia/f7YavhS47ybgR3jn+w+trMxkpwtZhm5clw2Lo3Hm7Cvi4fG4qhcjm1C8/NOnWGsi5Qm+TJqyG509p9HNTvM99SzElRovPZldLL4=,iv:10hA6nq0WIO0a2I4ZW6uPGXPjesFgxrwmXohNO3FZz4=,tag:bJ37e8jvgEactq6T/pM2UQ==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
@@ -1,23 +1,28 @@
|
||||
apiVersion: ENC[AES256_GCM,data:rGU=,iv:w1yJeQvN0MPZlPx8VQZmC6zkRaHW4rtjjROea0Kx9X4=,tag:qgYgVUGMQ2BtN0RZ0nZYjQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:wp2QP1he,iv:C8c/jKh5pnBOtHqXsTEchGf9DACU/5hOJ0Mp/+Jk1kg=,tag:QUZNeguRXdi3JO6Ov6GMDg==,type:str]
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:jVsnAcFTlRiKxDoU,iv:bWEqo28kH3bxzfI3ipiY6dsqhIcl9wIxBjFu0mxVGLg=,tag:PVWTveI8xhvLxvXPldhimQ==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:ZHnVZyd3wA==,iv:3evUnFTQfRz/ZilUQIl/9dsCba3nsX6o/iQsGXMBfgM=,tag:+9kp+VRu1ksroDTbTGmGPA==,type:str]
|
||||
type: ENC[AES256_GCM,data:bHX+Bu9o,iv:8qqNddsuw4YoHAuJfUcPIqjcqNySIh5Ln8OhsfZxuk8=,tag:P7QGUXo9Io2Y19YqlWv5sQ==,type:str]
|
||||
name: grafana-oidc
|
||||
namespace: logging
|
||||
type: Opaque
|
||||
stringData:
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:qW2SQSsGD0YupU2SMzRaYwkLRjXAwV1OJBnhATpCJrOHWbEytw6IphOmPPd3VZKfHncekA4agXi4K3rUPHIx0Q==,iv:NCeW/UfIl0RvHV39ubN2mDfFp7GTQNuFv8KiEF+7KOE=,tag:lisx4h4PqUfLwBX8ca9Siw==,type:str]
|
||||
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: ENC[AES256_GCM,data:fGqA1ofVme1lVLxFjWIiU7sbvE9ip2Af7C1ua3+HdidlcVgkwW59vVlXZhgBRdoif86C8LKQSyoCGUqiO+740g==,iv:6Kv2ybbJg1GT8DLqz9P5y+E1zr7vcIBDMUErmvxNTaw=,tag:0wK28B3G/owCVPPqb2sLmQ==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- enc: |
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBrUkxLMEV3aEpILytic0Ux
|
||||
V2VEV2EvcENJVnNNRUtBbGkwT0d6eUxYeEFvCjZPSkU2MG5qME5jZ01mRVJwTUFL
|
||||
M3M0V3c3QXBoRml1Vm9NWW5JTlNRSG8KLS0tIEJnNlFmdFJXb1BPaVJzTmo1RDQv
|
||||
VXZOVHRHUEZJRUZ4eGZGM1RxSURyWXcK3ZyYCPhRUvpvT/pjPQGJoLIwaktZY6Tg
|
||||
LS0cIDUhjPFbeu7qUATOrat6vMUi0UREbXSFZ1KyAjNAwJZaUusPMA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSArTXMxMWdVRHIvbWh1bVNk
|
||||
bEFFNlgzNU9zTjlzY2o3VGdnbFpnU2lkYmxrCjF4aWVGSlJaNkcxODJ6RDkrVFo2
|
||||
UkV2RUwwVHFCd0xMNG9RWXdTUHZGaXcKLS0tIHVaWjhaNHRtS2c0UkVzRk83ckhj
|
||||
OCtBVFR0d1F5amVhV2E0emRZWThvYjAKFW46i9brIL9ZoO1CEn+IewIdoUhvYK/A
|
||||
RqzZ6xV+mhlDj0zC+SpVcEm/Ki83nMYOj4+vQingZlr/Kr3Nwb0iLQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T18:02:01Z"
|
||||
mac: ENC[AES256_GCM,data:yUBBV7dLEPld9yJacn+YaOT8M1NJ2ploGIxV66aVOH5x4tDAsskcUQJlzApWCQmORuCuMKg6qfpGgLTojfD5msha+ufVVJXHl7ua+STar1/FPJXaHm+z7YyZdsD5j6f3AH8q18DuLy2DYLjXELmPBsp50g7MDJEm3xLhCUXygjQ=,iv:xl0OtA/2umz0YH5b+6QjlAByZWbvfGOmHqomziNjlkw=,tag:xwkQDZx19Lf6QSwU2zPYTQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
lastmodified: "2026-10-27T19:44:32Z"
|
||||
mac: ENC[AES256_GCM,data:EJew+p/kJrurcNpLlFxJGTl69hQYxn3IkzP7EDtdSriW/WI7X1xNLC2dp/dTiLomGmG1uh6+2JI9Sc8gXmrRAWLELAbiOaYbcoU6gi89BjTCWGDAMX4lNSIuH40rsGs4YtYj5U7SGfgx5qto4ZH7LxjBmxzPWM/Xz2Uq/I5jV4k=,iv:LdhjhwCfPUIa7D0gved8Twf7ujN+dg3lN8lCnFBFs0g=,tag:bjkwUCMtR8IBL9BkbuXcnw==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
apiVersion: ENC[AES256_GCM,data:uiQ=,iv:vknDc16Zrz/lxS1WG3ksG4SHjhJSTDdklVxxwL8eylA=,tag:1srpVd6QTYQGGGYGXT8Cbw==,type:str]
|
||||
kind: ENC[AES256_GCM,data:K6EPYaNE,iv:VWzawfTDNlag+shjBqwSzZGcQQq/TGxeHDqd4fAh5Vc=,tag:ncPqMYjq/73oMzphWhuuFA==,type:str]
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:vaRNQHFYfdUz1hzuYO0JxQ==,iv:W7aUJID5mxoO1NQAcyPDYV1FGtBT8myKVMA3s2WgrPk=,tag:4Ef4GBtTR3dVanscvigRmw==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:HDlmfGwad37/,iv:JFPXxz9xp5vDJRcWy73SYGWDMyHKWQq9Bvy/NJTwTnU=,tag:2ldVXgzKn4xp1rZI/VYgUw==,type:str]
|
||||
type: ENC[AES256_GCM,data:UgolDLIV,iv:O4himI90UVwx+OjKJXobu10Zg3a5IoQcjAORsdhaWpg=,tag:xltntRsamhQkPEoBNtMytw==,type:str]
|
||||
stringData:
|
||||
oidc-client-id: ENC[AES256_GCM,data:RLdxXY/a,iv:US/fBReK7u8WA4IS6TSzYiHQkte2CuVgugIMpA/pIlA=,tag:QKrHGQrzI9YpfNQD+ts68A==,type:str]
|
||||
oidc-client-secret: ENC[AES256_GCM,data:SS+I92xEkOcJHTfWvZ1AA8hh1bFAtND5adZ6Uh73nIPiqAo958GSag==,iv:9HcmXWrGtflc6WWQe0n/pgB1Xveakofc3hvz5vuE29A=,tag:0emgeyM4GBftVKn/fx7XPg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBCWkR5V0ZYSTBSSU1OTWF1
|
||||
TzFxdnZkUFFIcGJWZ1VUU21ZUnU2cUdob1drCkhVTm02dkZPZ2lNWktOcWZ2c0Vi
|
||||
dG1PSmZBR2M2anA2Z09iYUU0SjVzTlEKLS0tIGF1eVBBUHdhVk9YSzJlQXVwd3RE
|
||||
bUd3UTlwR1NiYjNlcGpweTBhdmk5b0UK8pwUoVCr1uBX9dQ7EGRgFMAPk/z8N9Fg
|
||||
6GRUORFug5h7x5kjXK9sU0yTHjFSekwBi75GLng0IrLEGJxHVM5wVQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T21:07:30Z"
|
||||
mac: ENC[AES256_GCM,data:NupjQWBJHMOo3/VX5n5nRx+RFT14N71XkrjXRC/VCP7i7H8yn3tcRdH9g14usgWunmqg2KKpGMUIZ9bsQ9Vds9sBvUKvLyRkfQnPwPh9+PFCF3ZZLPgzqcT19kg4I3QabmO+LPzQXsZWLKIqgF9/B60EVIgYyKhZmP7XblY5AVo=,iv:r0deq8+txE8TwzdtxOMaIAZbZc2fu+Tp4Jd2HZGbkhM=,tag:qMpZp6LcWjJHYMDp9ecBkA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -1,23 +0,0 @@
|
||||
apiVersion: ENC[AES256_GCM,data:FO4=,iv:kKIuJp4uGeX6vECsXk7w43F9S281jObQchmK8r1+kmA=,tag:x5LYd34dzNDLCma/78M4+w==,type:str]
|
||||
kind: ENC[AES256_GCM,data:o4RTN8SK,iv:uyOJWjDmHIPuBl9Wc1S/HxUabV2yytWr1bntHW68sTI=,tag:3On0SnQVJ/ICMMryQe+8JQ==,type:str]
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:a89dHmLdSeMHE7ZP0Q==,iv:OWJvJhFqJ7A0UGVJnZILzCJ/4VuVFib9sUDjXuwyqJQ=,tag:1XrIQn1GDh8RQ4vmQAu7+g==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:4wBVpZJgQGM9,iv:3Tn1u/2TP/+SnELu34PDf4Jauf2L4f8+MtgrPIlyKzk=,tag:L3qbKFBjm16g8KX5vUsK6A==,type:str]
|
||||
type: ENC[AES256_GCM,data:mdrF9vUb,iv:7ukOgwXlnRf5mpJ/+6YXtyGRonnqehEgT4pJJIafw7c=,tag:90mt7jpNb3N80kCEi+R/rA==,type:str]
|
||||
stringData:
|
||||
db-encryption-key: ENC[AES256_GCM,data:NWJryAEd/eUJxbNiu05RzLF/l7GDxIQM7RQe3xKBz6XklgRAFwQxxjakcVd85pePUBaQYz3zttFglWhfJlWhkA==,iv:eUWbcs0B/mIheyAQ2+DyKgcA98CRKQGAVC3O0WIkfU8=,tag:be06X6AQqlohYlxuYoZiTg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB1Ym5yQ1pmWHZjVnYxbUVO
|
||||
SkZ5dnI2OXR3cmZjZkM0K1ErMElLNktlK1JFCmpleFBrcXkwcDA4cGFuWWN6VDQv
|
||||
dVV4NjB4MjEreENVQnZ2ejVLblRYOVUKLS0tIG9VbXJvUmQ3SzMzN3IrRDlac2sr
|
||||
eGZNYXBoZFZDeisrVCtUMXVPaEpoNFkKjwhUU+Zsqv4Be+m558fET5UhF/2rO73J
|
||||
AhGbJVaA7PaOCswPWiOLso26zD+MBB3VKHNdUFnenpoM16Nu4pOf5w==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T21:07:30Z"
|
||||
mac: ENC[AES256_GCM,data:KrGQY/eOmihYCvQCbfcM6bZ6rfx4zKh62M8NM0VQr1//1dSHpOOh25dYGLuQeECnqLxSpGQNWzr2xjUYWs4NOdSEFGq6FL2iHE3LnqoBZspp/JFJYP6+z/8jSUojwKEW7OuZZdPN6gF2u+IPOeWbGftnjS7X+atoSkYag2ty2vk=,iv:IOPEgBizsxrVoK5oSdWmi0uCBUnReOEnMYXuwSBKTzA=,tag:9k/XQ/AE7wSFq5XMbY0FTA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -1,23 +0,0 @@
|
||||
apiVersion: ENC[AES256_GCM,data:4OY=,iv:0Lgui0+X2oTlgCX5HEIIihEsGJD0C2+pDIOTCNi4r+g=,tag:uL7vlreaHjcLJjV7e5I7PQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:x/ltkind,iv:KRBfS7COsTs/l3Fhyk5FvH7IVc+r/6M6FVOo+kPDe1Q=,tag:UuTbe213hDfK/WUtKS/doQ==,type:str]
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:LpOEecTJBuGy5PqNSFM=,iv:Q+DCLGtw3wihhdAgEbzYHyQhFTPWFyE2iJKl9mYaA2E=,tag:E2wrig4lRXGDvSCHec5clQ==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:6kMVCXaF68NO,iv:gnCCkV7GKXA2HWOOeNUGnpAycf8U7pl6F8Vfo6DhLR4=,tag:0VdeySsuhUr2heXyGXZwBg==,type:str]
|
||||
type: ENC[AES256_GCM,data:6muC3Zqh,iv:gBaC+6MXvZY/bIpy/cEG/zWwLBaUFMVfxAHa1OnwfnQ=,tag:v6NYyhAKAwovluHAXNaOVA==,type:str]
|
||||
stringData:
|
||||
SECRET_ENCRYPTION_KEY: ENC[AES256_GCM,data:jX9ULlaBYTSTFGRMplSder5rMrsz7THU3kgJSGef0r5rZvtoaAG9NVYU2QUEuD0sZgfqRooeMOkDCAqFc8qsuw==,iv:uOmx8w3XPKnoxfasf7gqi/bljxigpLwaAC30c2TroKg=,tag:Dmbu6m3+UZa1w0UR0LRh9g==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB6TDVyNGRtYWgzMDYwdjdr
|
||||
NXJWcGJZNUx6YzBGWm1nWlp0SlN3WXhaR0VBCjZ3bDBmUTFCUjg3U21wak90eHZK
|
||||
NlRCTUZQSHhoS0ZSUGZ4UHhST01KNmMKLS0tIGUxcGo0NmJ2bmJ6eGJ3QWxzYVM5
|
||||
c2Z6UkFjQmZ4ejIvK1JQQkZ0VFFIZ3cKA8UzmGsX+s3yOermOHqbnnEtekHCTqmC
|
||||
R3Jyzf6OolZMJpNwbetcsOfgO61aM7LHl/Xb/hFLbLjZ+eECrt8VpA==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T18:02:01Z"
|
||||
mac: ENC[AES256_GCM,data:HTrEEVDm8JqnVBFaY/FJknxma0nI95tRiwVc0K76k+2R4jgKCBA3EUAzPVS+gA4rTAeKHPSHrT2eidlkseT2WQUjqMRrpHcUsbSjNfpTEY2g/U0PIiOXZMRGfwmsQVgmu2fqeMOyzDSyya0scdWJXPraRPNZ+OpEkNVlm4rWcgs=,iv:KY3dnnmooJgsu66wc7gbzeIGUXZNgVrxAhpAwxoG+G8=,tag:7r6d41oTD136N57rtY7auA==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,28 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: igotify-client-token
|
||||
namespace: notifications
|
||||
type: Opaque
|
||||
data:
|
||||
client-token: ENC[AES256_GCM,data:W+InmDyZkU0i+OqQUsIaRNY/3p6zjMC19ASj/qYHTldhtGmHwj4koQX3tIxdCQV72xZKOxsc8eV3hGX3pqbW02c2/ng=,iv:wWI5AZdpxnzX61f8iTiWmxKcDFMaFM4PHFYo4DMBK4Q=,tag:CpE8Kg5V8fL7P3oAjWjSvQ==,type:str]
|
||||
sops:
|
||||
kms: []
|
||||
gcp_kms: []
|
||||
azure_kv: []
|
||||
hc_vault: []
|
||||
age:
|
||||
- recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSAwaUlFU0xKaEs0SWcxWEEr
|
||||
T0tUSmErR08zVkhuLy9VMEc3VXdmU1o3cWdrCjhuOW1yTmZPdWxDZHhMeUZtQlcz
|
||||
UmVzaEMyVHZOZS9KVHdMd2pndUJwUW8KLS0tIGxYeENwVEFDdUpDTGszdGtNRHBQ
|
||||
Y041WW9HdTJwUlJCUGhMN2RGZWZpUmMKiQbi/O9X3cFJgqw3tvwWcyy8QKkUuq/7
|
||||
Y+V51R/WFobEQuAZLmDKgWcPSGt51effufB/Vm9LRRqtbkbYQAIoXQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
lastmodified: "2026-10-27T19:14:20Z"
|
||||
mac: ENC[AES256_GCM,data:Hxl7AGzMOMK0pwe6T9ZmrE6K45XhQkq4qk/7YkrVuneL1e7jc8Bmv4NlywqRkt9fPVOCMwKikeX9U3ZcsqK13xPPfm1CmAxV3CCluwK0/9589N70OQxL4qJSkO6CXyGaa0Y9wVRX4y/N3wSjW+d25+GOYUXrOxkRpVyU4GFSqAQ=,iv:0Af/MCZBEYJVI3yi7DpPX1eBKwSANXJPCpSAEE3zCq4=,tag:SS1U9TfRQR3z8KkoQ6ZuZA==,type:str]
|
||||
pgp: []
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
version: 3.9.3
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
type: Opaque
|
||||
metadata:
|
||||
name: memory-agent-oidc
|
||||
namespace: poimen
|
||||
data:
|
||||
CLIENT_ID: ENC[AES256_GCM,data:c/RvtV6anofX8h1BmlKu9w==,iv:3lQ1BR8kVvF1RHVfKstmb0Y7u/uX5OrvmGiCi/xTpjw=,tag:9JC2WkfQFWaiZroAEmPenw==,type:str]
|
||||
CLIENT_SECRET: ENC[AES256_GCM,data:I258/6O43PccyllYN/wfoKvTgNgZONTivM/KFIr8TGqSl+2XQkwBXvK2VQc5Xgc5cpwlrgnjk8pjQbqr,iv:MZyA+V5z9Po2VoxqvOSlWh1fZp1lrkcbYMx7VcSIaJs=,tag:gXH0BpA8KOZiVd+4RnhRzg==,type:str]
|
||||
ISSUER: ENC[AES256_GCM,data:GX/uQPDoeJWXXQZlzD6oOIvxo7H4LRAv4MrPAiCgmv6WxW59+KKkymuDT/T9MiDm4bNhxVPgB7nHbDcMHfPvU2zCNM2TWYUsNOgGwxNAqM8=,iv:5OEsdrUZvMLO2C8Ea2i5alsyFGtmEk3othTemk8fOk4=,tag:8CMlRY2JCe4z4fv4WuWjRQ==,type:str]
|
||||
TOKEN_URL: ENC[AES256_GCM,data:KeU1pQzvQ2/1lQhOqNtcR7t5TGVx1J7NY8ogxCXS6IuHhVe0zWYIbC4BYFc2H9PAjsMJwCCg75N1FrIiCYBvpfK8ln0=,iv:NbPiJC/B7W1Zm+JwnEGDikE2cb3L4l6TBeQTKEb2boM=,tag:FRmllFLZzQW03lsCu9mAUA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBmK0dPSmhKT01FSitDdUNW
|
||||
Um1XQU9FVnBUeDdoWXpCcXRzVXh0VXkvNGxNClZaQ3R5U2NkYTJjaFdHRWhUVmFS
|
||||
K0tHZVRFRDZGREVRZzZqcWJ6dnRpUUEKLS0tIE15UUNtMStKbWwzb3dEVGEwN1VQ
|
||||
cktlVTM2SUhpOGJZMXJOOG84RnhCQzAKOdM0pThEaN37SJtRrKIivncMu8Z91Btq
|
||||
es6NOlJdBUhzIhnEJWfDzZ9aA6I/fYzugKbC+AfUwdGgThAgHx9crg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-06T02:01:24Z"
|
||||
mac: ENC[AES256_GCM,data:/BTQ984D+hsk0LmnjD3hxopB65NAR54LhoAPNcATWA7MCTJXfdnYRu3gNxUzyiyCBd+rDaPp77TBnak4jVjMnOiYJsYLj1R7hrrhTZopz8pT+YNMupperwsirKZwK8it4Mu+fnpCyox2mtGED0hk+Nh/b6IteTRt5lS7noumo2M=,iv:RWIrVkNYj+8mqMSPiLueToexabzAtVbIR525m6bUrlg=,tag:5+wcNoNYsjGKpFlML+N70Q==,type:str]
|
||||
version: 3.13.2
|
||||
|
||||
@@ -1,72 +1,72 @@
|
||||
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:xs1k//nWy1iJ52Q=,iv:NDGvM319FCypxFLWwONcF12osaU/EL0IwD0c2xtqYnU=,tag:6IhfISF08WQbxrUmT49d7g==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||
name: minio-creds
|
||||
namespace: storage
|
||||
type: Opaque
|
||||
stringData:
|
||||
config.env: ENC[AES256_GCM,data:3RIQ1i6NtY1Tv2eJY8JXSgfBEqi9px/9TJVNrPf9+brtGy9Mx3SwjKVnjGXUcA2cYcNZb1JR7temXmznXQdsRfZ9yohe4oc1vL3yPGPY9YfP7AHGYj9E7tghu/rgkHOf93RmfdKv0Rrq6miBcA==,iv:0RQVMi2mMHBRTJ1OX6IDFvq9Qr7zpqgQzt+MPPgBIHA=,tag:HNXLKf5X4P3E+hc7YbDtNQ==,type:str]
|
||||
config.env: ENC[AES256_GCM,data:1yogGyBdwrlg27H8R5Kr9VcPu+xwS5/nPhi7eLcmVYtXmPlNokCsWHA70qnn/+I6i4ODys8GML++VeZ/lmcHiP7RIbY+G6xmw6nfBCXitgE8kQwTPsUUzn9bobOn4Sr1YMBf7FQ/Q0N8aupGJA==,iv:EfCdim4TLkP/FP1u1yoKZCq8YGFzVh7ca4yu1+WzXwE=,tag:rTYe0SIAEsUhh0KHQIEKNw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2SStVR0lpOWs0VnN6dkdp
|
||||
RktMUmNRR2diTUtiVk9tRk1wSjVTMnRtVlJBCkk1S0dwNUNHb25RQlZ4ZFRocisv
|
||||
aS9kbmQydWVmN1AxU3VvbFZsS2tmTHcKLS0tICtRY0ZKeFhCTUdaNW5PSFRNZ2dG
|
||||
OERIZ0xMbHBvSVNyaGxCVFpqRzlxa1EKbrjfk/CFlTSyJYgTzQ67W3JyR1fwv8VF
|
||||
dxGeAgLY4EQrf+2PbODtazRFreS+sl7dxq+rKMOVZTsjGHakkqHy9A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T18:02:01Z"
|
||||
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-26T04:03:16Z"
|
||||
mac: ENC[AES256_GCM,data:gxu4WcJEuFgjtWCfAjose8R8a566/OLrXoiJmKfcvVfTFDTdGM7I4gzQVvpjEfra5Hjyt7kvLlaV2K5bKJ0k6NXbbmgrqAqEOEcPg+cnPy1FtQGAYOo3VzgKqrpLOCs/7wh4YR23XHHsjQlHAcP8BmhrqnpHXR9FMZObrKjnQj8=,iv:KJZfw0n2DOtdZHeVDk6TrpO8q57yD+xdEVt9+hM0D4Q=,tag:rHQaMjvQwaB8Am/AlTKq1g==,type:str]
|
||||
version: 3.13.2
|
||||
---
|
||||
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:XEQHsQqTgwukYw==,iv:wTw2eoooDA9EfB2df3jzb0IWUdtLSwcuKzx8yDBaihQ=,tag:oncXt2gM1k6Qppafpu/D1w==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||
name: minio-oidc
|
||||
namespace: storage
|
||||
type: Opaque
|
||||
stringData:
|
||||
MINIO_IDENTITY_OPENID_CLIENT_SECRET: ENC[AES256_GCM,data:IYBfNW19cTo8WkCGT4zuCljzi2bkbYWJ8DY/yedR2MJNqEO+bMVvjx0OqSqbPp6rnwGyMQOhBN0gXPbEVRDBGg==,iv:I1aGIikbHTKvh//4Inhp7UoNfhjlIXSpcyP4RqljvqM=,tag:Zh3Vh5DQYAsTtiW59D0/QA==,type:str]
|
||||
MINIO_IDENTITY_OPENID_CLIENT_SECRET: ENC[AES256_GCM,data:eFfwW2ZnV+o3/6XHjUvyFsDbaIAdosY2PXOnRdN4Ie49YDIjfJPe+Mg1FsbgoEHEjuB33hfxesb4TWOJQfW8Og==,iv:+kRr9A3auts1XHgZWa+t5b2m4lF0x9dVhiRpB17fzv4=,tag:tH/jszFzLQdqORGr9vfTGw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2SStVR0lpOWs0VnN6dkdp
|
||||
RktMUmNRR2diTUtiVk9tRk1wSjVTMnRtVlJBCkk1S0dwNUNHb25RQlZ4ZFRocisv
|
||||
aS9kbmQydWVmN1AxU3VvbFZsS2tmTHcKLS0tICtRY0ZKeFhCTUdaNW5PSFRNZ2dG
|
||||
OERIZ0xMbHBvSVNyaGxCVFpqRzlxa1EKbrjfk/CFlTSyJYgTzQ67W3JyR1fwv8VF
|
||||
dxGeAgLY4EQrf+2PbODtazRFreS+sl7dxq+rKMOVZTsjGHakkqHy9A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T18:02:01Z"
|
||||
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-26T04:03:16Z"
|
||||
mac: ENC[AES256_GCM,data:gxu4WcJEuFgjtWCfAjose8R8a566/OLrXoiJmKfcvVfTFDTdGM7I4gzQVvpjEfra5Hjyt7kvLlaV2K5bKJ0k6NXbbmgrqAqEOEcPg+cnPy1FtQGAYOo3VzgKqrpLOCs/7wh4YR23XHHsjQlHAcP8BmhrqnpHXR9FMZObrKjnQj8=,iv:KJZfw0n2DOtdZHeVDk6TrpO8q57yD+xdEVt9+hM0D4Q=,tag:rHQaMjvQwaB8Am/AlTKq1g==,type:str]
|
||||
version: 3.13.2
|
||||
---
|
||||
apiVersion: ENC[AES256_GCM,data:Mdg=,iv:u/JjNJfdyz7ehk8lHZRh1u9zkNuufHosyXv10Gm+T6E=,tag:pkgZVJid6kpGuy61+oEMlQ==,type:str]
|
||||
kind: ENC[AES256_GCM,data:/kWhhqyz,iv:DymezGpBHmKT3BM/CcUKEGIWZw/JXNutReSKfhfmoD4=,tag:DnA+316Z+0eqBWN8YLyv1A==,type:str]
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ENC[AES256_GCM,data:rYX7nN6SYoG5HycaNNrprDM=,iv:4uX7+Sl1UzgsImBH+qm9p8DlOfJu0i0W3bpoE7cFxXg=,tag:RROCnYBVKVwPBNSGgV30FQ==,type:str]
|
||||
namespace: ENC[AES256_GCM,data:jpUzI7BmZA==,iv:PQrfm67la6RJCRq0H7qpiY8n2hPUjcaK6gmJ/zPQW30=,tag:bp/3GYbRpYOIub5nT0zz/A==,type:str]
|
||||
type: ENC[AES256_GCM,data:fndXB4Y1,iv:EXOe0day1ScxvZ7ozn1QyHgaZgnvlPNmvSQG74aw7lM=,tag:quPmKvFTjRqtZoVUEoQAHQ==,type:str]
|
||||
name: minio-user-ollama
|
||||
namespace: storage
|
||||
type: Opaque
|
||||
stringData:
|
||||
CONSOLE_ACCESS_KEY: ENC[AES256_GCM,data:FhXl6yqTIzHsbJO6P96KQrA=,iv:gGTaSKxk721GwMQWWyymqUPZlGLidrBzWCWdRtnlOr0=,tag:IaVl8GQqPstBuUjD4N6JTQ==,type:str]
|
||||
CONSOLE_SECRET_KEY: ENC[AES256_GCM,data:s22GOXY4B/m+S7tgDw8P2gU2F15vdn1tqAp53FWzFTI=,iv:GXBhHQ8YiaVjRRMw2yG4FxdulVOuOurkLXXC0u/SFRA=,tag:pvHcD9+4qaxnvhOSkNxoHA==,type:str]
|
||||
CONSOLE_ACCESS_KEY: ENC[AES256_GCM,data:Zxr/Nj0SUKSJbBD1cFjt3GE=,iv:r/B9ULn68hsXdH8uRQkKAoQTJIJIYKMoJuWOZfxS2I8=,tag:Xo4i+IVxuqrp+qDpYQysPw==,type:str]
|
||||
CONSOLE_SECRET_KEY: ENC[AES256_GCM,data:PoIDNZ+Xs6BdhGm10y1HdL5jy47EVHfHHIJhGy/KX6Q=,iv:voAv32GoTKi9s9/4MQy2yT6i9VY4/igA5XXzehwUqRQ=,tag:DmPyk5biVvlxHjeyDyrLbQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBtUVJFQ1ErNmkzd1N6b29t
|
||||
L0VWR2tlcEIxeG1CR1hXRkw2cEt0Q2crMWlVClg2UlNhUzdJZGpLZjJtYlAvbk16
|
||||
eHk0ZHZiNzVRTWhyQzlieTU5Y3QraGsKLS0tIHA5UVhvUXhNSDk0QlV0R2RzR1dP
|
||||
azVEQzgvSFNwak9TcW1USFNOb3RvSkUKNLuW3h3cHDSSCZGe9Vyiv5m+wXihtCYX
|
||||
oJtbRK+kJF8oOUo4emI+SLGrCsWq5cG2wN/wgP+ChR1zUgRgfq5oOA==
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2SStVR0lpOWs0VnN6dkdp
|
||||
RktMUmNRR2diTUtiVk9tRk1wSjVTMnRtVlJBCkk1S0dwNUNHb25RQlZ4ZFRocisv
|
||||
aS9kbmQydWVmN1AxU3VvbFZsS2tmTHcKLS0tICtRY0ZKeFhCTUdaNW5PSFRNZ2dG
|
||||
OERIZ0xMbHBvSVNyaGxCVFpqRzlxa1EKbrjfk/CFlTSyJYgTzQ67W3JyR1fwv8VF
|
||||
dxGeAgLY4EQrf+2PbODtazRFreS+sl7dxq+rKMOVZTsjGHakkqHy9A==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
lastmodified: "2026-08-12T18:02:01Z"
|
||||
mac: ENC[AES256_GCM,data:hQfimKuhfiIiZQAyNDZQR25HZ5biU+kaWig3ElVPiqMY3B0LrmKyOEs7ittd3j3meUkPEFSmm1yYSXcdCuf6w7cwSbEiL5DG8t7QKPbnx/aRdyHAJW+1VC0e3Lye43gaHya0iUaWS/YFQcrT01oEbckLIDWPKfvA6pg2bZ4kMZ4=,iv:AydyWMVcjTQu+7Fzw5vtqkvTTeQEMt8y6snESoLUDE8=,tag:SoKklNqa9nH9G6d9+PnolQ==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-26T04:03:16Z"
|
||||
mac: ENC[AES256_GCM,data:gxu4WcJEuFgjtWCfAjose8R8a566/OLrXoiJmKfcvVfTFDTdGM7I4gzQVvpjEfra5Hjyt7kvLlaV2K5bKJ0k6NXbbmgrqAqEOEcPg+cnPy1FtQGAYOo3VzgKqrpLOCs/7wh4YR23XHHsjQlHAcP8BmhrqnpHXR9FMZObrKjnQj8=,iv:KJZfw0n2DOtdZHeVDk6TrpO8q57yD+xdEVt9+hM0D4Q=,tag:rHQaMjvQwaB8Am/AlTKq1g==,type:str]
|
||||
version: 3.13.2
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
type: Opaque
|
||||
metadata:
|
||||
name: oauth2-credentials
|
||||
namespace: iam
|
||||
data:
|
||||
api-gw-client-secret: ENC[AES256_GCM,data:YiUCk61cCsTJE8gTLZWhqVPsCSu3b4OnG+Oh456A+mnxip0FjLTTqV+kYBdfZivIkn9ua4RcVW+rYILu,iv:K08m1EaB7/SIVNWZ3FfeRP4tbCAo6KeiNC0NpE9VO1o=,tag:ZXV9YRWUOPJ/vdoqMtvQeA==,type:str]
|
||||
minio-client-secret: ENC[AES256_GCM,data:vcwLo1cJqbZr2MwkAKO09xPjb09ss7+s6HIvEfaLxt8ka5PUAlXQ37pTkynuktlKq/t0DvSk+x1+5G7QqVD1JJlwyDGM6yuOhBQ3MwucLNl+T4RDKtgGuA==,iv:sCdd+UNX2TfTAM6Zslrh8KrvBSzqpe67F3YMGrl4YDU=,tag:9Tpzm9NEXNuXyCd7l7GeHw==,type:str]
|
||||
poimen-client-secret: ENC[AES256_GCM,data:dYtCiMGZaOFokihfGD2SXJDfXrhfwEQcTOPNu7JNHDUnx0hqwAdYAPAJTEN21p0OpHYDINMs5lwUpMwY,iv:ll8USuzs0881ryHImVjHGO0iFxUzqg5XtI6Ekd+EFXE=,tag:I/4ZfB9Ss4zEP57sRtRTXQ==,type:str]
|
||||
paperless-client-secret: ENC[AES256_GCM,data:XMj9rHkI20lI37vChq9wS5lUq7l95SisRX0LChuj09O5w76Zt9NdG6OEUYQFf1z8vCfMJxBInIEEIq0e,iv:UpYy2ZEZ4AgBACEyrlxEcr13V+8Ksu4p8JHYApmyYHo=,tag:o9Z+/b6T9POgowIsks56kw==,type:str]
|
||||
grafana-client-secret: ENC[AES256_GCM,data:V/j0+P3HoDLqYjRalWK4v1hvJTuVmzxVra47HsF1/BZvWZjIyEIH3Pt8BekT3O1dUFynxjcaxdZMBVYg,iv:9pMp+vIsp/sA7GP9wqNDf5DdZVLDow46OURW0mMRXIc=,tag:i7VJFA5o0hToZEJ5JPWIvw==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSB2amZPendIOU9PeStOcmJG
|
||||
c1dzOFVvd3RoT3dXQlJUNkdCSytEZUVGVEJJCklMUkNjUytBMXh0OVUrNUlpMWw3
|
||||
aHRFT1hBZTZHNHVtaGJCTEFyT2ZWNWsKLS0tIENxR3FKMEplV0V5Y0dsZzBkODVG
|
||||
U0ZlT2R0VFdEQmpsakVvZ1FJRkRQYTAKESQcSlMAuCG0/tLWWZaeoPayf1EKxSaf
|
||||
e4o8TmjmSgPEhepQ/LmNXl6obyuqUxztkh4Mm6pocqDp+gfzt7X/1g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-06T02:01:24Z"
|
||||
mac: ENC[AES256_GCM,data:AI/znHiUa/Rwg0YRz65qUZ+b6V73oi3ZfAG/V5uZZvrGFrA0EISzD5LaP5Kl4GUU1svo4G5EI4aftPDgpdPptz21CCa8iYkXGe9pTDmtaE5ZICQAUbRcVhGMTfuWFhrgEhMfCJdOpfHaIQVEsQJQJgrnkXQVuUe9aT75LQz7pnY=,iv:KbIaYT0VeBT2WeSU5V5ZB4e+04JICYaY+egGN1Pv5bM=,tag:pPbhDxeLUx6V1XC/LPwxTg==,type:str]
|
||||
version: 3.13.2
|
||||
|
||||
@@ -6,7 +6,7 @@ metadata:
|
||||
type: Opaque
|
||||
stringData:
|
||||
PAPERLESS_API_TOKEN: ENC[AES256_GCM,data:wlBC85ep6zioWLhfhNczkKfTMR8tmciN+4r3of8GpQoJmKyE2r7pOg==,iv:Rem/UJl5wUNi8G+uEV7f2eCLabaY26sZ7+AsKYiOZ4I=,tag:bGS1fu13Mwsrr+5NUEzzLw==,type:str]
|
||||
LLM_AUTH_CLIENT_SECRET: ENC[AES256_GCM,data:co7FfmRXKgrz9fNgR10KtlHL/iG4F5WKJgjcKem1H5hF5FviVi4+CQNekA==,iv:ADtBHiI6BtrkuH1iIfKV3I5odNLMQe5y/9GGTJrPNN0=,tag:j6wX6zO5VhKoLzdr7BQZCw==,type:str]
|
||||
LLM_AUTH_CLIENT_SECRET: ENC[AES256_GCM,data:OEZqGgJ4cpMsWr/PJH50lsQKvk6sjVNPCFcZP3yQ/7nvlKP3giPlLmW9XgWr1ELq2hwWDs61+f3fAyENt4ln+Q==,iv:W0Tii/nvgm9ZMIEtfzgO5pj4Li+6fpxfd3zFtQhj874=,tag:tsM/fGeduMOEfMZsDZ6lrQ==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
@@ -19,6 +19,6 @@ sops:
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-09-14T14:19:39Z"
|
||||
mac: ENC[AES256_GCM,data:jDdmiFg6uB1za7sAjO1CnGHdRPyUnmWCMJxqHrE3BO82pNbtm8yLOf+W6CfFKAjfZYqEE2mdKhABm7zeJgJlub3eXJbAirAFrp5jrbfHw0qsFo85Gj/BsrSDB401Aky6PxvRomz66FyX5zDDXIGAMEMlyNEadMplTfrZQQ9hQWM=,iv:UTSF72ofUyWs1LXGKQwBP60hK64UxhqTFM8QTVozGoo=,tag:OKivMDAqwWgrX+kNjvQrPg==,type:str]
|
||||
lastmodified: "2026-10-26T04:03:16Z"
|
||||
mac: ENC[AES256_GCM,data:gOrqy2fPX+77303L+E1EypaTU8mdUXYjbRZnTIJtoRpZhEmHsVvS/Ho8bVcl5KiTPN7UoFNmGyDz8fpyQ/rLTbeCiUYmyxvH7ym9DT4oWvRxITpOqBxz83pWv1BB++HJvqNQ5FllqpnAz60/bAVyxOdOXk4J3lDnuJibNOpxhW8=,iv:suV68u1Uiejl0NNJ3WlAHshgdrSL+aTwUxUBeMXlfWs=,tag:5wug4NeDZ+qclvFGPiEviQ==,type:str]
|
||||
version: 3.13.2
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
type: Opaque
|
||||
metadata:
|
||||
name: pi-code-agent-api-keys
|
||||
namespace: poimen-sandbox
|
||||
stringData:
|
||||
ANTHROPIC_API_KEY: ENC[AES256_GCM,data:TBcno/dzj3qykHjeX96UKZekuA/WMQT2E7a1ziPyYo5J6lWLPydgli84KeJZhH62/8CnnFtFOhVIe4FWCGPGjvqKH6M8WbiQHaP9y7FN0qQUpFHKNVAQ2R+s0wInhw0eUmANvd7bRx8aIZ2X,iv:oex19yVtWUevoJ0A3mkMLCfWu85o1gPSNp43x1iV0LE=,tag:BW3zQnwvagPceeNird9CZg==,type:str]
|
||||
DEEPSEEK_API_KEY: ENC[AES256_GCM,data:vq/5gOsMVjlVMd0oXiBtHFHFEr+IF2YR2iP2OBFWx3ZFNoQ=,iv:QhLU/BRIJb/7QKDCRyw+HWPphZifVEhIOLywYDgOb20=,tag:BZdtXw+WsuK+P4E0caTa0Q==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqNm1LNE5nWktnd3c3STg3
|
||||
UlVWbWJxWElxUHMwa0JVMk81Z0c4d0JaQm1vCnpLQWJWV3hxOU00NzJIaG51Mmlr
|
||||
ZWpNTFJWcmdtV0w4YndQeHpvSUx2MTAKLS0tIDVnNkNuVWhPUFZ5V2hFOEt1eitG
|
||||
blVqRy8yNnYzSU1FNzZGNkxrRElvWVEKL3J77INje75+CvKXaJHC3CvONgHv7d0a
|
||||
a10MITvfreFr6M4AN3Z/mno49SDMp15Q4oQRagHHxSTS1K54mWHo8g==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-06T01:19:49Z"
|
||||
mac: ENC[AES256_GCM,data:GpYl8IRefms36GvO3RW41c0US7SOgoOhd4rumxsCdEf5ME9kJ0eOxPx8klMgnlYrkBnwHmad8jj7C+HcA5Eim7mdUaYOZZWPFohvC6N4GLHX85aH78P09CelqG5tdm/Gm+1QfxYu0vANYGCAVjLK7YbBbZjbZmbvVfQvAbs3mOY=,iv:y3lglGu8FhTHQpO1poSIKgGYJFu5J9yjIaOjulda6fY=,tag:5Sgh4TbtNFMw7xOJjYRn0g==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -7,29 +7,40 @@ metadata:
|
||||
exec:
|
||||
path: ksops
|
||||
files:
|
||||
- agent-pod-models.enc.yaml
|
||||
- agent-pod-ssh-key.enc.yaml
|
||||
- authentik-secrets.enc.yaml
|
||||
- cloudflare-secrets.enc.yaml
|
||||
- forgejo-registry-pat.enc.yaml
|
||||
- forgejo-registry-pat-api.enc.yaml
|
||||
- forgejo-registry-pull.enc.yaml
|
||||
- forgejo-runner-token.enc.yaml
|
||||
- forgejo-secrets.enc.yaml
|
||||
- grafana-oidc-secrets.enc.yaml
|
||||
- grafana-secrets.enc.yaml
|
||||
- homarr-auth-oidc.enc.yaml
|
||||
- homarr-db-encryption.enc.yaml
|
||||
- homarr-secrets.enc.yaml
|
||||
- homelab-ca-secrets.enc.yaml
|
||||
- loki-secrets.enc.yaml
|
||||
- minio-secrets.enc.yaml
|
||||
- paperless-secrets.enc.yaml
|
||||
- vault-secrets.enc.yaml
|
||||
- vault-unseal-keys.enc.yaml
|
||||
- portfolio-secrets.enc.yaml
|
||||
- gotify-admin-secrets.enc.yaml
|
||||
- gotify-tokens-secrets.enc.yaml
|
||||
- gotify-smtp-secrets.enc.yaml
|
||||
- forgejo-smtp-secrets.enc.yaml
|
||||
- paperless-ai-secrets.enc.yaml
|
||||
- agent-pod-models.enc.yaml
|
||||
- agent-pod-ssh-key.enc.yaml
|
||||
- cloudflare-secrets.enc.yaml
|
||||
- cloudflared-credentials.enc.yaml
|
||||
- forgejo-registry-pat-api.enc.yaml
|
||||
- forgejo-registry-pull.enc.yaml
|
||||
- forgejo-runner-token.enc.yaml
|
||||
- forgejo-secrets.enc.yaml
|
||||
- comfyui-oauth2-proxy.enc.yaml
|
||||
- grafana-oidc-secrets.enc.yaml
|
||||
- grafana-secrets.enc.yaml
|
||||
- homelab-ca-secrets.enc.yaml
|
||||
- loki-secrets.enc.yaml
|
||||
- minio-secrets.enc.yaml
|
||||
- paperless-secrets.enc.yaml
|
||||
- vault-secrets.enc.yaml
|
||||
- vault-unseal-keys.enc.yaml
|
||||
- portfolio-secrets.enc.yaml
|
||||
- gotify-admin-secrets.enc.yaml
|
||||
- gotify-tokens-secrets.enc.yaml
|
||||
- gotify-smtp-secrets.enc.yaml
|
||||
- forgejo-smtp-secrets.enc.yaml
|
||||
- paperless-ai-secrets.enc.yaml
|
||||
- memory-agent-oidc.enc.yaml
|
||||
- oauth2-credentials.enc.yaml
|
||||
- forgejo-registry-token.enc.yaml
|
||||
- pi-sandbox-secrets.enc.yaml
|
||||
- etcd-defrag-creds.enc.yaml
|
||||
- zitadel-masterkey.enc.yaml
|
||||
- smtp-gotify-secrets.enc.yaml
|
||||
- igotify-client-token.enc.yaml
|
||||
- grafana-gotify-bridge.enc.yaml
|
||||
# Login UI RSA keypair, adopted from the chart-generated Secret so ArgoCD
|
||||
# owns it. Without this, the chart re-generates the keypair on every render,
|
||||
# which changes checksum/secret-login-service-key and leaves both Zitadel
|
||||
# Deployments permanently OutOfSync.
|
||||
- zitadel-login-service-key.enc.yaml
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
data:
|
||||
token: ENC[AES256_GCM,data:CUJ0rUtgye71fpFz50RSDXJeG+t8Wjrm6T6jboVPbt2FVuIRSscqKg/Pc3k/L8nvLvrkYlzz1rbYu9v5NAdNF2c35AQ=,iv:tsfQ+v4qwj6H7G/O79b6m4XMWRw3mwE67f+4LjDCOoY=,tag:SLGGBYtYoF45AQstETPYZA==,type:str]
|
||||
kind: Secret
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
name: smtp-gotify
|
||||
namespace: iam
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBFWmU5TUxLeGU4YzUxRU9G
|
||||
ZFR5a2hqS3huYlJtempaQ25zUkljUDJLSENrCkx0bjQ4UVF3UWpWa0t3WHQ2bkNl
|
||||
eEp2d0dQRHJSOExIYjN4TmRTZWdQY00KLS0tIGF5enUxTmxRTHlQRUt1aTFIYTFB
|
||||
MEJaOWd6SnhWQU1PQXdUZDJqWmp2MGsKIOOrp52+d3M8AQlMw0VSnLQ2efDtC6Ye
|
||||
oC/TPqYi9SqYNxaHkODS2QZN9msyfRFcgUh/6ZXr8sivnpwRY8ZZtw==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-27T06:24:20Z"
|
||||
mac: ENC[AES256_GCM,data:9Jsnij+I1MhLhIPLt2hr1a6Jryf8XcAb4QAknGkX1hNEN+Z9HT5PSnPARqyhl0WTxe8813a/G0h93SJ0A419jIzZugv6zEj4STsOG4hMI8O2NniqOwiDWl+Az9SRXIQ0ahH9En7ssvEdiosIDGJSz31ufNc8FbwKnHvgWCwfK9Q=,iv:VNTOpQkDakoaCu2dG4lRbd+6AzimNZVNzggq/7C2eM8=,tag:cJI9WdnKiG3K3J4LRDqQxQ==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: talos-repair-config
|
||||
namespace: cicd
|
||||
type: Opaque
|
||||
stringData:
|
||||
config: ENC[AES256_GCM,data:WO+jinh/2Rq5VeGLoDDJOQL4zfYJINSTYZCwGWndbzfIHw4XRwxDB7BixjCdpPuLOkNIBAXHQpKPBhlQpvRTso70L26njgVdVENUGeNhxvb6/cwl3VYRsfQQxKoQW9Qu6DyCD7ItuyazXMdIbENKBoih7GA2IjKdGeEtpB+a6PO+6rpI0AQRIjM9h65yJ6gvWeLZmb6U0qCFW61SelHmVSsfehxH/R9EKQROKwD0Z/TgYnoS28OuDtixAQVfnrI3gtgUAeXgnPQTJf1Vb9iwbRBvaSWkPSIqa7hOqWEIM2qyrvhTsEb8XDRUHTmp4BCxYktIxoiSWZEbeh0y0HKRzekZIj4rHA+c/JYSPqS0dvn1Ga+8PTc1LNdXDWIcKI58YQ2jzA0oE2vm0YzmOjcDTzTGLDBk49gIql+AMfwoxCop7t1PB7BrJw6M4bj9M5aHIq8ZlGXDvtWZcSEBj0ChZ0q20cB4v4bvMkbuJR/PS25uNkeG+UBCPidBQqCUw37aNf8SDwOaAC+6PeHdLmcJ/dkKntGFlU5MQzWQXfqs/fp95ze3mr7f3et7SqVl7fAlYSdEt6dA2xxnFnqgNwAm3UacV/We6SRnjrFlBAHZ3Q5hIuKkxmszsS4Bh/VuNtZI5YJ4ldnoIZC3aWKDt+rVB6P+Qxp1T1XqZj8GY715DZRP1q+Z7iQ9RcXiJBTD5qWVpnkESOvbTEYv/W3oXTufVRrJ8Ps0on/xWRF5NU04+tqvToC8+MIQDz5rb+eXYCEWjZG9ZhInY+M2HRTLg+c49dRlyqVnnqfNWBejSS/hoQbf5HmkUbtzsCftvu2NwS2nOMhm55xcCsAunB+0uY+Bj4jqyqRvWUjA99yHIdQH5JTJXE4FQPxdBTmBFp8xVdYWvlnCojK4qfv5EurLUunBugdSdYidi2TKYEoW2mX19RdVnkQgBjW9caaZUX9BIi+zNhObKenoWxRORCb0dHw72mwXMaSQM4IDygcuwi/79OtkYik3juECVmI58bu/GFRo2NtmgkGdMD29SMV6rerDtYjGjetgFvWkDy6hrB/Vz4dI37eGt/EH/7G0z/IT9Q2sM8nzJDtCYdicepLQ1WaM0P4BEn2rrqhv81VvDH7M9e7wtz4xly5RYQUr5wkzvoSEIglFekPvPmhIxEsNdaBj6MlVmLTbEbv85oBLwm0ZLRJcgUsrBbZghGKQaI5udAk1dgwOng9v0Ai+ilJ2RSt2VI6WG1rwRnDG+Hv+H1cKT/NwA8ocmM/kxQlUE3BxESrxhTnKq6W0rzPInIj+xeS8v1X4sloGJIiweCCI8YrqUnmYKKQ6Rtd5B4jS5fAa8jp274/R4xvLaR2r70wKp890Iwfb948PwqjNpH/XlOUcfIxPu3jHO65OEyX3OG/+qm+g91AmxlrwZRDYeSioHztz55Du+JKX2Twnb5ttEwVm6I7eQWGbzw50Qgg6I0cHM1YRrXv/h35C8pkXxV7vMlyR0g5ELAoRWYkuveniruKNIIPBw6SY81RtjIIoPvKzNAFssNdiNxfYEsHiDlfK9kUvTLs8qREmLt1rzgD/vHtanmKh7WMtHN42tSUp+NhoHPlEyaaJLUMkFS7suh2S9xPmhisw61npROXrc2PBxLR52i8t0cJYFa1XuZ40KMCZ7kw0UmKiUxeFDTlZkaOTocVqw3qLY906argYnVdkpaoJMUXsnE3JmXch9HG1qpP+hY8LJSOl5ejAiQADHYDm8P/+9VVlNL6eo2O6qFUs9lC+I8hVeSOvJd8+aLeWisFeboKjarhw/52eXwZdTEpPHa7yMDsIQgciqN82yagJYrEL6BevvZ/kGHP5TszYYfcKcN28k9y/R2JYND2TvYeIKb2Nmj9rhwuXBOFnlep4Exw1QS0If80gJG1BCub+u6GKk9FaFHUSH/x3MIRHwVzCHRnZxqbwc1uGZ2I3rRxVq5MHo5p7htS/KYi7+cQjSdRvfdlCofo6YKP38qQwh9y9yAUqMEOABtjUaEJSeWlGq2yh0onm+Uqm54fcCSr7S5MvBTZ1oblLqVUrZBDTIkY5cdbuiyyoj7GPLHrxUo25yiGI+FXHKy136E0Rv7p5N8o3U5O8ycPdk5vOVYgvM3pdLS1zHHsVYIYY/Y/svtDUnMhJONl5AwQ2sDZ2GiWx1nWJ4VumLuwb/Yow/BHtnW6SOeWFSwRkgJdNL5mzrNoexytbrDaSWR0IpuLUbvK/Gi4MJaEkm1acQB1CMOjIymVu1pcih0H1Zl4O+fhQM+bVke6NKrBSUKErHWca9iNH16Du7iXBbaK0Dz73nIhALJKCivr1rWviXXxeQuzTFMRXdK/SalJySHM7ov6qOikQTG/Q97zQ+UYiUnLo3UxlWngXrnzGy8fAb58v9bHhEtnvCrAhiJTer0x4GYQJGwYjQDbN9ucpu00PDnQU0GqYJHqCf4Vm9Y3SYTs92T6Lftvu3nw/ZH6PblcznC6sueEPON5QzzQppepezdolRNyx4g8XYsI+XJlCNrExWxDxErLBTOm3f7aB19RYCS1tG0G/aKEJTXh9RE5VyWSyBzPNMuLZF9HIIHp3yGpqGM7xsYKsMtngBiyXC+i/C/geIgUFJym/sqWn4iPDWtd0YeELg46B5krcolTg87KEeiqDAebieqL5ZiftnVi10kV2L2XPs4ofJjpVI1Q53RsgT89zWXYv97DTZeDwSE8FY2vbADnUpGk/UZ2WMQlX8iwFOTo1woE6AHyqrq6kzGamUGsyoAarqmJk77o2ktzO2M4OqsVqJErSWTcCLZOlxsDWUn/OsQX5+9vaAhHwDHMmrqWo6cJuAtvaLYOHVB/BUOiI/MJLSYeDIrKUML3Ew0GbX89jkR45SPjRdsXQ/oVCJDxm7WjF9jd6bly2wPTmWCzoqeTJU48jPk47tDHWA7QjLEah6PajxbAs/xSN4c2uK9ltR3K/urNpOTYwsFMLgRfzontPe8RMB9A6aZcwQnIhLTpgX8fw/j+aqwqJwVcIk57/KvcOksBjN4rwPQ78kCmpLJ/KEjYh1z8EwIdVX3UDvq8c8YSZnzwsTAVfhWxigvLGzQDTXrr8aNMDXwB2iUVwhNdGw/E1UCSvsKbvw+p9YRaCAzPnnKa8AVAJySAc1Ys/DAYskdKYaTPUwdCXyZfLzfOsFvEzBxqrRgxxK77ij5UQXKINjQpAIP6pkDze+JTwWQsPfcMZ5xtb2tpiUF9FvCVqR1dfblppq3/f3ZHSl7f4s/sOM2/ZRkiI4m8yxhWDMtauxyrnkgiLsOBUcK4MclD5E3H6KtaJODFiCyH+NPtAMmaZ1iduRM51qxmrHlcI5B4ErJum/iZg+zIokXqIi/U6B+tbjj0EKugN+5V03JaoTvdaGQKNvJHnyP0odFN05ZgZnYCKGGh41EGyCZ8/lmrNbAfZRPjRtHw5ypBUiW8Yz+SkumgWA637ASf1AklCNs9MBBuOPmGckDWy8zTvUCgWCwYehIjKSQ+1hYoWpraLNOZy7tHlTQZ7G5zIFR3cJljO4bfmpAGDsSeDQtBsAI90gD4P3ipKFXVQTfl7c3CD1kB8TiOpG0xVGlYQ4dP15z1wTUfHRz82eFxfwn2fc/R/AKmINq9P5PEL31jhLM+gTxctb2BiGn8cCRuorLCg9FtkwC4jy2AluVj6uZZOGvDCYyGqFsdObgwb+FdIoCYDdeZXo+1dxlxhL1258L6h5+yJBgSy6QDN26N6MMNnK2iD7QTQwtB9CfaX3Bk6T0nZ0TT6zSxu7ix7UG+E5AKwoMVcD0EuLlmJ0IhLI+4ZZ8z/QUIzeTZ9ve+r8uyHKmWr2oR8ec5m95PGG5JzWmw30sDelFFl6S3KxsHwzlJsXeXbE5I/Z0RWZQBa8xPbmY54qYXPzp3O+9WYys6A697UzL6fcCORYokB/TR5exBS2saW4OhkwX5ss9aUJGZiMFSgfF1paxqXkGIx3IuDH5xP3QxhGxNMB+T+Yn+meApf2A00dEUng1HRtgBU5Mk/TM19g8l3nVtwTrmAZDm50jIn+G51GpeoBeYPONhGfb1Ngao383TPkvShaNujE3zsJLfJCrNda76rIechIhAC25wWauUMGUU8daL8cdtJnAEeidnolR+lVf5WFKUxvO36X84FCSaR4PqzvlNk7hOvd607h7IGOEVx7/XRLpWDDrrT6hpY653lNf3RCsxdQ7kS9e5hi/RpU1mf9mdTOsf+AtHkkog336jKdyl1/plDV/AlRS8/QjlPfeyvGmA6gEC8cZmBkvyZPbs81tYmif8q2Amd9La3wldh3evYOOkC/b1QQ3fLryoeUl5/9oVprW/aEfmcTxG9CytaFfsx6cAejAb7g6fz/iaWE57fXfth7K7ATi827ZwAs0XX9lLkyGn60ePtqg771isK5/v1jCsbDQDwHwNLehH+BX9zZWWjaQxK85/o09Oaiy0oGY1nivv5kHelmGwj8qqd,iv:a4NHhErZolhOuzNCUU51GQiCfCT8GzZ3GQ24s7fkuyU=,tag:w+GrXmaYWADTrBxMko3xtg==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBjQmk5aE1GN2dpVlREZU1s
|
||||
N3V5ZUQweWxMaVo3Y1VjdzZTYW96eFBYZGxvCnNJcjVXKzI4VnliTWFXQy9iSy9k
|
||||
eEZmWlVPQzhZK09NZGowZUVXc1BoZTQKLS0tIHIrMEpTM0lMU0w0eG02aUxJY2VZ
|
||||
aUQ0cmkvK0UwdzhqcDJwM1RDcnpEU0UKQRiTa9/yNrqlkIdlNeaIDzIKa/xvx5nW
|
||||
7wTNfjq2M3go0DMnaVjJA3e15JZBOpOs0QP9z9ydKF/FS4PeCIEFyQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-07T04:27:33Z"
|
||||
mac: ENC[AES256_GCM,data:CxZdlGJdIFFTwaeeS9LW2XDDgvUmSsBeBKtFYyDM1JliS8BHtgEv48yuCWxP5sR3SaYtcgNSIQ55zUa89jM6dQHqvjK+1jSYu1xzFWPOlxUu8gVSlU0QLS4GU1RChXuPpTUOjxzn4ymZukGgclZk9iuMvI0/U+nGD8nvs8k0zpM=,iv:MGZ7YsOsjelJPjLYWtALOrM6r8Vd1HgbU/PKL4c3r3k=,tag:dIE38TCobESY3j56NVEvIw==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,24 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: zitadel-login-service-key
|
||||
namespace: iam
|
||||
type: kubernetes.io/tls
|
||||
data:
|
||||
tls.crt: ENC[AES256_GCM,data:Ad4omL7pD4QhixW6wSVsVor5FtHbWyOHeYbxOQsEBAaSuvb7JIkvnKqo+Xy6zKuMmRViUIbqhlLQDXCaseAVGgpu3PdtShwJ7LRXJmr0XtQV1M6YtRbRexN6UpFA3XdD+JfifMhtvd29rM3WzLPX69uk4k7voYN5m0pNXSN23jVeIeqm+xB1CpxDW/guZ2hA5+b5XBzVOkMsSOSdWFWH2lK7nInv6ZatCa0/fuIKmB5nZpSV9tHpCTiTnBuNc0rW1X4HOWa8zMiSbjQAqtDitm7lrVHJyImXwBjmuS0wM35VwrwxVUUEjS8kgqnyb02JvNRdNjuwOmIcyKjrdgO1B1ft0CrAbRNkk2dYR8Y7SNhR4F4tYYF5L2szQgj+5tcYrnRGEI2QkXbcmTa0Y9V99+D2bKjt5t59Pi/1r8/jt0CNy0GeeZDvsN5scy0/A4WqtJ2sZDyTnTPwtUpAAwVC2WSqsD11gkzhyRJAgmAmEgfPIL7kTZsZBoSQeuuCJ82Ghqx9kLTpui6kLULxxUpP9oefsKw9PjTwExRbNNcPV5p7PPi9dN00TNe9iYuFqoZFP/lp5SMjDqbsV4Ie1ffcfyZsIHMuOv5Ths7n8ExYdR0zIn+aQePXASt+dE6njhFiblxyBZsn0xON3EIUaE9e3WsdcQKjlPOaAW/DvU6PwBqCeaB2nYzM2NFk6W3lN8BahTE9k7xXrILHIrXuIXezotCK6jclb5yZmdm/fTCX1iejGVDktvvcmX6bbv3YXTRZmsviRhLFHA8oClU0alTsY5Q3nn2Xl0UfXU+rW+10AIpY5WFMzP6A8hXvHx0ISC1S4AIqjmlsDmq2yEZKpbiL+Bs52ymW7t2rQ+KGhWH/bcC3VrJwUWnVYP9b80xyHZdKqubwLc5UcDzCs/Lan23VpLxSJkqb8vZP7SHQ1nc+rlwrqFDkzQ+yMNLZmutqbaAmMyXbrR51Rz3VqofraPXjS+lmLAuEcuTEFbrImsct5Zb57MtKvhHi6CUWt/Vj/NOUJlwy09zPy4JAMMJ81ooyBj6CBHY2f1vfR/UUDN5VZdfg4dUWXS0CQdOb3hx19COr4lR4RcpA46qJEjum+4icm5ckziYT5Nm7NB+ydXJ+Eys2Ba0dGRM6CIeQVhghhCDOZ6uoQ2A6DNql6wjrfUml8UwuJNO0nXVqNS28pLwddsOjlbsysJAUEma6EHXb2Vjb90JZErVswGuhQ2+UdhtpuIkgBYX/MMCZV942C+K41A9478BaCiWuC9KbnAcx81ceR+T3syARctZvQaYaiH7WfF8qyo0VOGzZRYjFbqeyHefiOmPmWTS/XbIti3evyhmxHwjUcidK2njrPB0O/3w6R69UA1p7ph9EgFsj18gPYnJthikgX8vcfVaVnPIAwCqY5DJJmIFTNXVkNDBX+2orrW2qj6PEH+go+uc+aUmu646niqaMAcrUl7gGNGd2OWFHdIRKdtogOvoVpQMDB4eWHPRsL29EVt3rnknHfpq7cIjQXuk9XtlB8zjllYdrh50Q8ZNBOvzWgCiMvAyanfIA+kW9KHQlmb1qJGYp7it2Lrg1K7sDEekrlbuoNsf+2rl8YRQz8pVBuCSzG9DPp4+Rj+ddiqX/Rrs3ufWDKFV9zJoN1EpJ8+z6n8rsQJBe/lSFLW1mLVkaPSNNdIQfdJ6MbsHpMlqqFYP8dOKckxv82uOkHoCLHrgaXiiMd1dBsX1fBiv2NvzDiuLyN81d7R2wNAQbkG9b+/+C6wVr6kDg0n6seseoCyeSI++PJIkCrkTXzd7rLs8Of9CHZQEB15wjemlmYGlJnCiFWPFa6h0jJPatPrfTYv+GpqzWV8u1Uh3aNjd+g1wVQok/ojNOZJahUSI4B67HkJI+I7WOF9C+rRCwNutqshIxYzW7CpyVey5P6Kyv+/cucDEgb0ggH032MBtzTxazpJcE4Y6fTrOiXHp9j2KjzyUt2YeCCL8UMwbwJvW9p1I5gx3ArsOuyHm1kA==,iv:PLsjmrTwjM7IUS7ZBSyVvJfAJzCSUtU+UAvHbSwoaVk=,tag:pUQwbHoZgfHW4CpPKoULAw==,type:str]
|
||||
tls.key: ENC[AES256_GCM,data:rO1NVJZP9TP96C48it4+2lGyRutwqdlX6qiM9R/+0hgYc5ceeeopaCUG/i/LOgltxRQK8qkyed/W3zU2VI6alBpt75XkT3o9ZRy3uxe8mB3ixS7+xQnMc0ci8Mf2bCtGzqwUQj8XPRP4K7Mxw8r4XO5rQLnSmNme0+BxS+XiE6hqifS16UUKfLuFeUtTEoI850sg1GQt0uaHf9kOrpmvsbg3sflO2dQXk8uSFi3B1ghG5DdWImVoc4f5kFK86XG65WVVfYjLaUvm7ZBPT0mwvq3SgPdEXeWq1fp7tMQqCRKuluRZon1e+J5SlWOPUTxwyTq336oK2Nfk3HsiQ3uCG76oCmI5yTe2tCN1f2IXFXBvLNM9QTxc/KFhI4uI6bvHjimOSpXYuRdroH0hc4ohLP44WG0E8Nz7sAb3Ai3PbDTKnqYQemvGEUTdcdbWuMK5/JE4f1IE9DgOOO9RF+s7uXTdzZAmoMcRtVsckQJEJl7AKgqSn7BbK0cjRL/QXclNySQ/qcgSDFiV/pPbGC9JFweJon5F0Qn/AIAnGGzqmVbS6x45UAJieI5oI/30OxjzF+Dov6EGMoE0mys+6l4j8v1EFCtHhhopS7zM4jZRTnNzlZWPtEISU2kopLroGpKESE145VQnB7X66F40KXcOIiXmU1H7Yxw5rJ0SqLAFWQw7gyNdzbnbfCmGASByxnYYwZRy3QDZYrQlthLd5tlGe0cg7j02ZSHmmJNQOx5yOmSQI7z4njDvJlOPncC14T59yDZUIYYLTAYjL8nmCNk2yD55ImVGRIb/f0JGAc2vHMlDN6fkuiSLtbASz11PwWzdln9tS9BTpRx7sfv5+kMhFkzomufftAJbs5XdOxcvGRHkRkdyCn3t8jcRXaNDBpq3o8zWLNh+411I65DMDN1B0c4+7Uw4N17I+KfrVh5Fb8ix0LW9aEL/SUxeIqdEu0ZMTncxGS0QVzZmtJUTdWuT0JjtgfsD65jqpg9l8RdIKX4i0ox2+GeQuXKCQiCaXS47yDxUcICYZB7UDnfwBcXlnGWw1frvkBBqwvwUVa7G7Al1A1a35ob1qb9gpyJZGqmEyV4Qe3PmfUHkp+/bAWarLXnk8P4l2faFjCEnPDIejGMQgDdtkodq7lfqwtHwg02bXNl9X3FbXjyp51/IJshE9dhHLkniWyHwZvHZJ4jjqndf08682gxTnW0bscfv7d6lKznE7rgd/el5ygdlL0PSr0pbxJtcd7MYrACMfZZxRuY3PWZayr1n4iaqh161S8ldeEcLCiHFvDeGHslUK+Sl383AHhs46NHEVgMbhuKJ1thHckQSgpqm9vGE1gBIo0d7z40EulgD+KS4Qm/vdydCgqtzyNmFDg17L+SVCQiyckGxgl/j1YzQKz0o1EWXNssk121m7Nh/e/bYNN8kC6doJPBQBkdhU/+DVZa4+/je+qpqvqnjpheX42k49ubk638QhOkcMmx6ZNnEbL9Ra0mNJHZO27t6++LgN2XkPg6t8VO94yx1c/VFyoF9Qg21O1+BadK3RF9l9s1eUB7n93YwUDhxZpimr8j8oLCn+PvksKwzWEIiAFXUWvuROwh6yC31GghO6veGx7UqXGPM2O8+mkOxXZObbnYAAhteRhXO/k15HL3qf94pudZ6+tETmeej4FKdhZzF9iOsTu/69izQFhl57E+n5hDjRzeVANKQOdq2x7qDG7pGfpJzrDVR95Z1q7Y/d+7FJjBML+YvzGvbxZno+PbPmLGrgxSebAWgO73fUDkBHvosMDjeHjg0NKBXYfR3kImceo4iJwF0bOeFQA09j+6OKW1EId9r1EzYwPGR0DUDt+A9f4DkU/27dbKUmJovBPIAXtYHErwZ7EW0ITd8ebNXyOiArz8JJietDsR++lwetArzrDszsPsiumoSu1/FajtbBfUQqllxJBeoIqNEPHQED2ysSWzqTlHe8tfmY3Vp/PlZh8GbpA/EV5uikWzDSaZbADqXGny/xd+g+8mm4J3DxgPOEEgFqeg+q4C1U4yEy3gYkknT6AcIO8MGOa3daRowEv2zfhVlrNhv2ezLjzsPBQJSK/ph9KZbYiREZVCBQioJJlvfG/dkkycjkVqvTNtn3swhipxUqYc0vKmXFG+R9+fUOP5u9ctqmdIf7Jclew4OBO1ML+sH+l6NojgfX+T+OqQ8EyNMY2Aa7i24ppwS8ySDwyYEk9kjopVZ8L3MSw3CQnWBNMg734qUawCUoFMtGPZ0jC+o6DiV18xpkB1Ffrmjs+72AoKk6wR9zIYyVQG7wr7O4PLWeXEX2NgBDeKaWi0gHInVAb2puJKgGgaZbImuecSaKaqA0y1fXu3+4teozv9eLkBxhYUJRwIBQbA7E3lQ4ocziE3ZMxdAIGAOEJHgDs6UreB4ZMLgdKxqhYzR3GxrmZmspOw/TVBGGjClrefGZh0/gLkXSDZ0uugW1rNM3TkA9t2U0eBCNyRh+8sScUKzja4Ei2ukkfoCNEf0alJkG3udwBtjDBcTRhQJARFD+UWMut5Xqw/dAxs777/VmY00/wD9FaCq0aVc/CJI++oGYgunPDd7ACSTyuY0UST1AEBrCrFBB+T+eKrFcU/4gBXEGiAY4x8lrp7fNZ/nsGFvBvs5sxq9NvLFXGA8Tv3LHU8x8m8oGHVnT0AaFJIKM8vGf2bkdD89EmQkKG9pfnKPCTbyFszqOJlgORDLSn9bNvDYlVC3dS5HQbqdv1a1fn1WrLDEXTNuD1Nj6dJXUBLAZfqzRHaPRi04YNpw9MY+c6f3XmAvNmwrFgdnxHja47ObX4XGigltlCWUSImRboBinCeZ9k9GdQ82Pw36dO433Z3IjtjuFLTWFsUurpAvmgiemzJ3xnhra6pOEr2jy2dw6fUL4gMQtPpSwqB4Qd9TY6MOUpwBqK0MpuRMBisa3zOBphJyvnXOIG9RCYzivzOQM+PKzLmznjTyeql1Ipj/9LwEgQ==,iv:PbRgjgm5ERWlAkkWc7As8eIx8e0rjTUkQ7XlDBrEUlU=,tag:yh2/JuHu5I+dWlxVAmkkXA==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSByOGNaakZoV3lhemhMSVVF
|
||||
RklKbEsxeVFMWWdwQ3ZqR0lhRHlPM1dWT3h3Cnk2RnZUNnplVEwvNW9qMExsSUlt
|
||||
VVVXejMrcDRRTEdtT1krdThqWThLWWcKLS0tIEVZZEVBWFZyVi84S1puRHdtMWR1
|
||||
cDBHc1J5aHFpQkxSSEx2VDRjZ1RsdXcKG5MgnUBpFmlNGSkxBijXp7m492ujyqsC
|
||||
TbTfU8SWJwXhio7V+zDbXG+JW4R51wr9F9Sj47DCw7b64nbsD3N5TQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-26T02:15:51Z"
|
||||
mac: ENC[AES256_GCM,data:+2eGERwwzeK5JE9H6qhsu+tskR3Y9c7DecVGopfG8Ggq5SHQBfYyvz//IU+MGCOb6nIyP3JPf9+9IDS2DthVcw+RLtviQkN1TeWuUTDbxBxHdCoUv9zepwU7+d0qUMrdUK0N9QQSRZ4TpiwceViDeMuhPF/vTmADP9QWwid/qwk=,iv:nuJo37Dh8MAf+qI/Ty2jsW0IheNhgmZ4l0Va8pJG6+4=,tag:vCpifXvYF1/QU7XExc6raQ==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -0,0 +1,23 @@
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: zitadel-masterkey
|
||||
namespace: iam
|
||||
type: Opaque
|
||||
stringData:
|
||||
masterkey: ENC[AES256_GCM,data:Da543Jt48o6p3f6/AbZRsjChz3NexPJoi/mC/LB6jUI=,iv:irS0n76tZ6QzoQMm0dWrvbw+SPfXz4h13jQb/Bjfgwg=,tag:tfbgaAdxmPIjleB1OXLn0Q==,type:str]
|
||||
sops:
|
||||
age:
|
||||
- enc: |
|
||||
-----BEGIN AGE ENCRYPTED FILE-----
|
||||
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBOZld2VG9NaFhIVWpBcGpO
|
||||
SmVwNm9FT2wrQmFKb0RGQmY4TCtITjFEZjBjCmNuNXNJYTdsZE44cDNzR2hiNTdy
|
||||
QWlVdzBtdjd0cSsxUjQxMzNyblhWTkkKLS0tIHBLT2x1MDBGdjE1eHFOdHVGa1NO
|
||||
ZGM1K2lDTksraFdPeFdSZ1JyU1VtcjAKgTjjhr7u4YRuQVZQ7kAa1clJ0Rzy10hQ
|
||||
Umcd4/YStyXHZjsJJpFSpOFdP5cZscGo3xBgpXtxAscKTS1Nw4derg==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age1e5fq3hwxy78psus2nfvmtmua36g0u3suk78ephw6246l974d2utsvn0hla
|
||||
encrypted_regex: ^(data|stringData)$
|
||||
lastmodified: "2026-10-25T01:24:21Z"
|
||||
mac: ENC[AES256_GCM,data:QFVk7KLsMLCAMH/PHVGCRyfU3k2JiO8AE3jJXmhnlUB4C8MYCTyOWQbnTFXozCMOr0kp+hAt3dYn/e23SWhyvJ7BgVLRb5QF1py704Dr46atVoLUvwHpFI7dyGK/LDTxqiM40RXAnDtWUJlTkVCzKdSFiV8CokIoByKWwxFPayM=,iv:t7HWMcpQl/DJ8Rp51Du7b5gURZO+C+FodsZSJuy5jBc=,tag:qEXSS4S0Y5cvn7T6EjakhA==,type:str]
|
||||
version: 3.13.2
|
||||
@@ -8,6 +8,19 @@
|
||||
# depend on them existing first.
|
||||
installCRDs: true
|
||||
|
||||
# Point the ACME DNS-01 self-check at the public resolvers directly.
|
||||
#
|
||||
# Without these flags cert-manager resolves through the pod resolver
|
||||
# (CoreDNS at 10.96.0.10). The self-check then never saw the TXT record and
|
||||
# reported "Waiting for DNS-01 challenge propagation" forever. The wildcard
|
||||
# certificate renewal stalled on 2026-09-23 and the cert expired on
|
||||
# 2026-10-23, which broke every Cloudflare Tunnel origin with
|
||||
# "x509: certificate has expired".
|
||||
#
|
||||
extraArgs:
|
||||
- --dns01-recursive-nameservers=1.1.1.1:53,8.8.8.8:53
|
||||
- --dns01-recursive-nameservers-only
|
||||
|
||||
# Tolerate the control-plane taint (talos-cp-1 is the only schedulable node).
|
||||
tolerations:
|
||||
- key: node-role.kubernetes.io/control-plane
|
||||
|
||||
@@ -271,3 +271,40 @@ data:
|
||||
NDKi7/Gxha790QN3TD/U7p9GcEtfXKv8KISKfxnfD9o9Yx6pWOXcgvGefQkpXuRm
|
||||
kGzDM+aY/VKs5Et5/RyKavBDihuOcumLsmoRg/bFoB/xpeURTyiehUdu
|
||||
-----END CERTIFICATE-----
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: homelab-ca
|
||||
namespace: cloudflared
|
||||
data:
|
||||
ca.crt: |
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFBjCCAu6gAwIBAgIJAKoBPdKPDL5AMA0GCSqGSIb3DQEBCwUAMDAxEzARBgNV
|
||||
BAMMCmhvbWVsYWItY2ExGTAXBgNVBAoMEHJpb3RwaWFvLmhvbWVsYWIwHhcNMjYw
|
||||
ODEyMjA1NDQzWhcNMzYwODA5MjA1NDQzWjAwMRMwEQYDVQQDDApob21lbGFiLWNh
|
||||
MRkwFwYDVQQKDBByaW90cGlhby5ob21lbGFiMIICIjANBgkqhkiG9w0BAQEFAAOC
|
||||
Ag8AMIICCgKCAgEAzw796vZRFOBc/orUu9J6rv91A3SNLJsqz/VuetwAXCaVSlK5
|
||||
kL/RaBKKkcXToCC5avwix6W21FnwLUW4R8QlQdin8+XEHpx2LY5R4GmSvaCbU7wm
|
||||
YwYgVGQVv8KlgcMbz/pF09vXU29cHUHwlkN+BWVGAxVGWxam94j1pBzV0dOhhRw2
|
||||
q5sZYUUrPO5l775od2FMMnMzoRFugxDvJof0p3fAChNAIiFhP95lrcb2YsTwK+a2
|
||||
6mk3AZc0gMtmgVsH+zryGFcAmWLixq41ye/H23ktj2bjEHCr9mIDpuSOKLyLBzrg
|
||||
js/HqdxvwkZkkxwXft0Vi7GN0/LGHMdPrDE499pJelAHbxUpBoOk8sfXSNnfESI8
|
||||
v20LybATGrKXFlOhMKshY1a3JLAoZTaW0xkG4PM4HT7lK/ovr/NLF6kdP7HxJFFQ
|
||||
+4WjySiwbjusNobpTaQWVwCjD3Imf+2feAqJeX3z+P3EhxfrmvEojLOdUiiPcjMn
|
||||
CFl9u2EpSlax53umSkcBQJGfFKM17fZzhtakjqhCTzAfXXlxZy4hsHDtfzRcfaoB
|
||||
Zz/PiJ0TtJSC7TvxZZoPpGexQdoeT7kJEksTvGi5OSfJD7HaKe5RQSZGgpz7ATB+
|
||||
YMBKbjhMXVu+TAdcO1qZRN0UCncrliN61ki9F0tE8kNgpS9biZ6kheoJMZUCAwEA
|
||||
AaMjMCEwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcN
|
||||
AQELBQADggIBAFN1At11d/+Ue6cEjCklnRQpLuD5agXaE+pi5oHNfnggSlO8UjUj
|
||||
lDzfnmxi3IAI6HruzDARM9J95qJboUxWOAEmK9SNnZNS/1b0GyikP3YcF1gIFqtW
|
||||
6SgmVfC1cVUFKGt/+QQzZxhOLuoydv9Ozu12nxsTW/WY2t+cpeL+1wrsM22RA6W/
|
||||
xV/bzYbwzCUT9v4tMZLZHk5CkXztVFXSJyW/8w9rVZdUGZM+mwwn81PLSE0v3s/2
|
||||
wJH8eUqGl73i3Xgn7CVanN4cJYMGYVVlRD6Qr6muBN4Vh0ETY1PrjzsZ6XsV6vk9
|
||||
glMbQnErYMZHZIDtnVdaBt60MecUD9O4eLeip0SE6bVRAucKBXA9xNMilMQEL8/X
|
||||
FBMAxXl10rowBlbX2oaFSOM80eLTla4a3gSL7+0vua3C4Dtv2bLo8ZjCyqXwOJeb
|
||||
N85cqcVZStz3qlgQN1nG2qGE3BS6scOs7+mUGe9CmS8acJV++PKm8CtmLH6/ibjV
|
||||
tAc/6+Vg/keo0kty7H7L6oQd+gKpuaJ2I+wKYe4l9UwHi9zEb8YjzGB4n2AtsoWI
|
||||
NDKi7/Gxha790QN3TD/U7p9GcEtfXKv8KISKfxnfD9o9Yx6pWOXcgvGefQkpXuRm
|
||||
kGzDM+aY/VKs5Et5/RyKavBDihuOcumLsmoRg/bFoB/xpeURTyiehUdu
|
||||
-----END CERTIFICATE-----
|
||||
|
||||
@@ -49,30 +49,6 @@ spec:
|
||||
port:
|
||||
number: 3100
|
||||
|
||||
---
|
||||
# ── Authentik ─────────────────────────────────────────────────────────────────
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: authentik
|
||||
namespace: iam
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-buffer-size: "16k"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: authentik.riotpiao.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: authentik-server
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
# ── Vault ─────────────────────────────────────────────────────────────────────
|
||||
apiVersion: networking.k8s.io/v1
|
||||
@@ -299,28 +275,3 @@ spec:
|
||||
name: temporal-web
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
# NOTE: api.riotpiao.com is deliberately NOT here. Its namespace `api` is
|
||||
# created in wave 7, and this Application syncs in wave 1 — an Ingress into a
|
||||
# namespace that doesn't exist yet would fail and mark this whole app
|
||||
# SyncFailed. It lives in k8s/apps/api/ingress.yaml, synced by the api-gw
|
||||
# Application.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: homarr
|
||||
namespace: dashboard
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: homarr.riotpiao.com
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: homarr
|
||||
port:
|
||||
number: 7575
|
||||
|
||||
|
||||
@@ -1,13 +1,21 @@
|
||||
# k8s/bootstrap/ingress/riotpiao-com-cert.yaml
|
||||
# Public wildcard cert for *.riotpiao.com, signed by Let's Encrypt (DNS-01 via
|
||||
# Cloudflare, see ../cert-manager/letsencrypt-issuer.yaml). Distinct from
|
||||
# homelab-tls (self-signed homelab-ca, internal *.riotpiao.com) —
|
||||
# reference riotpiao-com-tls explicitly on ingresses that expose riotpiao.com
|
||||
# hosts to the internet; it is not nginx's default-ssl-certificate.
|
||||
# Cloudflare, see ../cert-manager/letsencrypt-issuer.yaml).
|
||||
#
|
||||
# issuerRef starts on letsencrypt-staging. Switch to letsencrypt-prod once the
|
||||
# staging cert issues cleanly, then delete this Certificate's Secret to force
|
||||
# a re-issue against prod (staging certs aren't browser-trusted).
|
||||
# nginx serves this secret as its default-ssl-certificate
|
||||
# (../ingress/nginx-values.yaml), so every Ingress that omits a tls: block uses
|
||||
# it. Do not add per-host tls: blocks for *.riotpiao.com hosts: they create a
|
||||
# second, single-host certificate and split the wildcard coverage.
|
||||
#
|
||||
# Renewal is automatic and does not drop traffic:
|
||||
# - cert-manager starts a new order 30 days before expiry (renewBefore).
|
||||
# - cert-manager writes the renewed pair into the same secret.
|
||||
# - ingress-nginx watches the secret and hot-reloads. No pod restart.
|
||||
# - The old pair stays valid until the new one is written, so no gap exists.
|
||||
#
|
||||
# The DNS-01 self-check must reach the public resolvers directly. Without the
|
||||
# --dns01-recursive-nameservers flags in ../../cert-manager/cert-manager-values.yaml
|
||||
# the self-check never sees the TXT record and renewal stalls until expiry.
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
|
||||
@@ -45,7 +45,13 @@ gitea:
|
||||
SSH_DOMAIN: git.riotpiao.com
|
||||
SSH_PORT: 2222
|
||||
SSH_LISTEN_PORT: 2222
|
||||
|
||||
|
||||
# Self-signup was open (GET /user/sign_up returned 200), so anyone reaching
|
||||
# forgejo.riotpiao.com could create an account. Accounts are provisioned by
|
||||
# the admin only; admin create-user via UI/API is unaffected by this flag.
|
||||
service:
|
||||
DISABLE_REGISTRATION: true
|
||||
|
||||
database:
|
||||
DB_TYPE: postgres
|
||||
HOST: forgejo-db-rw.cicd.svc.cluster.local:5432
|
||||
@@ -101,7 +107,9 @@ ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
# No cert-manager annotation. forgejo.riotpiao.com is covered by the
|
||||
# *.riotpiao.com wildcard, served as nginx's default-ssl-certificate.
|
||||
#
|
||||
# This chart Ingress and the hand-written one in
|
||||
# k8s/bootstrap/ingress/ingress.yaml both claim forgejo.riotpiao.com.
|
||||
# ingress-nginx breaks the tie by oldest creationTimestamp, and the chart's
|
||||
@@ -119,10 +127,9 @@ ingress:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: forgejo-tls
|
||||
hosts:
|
||||
- forgejo.riotpiao.com
|
||||
# tls: intentionally omitted. The chart would create a second, single-host
|
||||
# certificate (forgejo-tls) that expired on 2026-10-23 and competed with the
|
||||
# wildcard. The wildcard covers this host.
|
||||
|
||||
# Resources
|
||||
resources:
|
||||
|
||||
@@ -138,12 +138,15 @@ configs:
|
||||
# cicd — automation-only; apiKey (token) for the CD pipeline, no UI login.
|
||||
accounts.rock: apiKey,login
|
||||
accounts.cicd: apiKey
|
||||
# Authentik OIDC. clientSecret pulled from the argocd `oidc-secret` Secret
|
||||
# (created by authentik-provision). The groups claim drives RBAC below.
|
||||
# Zitadel OIDC. clientSecret pulled from the argocd `oidc-secret` Secret.
|
||||
# The groups claim drives RBAC below.
|
||||
oidc.config: |
|
||||
name: Authentik
|
||||
issuer: https://authentik.riotpiao.com/application/o/argocd/
|
||||
clientID: argocd
|
||||
name: Zitadel
|
||||
issuer: https://zitadel.riotpiao.com
|
||||
# MUST be quoted: an unquoted 18-digit number is parsed as a float by the
|
||||
# YAML parser and ArgoCD rejects it ("cannot unmarshal number into ...
|
||||
# clientID of type string"). Same class of bug as Grafana's client_id.
|
||||
clientID: "396757801210282367"
|
||||
clientSecret: $oidc-secret:client-secret
|
||||
requestedScopes:
|
||||
- openid
|
||||
@@ -158,7 +161,7 @@ configs:
|
||||
server.insecure: false
|
||||
|
||||
# RBAC. local `admin` + `rock` + the `cicd` pipeline account all get role:admin;
|
||||
# the Authentik `homelab-admins` group (rock is a member) maps to admin so SSO
|
||||
# the Zitadel `homelab-admins` role (rock is a member) maps to admin so SSO
|
||||
# logins are admin too.
|
||||
rbac:
|
||||
policy.default: role:readonly
|
||||
|
||||
@@ -1,70 +0,0 @@
|
||||
# ArgoCD Image Updater configuration
|
||||
# Watches Forgejo registry and updates ArgoCD Applications with new image tags
|
||||
|
||||
config:
|
||||
# Registry configuration - Forgejo allows anonymous pulls
|
||||
registries:
|
||||
- name: forgejo
|
||||
api_url: https://forgejo.riotpiao.com
|
||||
prefix: forgejo.riotpiao.com
|
||||
default: true
|
||||
insecure: false
|
||||
|
||||
# Log level
|
||||
logLevel: debug
|
||||
|
||||
# ArgoCD API server
|
||||
argocd:
|
||||
grpcWeb: true
|
||||
serverAddress: argocd-server.argocd.svc.cluster.local
|
||||
insecure: true
|
||||
plaintext: true
|
||||
|
||||
# Git write-back configuration (for multi-source Applications)
|
||||
git:
|
||||
# Commit author for image updates
|
||||
user:
|
||||
name: "ArgoCD Image Updater"
|
||||
email: "[email protected]"
|
||||
# Use SSH keys from ArgoCD's known hosts + credentials
|
||||
# Image Updater inherits ArgoCD's git credentials (mounted via ArgoCD secret)
|
||||
|
||||
# Mount ArgoCD's git credentials for write-back
|
||||
extraVolumes:
|
||||
- name: argocd-ssh-known-hosts-cm
|
||||
configMap:
|
||||
name: argocd-ssh-known-hosts-cm
|
||||
defaultMode: 0644
|
||||
- name: argocd-gpg-keys-cm
|
||||
configMap:
|
||||
name: argocd-gpg-keys-cm
|
||||
optional: true
|
||||
defaultMode: 0644
|
||||
- name: argocd-gpg-pubring
|
||||
configMap:
|
||||
name: argocd-gpg-pubring-cm
|
||||
optional: true
|
||||
defaultMode: 0644
|
||||
|
||||
extraVolumeMounts:
|
||||
- name: argocd-ssh-known-hosts-cm
|
||||
mountPath: /etc/ssh/ssh_known_hosts.d/argocd-ssh-known-hosts
|
||||
subPath: ssh_known_hosts
|
||||
- name: argocd-gpg-keys-cm
|
||||
mountPath: /etc/gpg/source
|
||||
- name: argocd-gpg-pubring
|
||||
mountPath: /etc/gpg/pubring
|
||||
|
||||
# Extra environment variables
|
||||
extraEnv:
|
||||
- name: GIT_SSH_KNOWN_HOSTS_CONFIG_MAP_ENABLED
|
||||
value: "true"
|
||||
|
||||
# Resources
|
||||
resources:
|
||||
requests:
|
||||
cpu: 50m
|
||||
memory: 64Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 128Mi
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user