diff --git a/diagram/poimen-schema-relations.architecture.json b/diagram/poimen-schema-relations.architecture.json new file mode 100644 index 0000000..d0f2fdc --- /dev/null +++ b/diagram/poimen-schema-relations.architecture.json @@ -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)"] } + ] +} diff --git a/diagram/poimen-schema-relations.dataflow.json b/diagram/poimen-schema-relations.dataflow.json new file mode 100644 index 0000000..b2601e5 --- /dev/null +++ b/diagram/poimen-schema-relations.dataflow.json @@ -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" + ] + } + ] +} \ No newline at end of file diff --git a/diagram/rbac-delegation-flow.sequence.json b/diagram/rbac-delegation-flow.sequence.json new file mode 100644 index 0000000..a2c6e92 --- /dev/null +++ b/diagram/rbac-delegation-flow.sequence.json @@ -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 " }, + { "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" + ] + } + ] +} diff --git a/diagram/rbac-fine-grained.architecture.json b/diagram/rbac-fine-grained.architecture.json new file mode 100644 index 0000000..83bb7eb --- /dev/null +++ b/diagram/rbac-fine-grained.architecture.json @@ -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"] } + ] +} diff --git a/diagram/rbac-m2m-flow.sequence.json b/diagram/rbac-m2m-flow.sequence.json new file mode 100644 index 0000000..32d8a18 --- /dev/null +++ b/diagram/rbac-m2m-flow.sequence.json @@ -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 " }, + { "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" + ] + } + ] +} diff --git a/diagram/rbac-user-flow.sequence.json b/diagram/rbac-user-flow.sequence.json new file mode 100644 index 0000000..77b013d --- /dev/null +++ b/diagram/rbac-user-flow.sequence.json @@ -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 " }, + { "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" + ] + } + ] +} diff --git a/k8s/argocd/apps/98-talos-repair.yaml b/k8s/argocd/apps/98-talos-repair.yaml new file mode 100644 index 0000000..7071af7 --- /dev/null +++ b/k8s/argocd/apps/98-talos-repair.yaml @@ -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 diff --git a/k8s/argocd/secrets/talos-repair-config.enc.yaml b/k8s/argocd/secrets/talos-repair-config.enc.yaml new file mode 100644 index 0000000..3a42fa3 --- /dev/null +++ b/k8s/argocd/secrets/talos-repair-config.enc.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Secret +metadata: + name: talos-repair-config + namespace: cicd +type: Opaque +stringData: + config: ENC[AES256_GCM,data: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,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 diff --git a/k8s/infra/oidc-bootstrap/bootstrap.yaml b/k8s/infra/oidc-bootstrap/bootstrap.yaml index 44b8f4f..5d3b41f 100644 --- a/k8s/infra/oidc-bootstrap/bootstrap.yaml +++ b/k8s/infra/oidc-bootstrap/bootstrap.yaml @@ -31,29 +31,3 @@ roleRef: apiGroup: rbac.authorization.k8s.io kind: Role name: oidc-bootstrap ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: oidc-bootstrap-restart-apiserver-v2 - namespace: kube-system -spec: - backoffLimit: 4 - ttlSecondsAfterFinished: 7200 - template: - spec: - serviceAccountName: oidc-bootstrap - restartPolicy: Never - containers: - - name: restart - image: docker.io/curlimages/curl:8.22.0 - command: ["/bin/sh", "-c"] - args: - - | - set -u - TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) - CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt - for pod in kube-apiserver-talos-cp-1 kube-apiserver-talos-cp-2 kube-apiserver-talos-cp-3; do - curl -sS --max-time 15 --cacert "$CA" -H "Authorization: Bearer $TOKEN" -X DELETE "https://kubernetes.default.svc/api/v1/namespaces/kube-system/pods/$pod?gracePeriodSeconds=0" -o /dev/null -w "$pod -> HTTP %{http_code}\n" || echo "$pod -> request failed" - done - echo "deletes requested; kubelet recreates each static pod in ~20-40s" \ No newline at end of file diff --git a/k8s/infra/oidc-bootstrap/restart-apiserver.yaml b/k8s/infra/oidc-bootstrap/restart-apiserver.yaml deleted file mode 100644 index 021ab0b..0000000 --- a/k8s/infra/oidc-bootstrap/restart-apiserver.yaml +++ /dev/null @@ -1,141 +0,0 @@ -# One-shot: restart the control-plane API servers so their OIDC authenticator initialises. -# -# Two earlier mechanisms failed and are documented so nobody repeats them: -# 1. deleting the mirror pod does NOT restart a static pod (deletes returned 200, no restart); -# 2. matching /proc/*/cmdline for 'kube-apiserver' also matched this script's own cmdline, -# so every pod SIGTERMed itself (exit 143) before reaching the real process. -# This version matches /proc//comm (the process name) and skips \$\$, so self-kill is -# impossible. hostPID + privileged are permitted in the 'cicd' namespace. Delays stagger the -# three control planes by 2 minutes each, so only one API server is down at a time. ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: oidc-restart-apiserver-cp1-v2 - namespace: cicd -spec: - backoffLimit: 0 - ttlSecondsAfterFinished: 3600 - template: - spec: - hostPID: true - restartPolicy: Never - nodeSelector: - kubernetes.io/hostname: talos-cp-1 - tolerations: - - key: node-role.kubernetes.io/control-plane - operator: Exists - effect: NoSchedule - - key: node-role.kubernetes.io/master - operator: Exists - effect: NoSchedule - containers: - - name: kill - image: docker.io/library/busybox:1.36 - securityContext: - privileged: true - command: ["/bin/sh", "-c"] - args: - - | - echo "sleeping 10s before restarting the API server on talos-cp-1" - sleep 10 - self=$$ - killed=0 - for d in /proc/[0-9]*; do - pid=${d#/proc/} - [ "$pid" = "$self" ] && continue - comm=$(cat "$d/comm" 2>/dev/null) - [ "$comm" = "kube-apiserver" ] || continue - echo "SIGTERM -> pid $pid" - kill -TERM "$pid" 2>/dev/null && killed=$((killed+1)) - done - echo "signalled $killed API server process(es); kubelet recreates the static pod" - ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: oidc-restart-apiserver-cp2-v2 - namespace: cicd -spec: - backoffLimit: 0 - ttlSecondsAfterFinished: 3600 - template: - spec: - hostPID: true - restartPolicy: Never - nodeSelector: - kubernetes.io/hostname: talos-cp-2 - tolerations: - - key: node-role.kubernetes.io/control-plane - operator: Exists - effect: NoSchedule - - key: node-role.kubernetes.io/master - operator: Exists - effect: NoSchedule - containers: - - name: kill - image: docker.io/library/busybox:1.36 - securityContext: - privileged: true - command: ["/bin/sh", "-c"] - args: - - | - echo "sleeping 120s before restarting the API server on talos-cp-2" - sleep 120 - self=$$ - killed=0 - for d in /proc/[0-9]*; do - pid=${d#/proc/} - [ "$pid" = "$self" ] && continue - comm=$(cat "$d/comm" 2>/dev/null) - [ "$comm" = "kube-apiserver" ] || continue - echo "SIGTERM -> pid $pid" - kill -TERM "$pid" 2>/dev/null && killed=$((killed+1)) - done - echo "signalled $killed API server process(es); kubelet recreates the static pod" - ---- -apiVersion: batch/v1 -kind: Job -metadata: - name: oidc-restart-apiserver-cp3-v2 - namespace: cicd -spec: - backoffLimit: 0 - ttlSecondsAfterFinished: 3600 - template: - spec: - hostPID: true - restartPolicy: Never - nodeSelector: - kubernetes.io/hostname: talos-cp-3 - tolerations: - - key: node-role.kubernetes.io/control-plane - operator: Exists - effect: NoSchedule - - key: node-role.kubernetes.io/master - operator: Exists - effect: NoSchedule - containers: - - name: kill - image: docker.io/library/busybox:1.36 - securityContext: - privileged: true - command: ["/bin/sh", "-c"] - args: - - | - echo "sleeping 240s before restarting the API server on talos-cp-3" - sleep 240 - self=$$ - killed=0 - for d in /proc/[0-9]*; do - pid=${d#/proc/} - [ "$pid" = "$self" ] && continue - comm=$(cat "$d/comm" 2>/dev/null) - [ "$comm" = "kube-apiserver" ] || continue - echo "SIGTERM -> pid $pid" - kill -TERM "$pid" 2>/dev/null && killed=$((killed+1)) - done - echo "signalled $killed API server process(es); kubelet recreates the static pod" - diff --git a/k8s/infra/talos-repair/kustomization.yaml b/k8s/infra/talos-repair/kustomization.yaml new file mode 100644 index 0000000..57b4a1b --- /dev/null +++ b/k8s/infra/talos-repair/kustomization.yaml @@ -0,0 +1,12 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +# Stable secret name: the job mounts talos-repair-config by name, so the generated +# Secret must not get a hash suffix (same convention as k8s/argocd/secrets). +generatorOptions: + disableNameSuffixHash: true +# ksops decrypts talosconfig-secret.enc.yaml into the Secret the job mounts. +# Kept local to this app on purpose: no edits to the shared secrets generator. +generators: + - secret-generator.yaml +resources: + - restart-kubelet.yaml diff --git a/k8s/infra/talos-repair/restart-kubelet.yaml b/k8s/infra/talos-repair/restart-kubelet.yaml new file mode 100644 index 0000000..02b1224 --- /dev/null +++ b/k8s/infra/talos-repair/restart-kubelet.yaml @@ -0,0 +1,57 @@ +# One-shot repair: the kubelets on cp-1, cp-2 and worker-1 stopped posting node status at +# 04:11-04:14 UTC and never reconnected, so those nodes are NotReady/unreachable and every +# pod on them (Cilium included) shows NotReady. This job runs on the one healthy node (cp-3), +# talks to the Talos API on the LAN, and uses Talos' endpoint->node proxying to reach the +# other three: it reports the kubelet service state, then restarts it. +apiVersion: batch/v1 +kind: Job +metadata: + name: talos-restart-stalled-kubelets + namespace: cicd +spec: + backoffLimit: 1 + ttlSecondsAfterFinished: 86400 + template: + spec: + restartPolicy: Never + nodeSelector: + kubernetes.io/hostname: talos-cp-3 + tolerations: + - key: node-role.kubernetes.io/control-plane + operator: Exists + effect: NoSchedule + containers: + - name: talosctl + image: docker.io/curlimages/curl:8.22.0 + command: ["/bin/sh", "-c"] + args: + - | + set -u + echo "fetching talosctl v1.13.3 (matches the node version)" + curl -sSL -o /tmp/talosctl \ + https://github.com/siderolabs/talos/releases/download/v1.13.3/talosctl-linux-amd64 \ + || { echo "DOWNLOAD FAILED"; exit 1; } + chmod +x /tmp/talosctl + T="/tmp/talosctl --talosconfig /talos/config -e 192.168.1.162" + NODES="192.168.1.166 192.168.1.214 192.168.1.223" + echo "=== BEFORE: kubelet service state ===" + for n in $NODES; do echo "--- $n"; $T -n $n service kubelet 2>&1 || true; done + echo "=== kernel log tail (last 15 lines per node) ===" + for n in $NODES; do echo "--- $n"; $T -n $n dmesg --tail 15 2>&1 | tail -15 || true; done + echo "=== ACTION: restart kubelet ===" + for n in $NODES; do echo "--- restarting $n"; $T -n $n restart kubelet 2>&1 || true; sleep 20; done + sleep 60 + echo "=== AFTER ===" + for n in $NODES; do echo "--- $n"; $T -n $n service kubelet 2>&1 || true; done + volumeMounts: + - name: talos + mountPath: /talos + readOnly: true + volumes: + - name: talos + secret: + secretName: talos-repair-config + defaultMode: 0444 + items: + - key: config + path: config diff --git a/k8s/infra/talos-repair/secret-generator.yaml b/k8s/infra/talos-repair/secret-generator.yaml new file mode 100644 index 0000000..6b0fbf4 --- /dev/null +++ b/k8s/infra/talos-repair/secret-generator.yaml @@ -0,0 +1,10 @@ +apiVersion: viaduct.ai/v1 +kind: ksops +metadata: + name: talos-repair-secret-generator + annotations: + config.kubernetes.io/function: | + exec: + path: ksops +files: + - talosconfig-secret.enc.yaml diff --git a/k8s/infra/talos-repair/talosconfig-secret.enc.yaml b/k8s/infra/talos-repair/talosconfig-secret.enc.yaml new file mode 100644 index 0000000..3a42fa3 --- /dev/null +++ b/k8s/infra/talos-repair/talosconfig-secret.enc.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Secret +metadata: + name: talos-repair-config + namespace: cicd +type: Opaque +stringData: + config: ENC[AES256_GCM,data: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,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