diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/consumer-delivery.yaml b/docs/proposals/2026-10-11-forgejo-consumer-eso/consumer-delivery.yaml new file mode 100644 index 0000000..01e2a98 --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/consumer-delivery.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: forgejo-keycape-forgejo-eso + namespace: forgejo + labels: + app.kubernetes.io/name: forgejo + app.kubernetes.io/part-of: railiance-apps +automountServiceAccountToken: false +--- +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: openbao-keycape-forgejo-consumer + namespace: forgejo +spec: + provider: + vault: + server: http://openbao.openbao.svc:8200 + path: platform + version: v2 + auth: + kubernetes: + mountPath: kubernetes + role: external-secrets-keycape-forgejo-forgejo + serviceAccountRef: + name: forgejo-keycape-forgejo-eso + audiences: + - openbao +--- +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: forgejo-netkingdom-oidc-client + namespace: forgejo + labels: + app.kubernetes.io/name: forgejo + app.kubernetes.io/part-of: railiance-apps +spec: + refreshInterval: 15m + secretStoreRef: + name: openbao-keycape-forgejo-consumer + kind: SecretStore + target: + name: forgejo-netkingdom-oidc-client + creationPolicy: Owner + deletionPolicy: Retain + template: + metadata: + labels: + app.kubernetes.io/name: forgejo + app.kubernetes.io/part-of: railiance-apps + data: + - secretKey: CLIENT_SECRET + remoteRef: + key: workloads/keycape/forgejo + property: CLIENT_SECRET diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/native-metadata.json b/docs/proposals/2026-10-11-forgejo-consumer-eso/native-metadata.json new file mode 100644 index 0000000..298d885 --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/native-metadata.json @@ -0,0 +1,666 @@ +{ + "eso_rbac_bindings": [ + { + "kind": "ClusterRoleBinding", + "name": "external-secrets-cert-controller", + "namespace": null, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "external-secrets-cert-controller" + }, + "rules": [ + { + "apiGroups": [ + "apiextensions.k8s.io" + ], + "resources": [ + "customresourcedefinitions" + ], + "verbs": [ + "get", + "list", + "watch", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "admissionregistration.k8s.io" + ], + "resources": [ + "validatingwebhookconfigurations" + ], + "verbs": [ + "list", + "watch", + "get" + ] + }, + { + "apiGroups": [ + "admissionregistration.k8s.io" + ], + "resourceNames": [ + "secretstore-validate", + "externalsecret-validate" + ], + "resources": [ + "validatingwebhookconfigurations" + ], + "verbs": [ + "update", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "endpoints" + ], + "verbs": [ + "list", + "get", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "events" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "secrets" + ], + "verbs": [ + "get", + "list", + "watch", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "create", + "update", + "patch" + ] + } + ], + "subjects": [ + { + "kind": "ServiceAccount", + "name": "external-secrets-cert-controller", + "namespace": "external-secrets" + } + ] + }, + { + "kind": "ClusterRoleBinding", + "name": "external-secrets-controller", + "namespace": null, + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "ClusterRole", + "name": "external-secrets-controller" + }, + "rules": [ + { + "apiGroups": [ + "external-secrets.io" + ], + "resources": [ + "secretstores", + "clustersecretstores", + "externalsecrets", + "clusterexternalsecrets", + "pushsecrets", + "clusterpushsecrets" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "external-secrets.io" + ], + "resources": [ + "externalsecrets", + "externalsecrets/status", + "externalsecrets/finalizers", + "secretstores", + "secretstores/status", + "secretstores/finalizers", + "clustersecretstores", + "clustersecretstores/status", + "clustersecretstores/finalizers", + "clusterexternalsecrets", + "clusterexternalsecrets/status", + "clusterexternalsecrets/finalizers", + "pushsecrets", + "pushsecrets/status", + "pushsecrets/finalizers", + "clusterpushsecrets", + "clusterpushsecrets/status", + "clusterpushsecrets/finalizers" + ], + "verbs": [ + "get", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "generators.external-secrets.io" + ], + "resources": [ + "generatorstates" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "patch", + "delete", + "deletecollection" + ] + }, + { + "apiGroups": [ + "generators.external-secrets.io" + ], + "resources": [ + "acraccesstokens", + "clustergenerators", + "ecrauthorizationtokens", + "fakes", + "gcraccesstokens", + "githubaccesstokens", + "quayaccesstokens", + "passwords", + "stssessiontokens", + "uuids", + "vaultdynamicsecrets", + "webhooks", + "grafanas" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "serviceaccounts", + "namespaces" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "configmaps" + ], + "verbs": [ + "get", + "list", + "watch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "secrets" + ], + "verbs": [ + "get", + "list", + "watch", + "create", + "update", + "delete", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "serviceaccounts/token" + ], + "verbs": [ + "create" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "events" + ], + "verbs": [ + "create", + "patch" + ] + }, + { + "apiGroups": [ + "external-secrets.io" + ], + "resources": [ + "externalsecrets" + ], + "verbs": [ + "create", + "update", + "delete" + ] + }, + { + "apiGroups": [ + "external-secrets.io" + ], + "resources": [ + "pushsecrets" + ], + "verbs": [ + "create", + "update", + "delete" + ] + } + ], + "subjects": [ + { + "kind": "ServiceAccount", + "name": "external-secrets", + "namespace": "external-secrets" + } + ] + }, + { + "kind": "RoleBinding", + "name": "external-secrets-leaderelection", + "namespace": "external-secrets", + "roleRef": { + "apiGroup": "rbac.authorization.k8s.io", + "kind": "Role", + "name": "external-secrets-leaderelection" + }, + "rules": [ + { + "apiGroups": [ + "" + ], + "resourceNames": [ + "external-secrets-controller" + ], + "resources": [ + "configmaps" + ], + "verbs": [ + "get", + "update", + "patch" + ] + }, + { + "apiGroups": [ + "" + ], + "resources": [ + "configmaps" + ], + "verbs": [ + "create" + ] + }, + { + "apiGroups": [ + "coordination.k8s.io" + ], + "resources": [ + "leases" + ], + "verbs": [ + "get", + "create", + "update", + "patch" + ] + } + ], + "subjects": [ + { + "kind": "ServiceAccount", + "name": "external-secrets", + "namespace": "external-secrets" + } + ] + } + ], + "external-secrets_deployments": [ + { + "images": [ + "oci.external-secrets.io/external-secrets/external-secrets:v0.16.1" + ], + "name": "external-secrets", + "serviceAccount": "external-secrets", + "uid": "e6017556-523a-4b26-b9d1-4c0c90cef483" + }, + { + "images": [ + "oci.external-secrets.io/external-secrets/external-secrets:v0.16.1" + ], + "name": "external-secrets-cert-controller", + "serviceAccount": "external-secrets-cert-controller", + "uid": "5134a56f-aa05-4764-9b27-c56e92203c7d" + }, + { + "images": [ + "oci.external-secrets.io/external-secrets/external-secrets:v0.16.1" + ], + "name": "external-secrets-webhook", + "serviceAccount": "external-secrets-webhook", + "uid": "6e6e4a6e-4c2c-4659-af56-d4281f2fe229" + } + ], + "external-secrets_externalsecrets": [], + "external-secrets_secretstores": [], + "external-secrets_serviceaccounts": [ + { + "automountServiceAccountToken": null, + "name": "default", + "uid": "cc29abe8-5ace-4cc9-b479-cf8db84df42d" + }, + { + "automountServiceAccountToken": false, + "name": "eso-token-renewer", + "uid": "4d44afd7-59e9-44de-9e99-8cd620be41de" + }, + { + "automountServiceAccountToken": null, + "name": "external-secrets", + "uid": "c918dc22-b1ee-4e1f-a8f8-1d15ac0d5a10" + }, + { + "automountServiceAccountToken": null, + "name": "external-secrets-cert-controller", + "uid": "3c78bb78-a3c5-4953-93d4-cf6621e1200a" + }, + { + "automountServiceAccountToken": null, + "name": "external-secrets-webhook", + "uid": "c75dc3ba-42db-4a59-8952-6c94d85d054c" + } + ], + "forgejo_deployments": [ + { + "images": [ + "code.forgejo.org/forgejo/forgejo:11.0.3-rootless" + ], + "name": "forgejo-gitea", + "serviceAccount": "default", + "uid": "e8eff0aa-64bb-4b44-9a80-a66612986355" + }, + { + "images": [ + "docker:27-dind", + "code.forgejo.org/forgejo/runner:6.3.1" + ], + "name": "forgejo-runner", + "serviceAccount": "default", + "uid": "89c8a056-bcae-4940-acdd-2eb2768e108a" + } + ], + "forgejo_externalsecrets": [ + { + "conditions": [ + { + "type": "Ready", + "status": "True", + "reason": "SecretSynced" + } + ], + "data": [ + { + "key": "workloads/forgejo/forgejo-mailer", + "property": "PASSWD", + "secretKey": "MAILER_PASSWD" + } + ], + "name": "forgejo-mailer", + "store": { + "kind": "ClusterSecretStore", + "name": "openbao-forgejo" + }, + "target": "forgejo-mailer", + "uid": "cbbea71e-70a8-4010-aca9-9136920f2f58" + } + ], + "forgejo_secretstores": [], + "forgejo_serviceaccounts": [ + { + "automountServiceAccountToken": null, + "name": "default", + "uid": "2ae15248-4700-46a6-84c6-e3bb3cd33323" + }, + { + "automountServiceAccountToken": false, + "name": "forgejo-mailer-eso", + "uid": "e2214835-32dd-4333-a9c2-6d248ab8bafe" + } + ], + "native_mutation": false, + "observed_at": "2026-10-11T01:20:37.519031+00:00", + "secret_value_reads": false, + "secretstore_crd": { + "uid": "1eefa5c9-6c2a-4a3b-84c6-3941488201c2", + "versions": [ + { + "kubernetes_auth_schema": { + "description": "Kubernetes authenticates with Vault by passing the ServiceAccount\ntoken stored in the named Secret resource to the Vault server.", + "properties": { + "mountPath": { + "default": "kubernetes", + "description": "Path where the Kubernetes authentication backend is mounted in Vault, e.g:\n\"kubernetes\"", + "type": "string" + }, + "role": { + "description": "A required field containing the Vault Role to assume. A Role binds a\nKubernetes ServiceAccount with a set of Vault policies.", + "type": "string" + }, + "secretRef": { + "description": "Optional secret field containing a Kubernetes ServiceAccount JWT used\nfor authenticating with Vault. If a name is specified without a key,\n`token` is the default. If one is not specified, the one bound to\nthe controller will be used.", + "properties": { + "key": { + "description": "A key in the referenced Secret.\nSome instances of this field may be defaulted, in others it may be required.", + "maxLength": 253, + "minLength": 1, + "pattern": "^[-._a-zA-Z0-9]+$", + "type": "string" + }, + "name": { + "description": "The name of the Secret resource being referred to.", + "maxLength": 253, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$", + "type": "string" + }, + "namespace": { + "description": "The namespace of the Secret resource being referred to.\nIgnored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "type": "string" + } + }, + "type": "object" + }, + "serviceAccountRef": { + "description": "Optional service account field containing the name of a kubernetes ServiceAccount.\nIf the service account is specified, the service account secret token JWT will be used\nfor authenticating with Vault. If the service account selector is not supplied,\nthe secretRef will be used instead.", + "properties": { + "audiences": { + "description": "Audience specifies the `aud` claim for the service account token\nIf the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity\nthen this audiences will be appended to the list", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The name of the ServiceAccount resource being referred to.", + "maxLength": 253, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$", + "type": "string" + }, + "namespace": { + "description": "Namespace of the resource being referred to.\nIgnored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "mountPath", + "role" + ], + "type": "object" + }, + "name": "v1", + "served": true, + "storage": true + }, + { + "kubernetes_auth_schema": { + "description": "Kubernetes authenticates with Vault by passing the ServiceAccount\ntoken stored in the named Secret resource to the Vault server.", + "properties": { + "mountPath": { + "default": "kubernetes", + "description": "Path where the Kubernetes authentication backend is mounted in Vault, e.g:\n\"kubernetes\"", + "type": "string" + }, + "role": { + "description": "A required field containing the Vault Role to assume. A Role binds a\nKubernetes ServiceAccount with a set of Vault policies.", + "type": "string" + }, + "secretRef": { + "description": "Optional secret field containing a Kubernetes ServiceAccount JWT used\nfor authenticating with Vault. If a name is specified without a key,\n`token` is the default. If one is not specified, the one bound to\nthe controller will be used.", + "properties": { + "key": { + "description": "A key in the referenced Secret.\nSome instances of this field may be defaulted, in others it may be required.", + "maxLength": 253, + "minLength": 1, + "pattern": "^[-._a-zA-Z0-9]+$", + "type": "string" + }, + "name": { + "description": "The name of the Secret resource being referred to.", + "maxLength": 253, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$", + "type": "string" + }, + "namespace": { + "description": "The namespace of the Secret resource being referred to.\nIgnored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "type": "string" + } + }, + "type": "object" + }, + "serviceAccountRef": { + "description": "Optional service account field containing the name of a kubernetes ServiceAccount.\nIf the service account is specified, the service account secret token JWT will be used\nfor authenticating with Vault. If the service account selector is not supplied,\nthe secretRef will be used instead.", + "properties": { + "audiences": { + "description": "Audience specifies the `aud` claim for the service account token\nIf the service account uses a well-known annotation for e.g. IRSA or GCP Workload Identity\nthen this audiences will be appended to the list", + "items": { + "type": "string" + }, + "type": "array" + }, + "name": { + "description": "The name of the ServiceAccount resource being referred to.", + "maxLength": 253, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$", + "type": "string" + }, + "namespace": { + "description": "Namespace of the resource being referred to.\nIgnored if referent is not cluster-scoped, otherwise defaults to the namespace of the referent.", + "maxLength": 63, + "minLength": 1, + "pattern": "^[a-z0-9]([-a-z0-9]*[a-z0-9])?$", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "mountPath", + "role" + ], + "type": "object" + }, + "name": "v1beta1", + "served": true, + "storage": false + } + ] + } +} diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/observe-native-metadata.py b/docs/proposals/2026-10-11-forgejo-consumer-eso/observe-native-metadata.py new file mode 100644 index 0000000..eb1cd78 --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/observe-native-metadata.py @@ -0,0 +1,31 @@ +import json,subprocess,sys,hashlib,datetime +out={'observed_at':datetime.datetime.now(datetime.timezone.utc).isoformat(),'native_mutation':False,'secret_value_reads':False} +def get(args): + r=subprocess.run(['kubectl',*args,'-o','json'],capture_output=True,text=True) + if r.returncode:return None + try:return json.loads(r.stdout) + except Exception:return None +for ns in ['external-secrets','forgejo']: + deps=get(['get','deployments','-n',ns]);out[ns+'_deployments']=None if deps is None else [{'name':d['metadata']['name'],'uid':d['metadata']['uid'],'serviceAccount':d['spec']['template']['spec'].get('serviceAccountName','default'),'images':[c['image'] for c in d['spec']['template']['spec']['containers']]} for d in deps['items']] + sas=get(['get','serviceaccounts','-n',ns]);out[ns+'_serviceaccounts']=None if sas is None else [{'name':s['metadata']['name'],'uid':s['metadata']['uid'],'automountServiceAccountToken':s.get('automountServiceAccountToken')} for s in sas['items']] + stores=get(['get','secretstores','-n',ns]);out[ns+'_secretstores']=None if stores is None else [{'name':s['metadata']['name'],'uid':s['metadata']['uid'],'vault':{k:v for k,v in s['spec'].get('provider',{}).get('vault',{}).items() if k in ['server','path','version','auth']},'conditions':[{k:c[k] for k in ['type','status','reason'] if k in c} for c in s.get('status',{}).get('conditions',[])]} for s in stores['items']] + es=get(['get','externalsecrets','-n',ns]);out[ns+'_externalsecrets']=None if es is None else [{'name':s['metadata']['name'],'uid':s['metadata']['uid'],'store':s['spec']['secretStoreRef'],'target':s['spec'].get('target',{}).get('name'),'data':[{'secretKey':x.get('secretKey'),'key':x.get('remoteRef',{}).get('key'),'property':x.get('remoteRef',{}).get('property')} for x in s['spec'].get('data',[])],'conditions':[{k:c[k] for k in ['type','status','reason'] if k in c} for c in s.get('status',{}).get('conditions',[])]} for s in es['items']] +crd=get(['get','crd','secretstores.external-secrets.io']) +if crd: + versions=[] + for v in crd['spec']['versions']: + a=v['schema']['openAPIV3Schema']['properties']['spec']['properties']['provider']['properties']['vault']['properties']['auth']['properties']['kubernetes'] + versions.append({'name':v['name'],'served':v['served'],'storage':v['storage'],'kubernetes_auth_schema':a}) + out['secretstore_crd']={'uid':crd['metadata']['uid'],'versions':versions} +bindings=[] +for resource in ['clusterrolebindings','rolebindings']: + obj=get(['get',resource,'-A']) + if obj is None:continue + for b in obj['items']: + if any(s.get('kind')=='ServiceAccount' and s.get('namespace')=='external-secrets' for s in b.get('subjects',[])): + ref=b['roleRef'];args=['get',ref['kind'].lower(),ref['name']] + if ref['kind']=='Role':args+=['-n',b['metadata']['namespace']] + role=get(args) + bindings.append({'kind':b['kind'],'name':b['metadata']['name'],'namespace':b['metadata'].get('namespace'),'subjects':b.get('subjects',[]),'roleRef':ref,'rules':None if role is None else role.get('rules',[])}) +out['eso_rbac_bindings']=bindings +print(json.dumps(out,sort_keys=True)) diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/owner-design.md b/docs/proposals/2026-10-11-forgejo-consumer-eso/owner-design.md new file mode 100644 index 0000000..a267302 --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/owner-design.md @@ -0,0 +1,39 @@ +# Forgejo consumer ESO reader — concrete source proposal + +RAPPS-WP-0016 already chooses ESO delivery for the OIDC client secret. This proposal fills its missing reader identity without asking the founder to make that same source choice again. It is not a live apply, a new credential grant, or first-case success. Existing ORGREF-WP-0002 remains the project driver; CCR42, MASON-WP-0010-T03 and NK-WP-0048-T06 remain the owner work, rather than another workplan. + +Actual read-only railiance01 metadata shows ESO v0.16.1 running as external-secrets/external-secrets, served v1/v1beta1 SecretStore CRDs supporting serviceAccountRef.audiences, and controller RBAC permitting serviceaccounts/token create. Forgejo and its runner currently share forgejo/default; they must not become the OpenBao reader identity. No consumer namespace SecretStore or dedicated OIDC-delivery SA exists in the observed metadata. Existing forgejo-mailer-eso is a separate SMTP lane and remains unchanged. No OpenBao login, capability check, auth operation, Secret value, Secret object retrieval, or native materialization was performed. + +## Exact supported reader + +`consumer-delivery.yaml` prepares exactly three namespaced objects: ServiceAccount forgejo/forgejo-keycape-forgejo-eso with automount=false and no legacy token refs; SecretStore forgejo/openbao-keycape-forgejo-consumer; ExternalSecret forgejo/forgejo-netkingdom-oidc-client. The store requests a token for that dedicated serviceAccountRef, audiences=[openbao], and authenticates to OpenBao Kubernetes role external-secrets-keycape-forgejo-forgejo. The role binds exactly that SA and namespace, only workload-kv-read-keycape-forgejo-forgejo, no default policy, and a 900-second maximum OpenBao token lifetime. ESO's installed source requests a 600-second Kubernetes token and can fall back to legacy SA tokens; owner admission must ensure this new SA has no legacy tokens, never create one to bypass a failed TokenRequest. + +The namespace SecretStore cannot select another namespace's SA; serviceAccountRef.namespace is omitted. No extra RBAC, projected workload token, Deployment SA replacement or controller identity change is proposed. Namespace/default is not an OpenBao-bound reader. ESO remains the shared trusted controller: it already has clusterwide TokenRequest and Secret permissions and can request this SA's token. This separates reader binding/revocation and avoids default identity reuse; it does not isolate this secret from controller compromise or grant new cryptographic isolation. + +The exact KV data ACL is platform/data/workloads/keycape/forgejo, read only, plus existing self-token lookup/revoke stanzas. No metadata/list/parent/sibling access is added. Delivery maps only remote workloads/keycape/forgejo field CLIENT_SECRET to target key CLIENT_SECRET. Target creationPolicy=Owner and deletionPolicy=Retain preserve fail-safe remote-deletion behavior. Refresh is 15m; the value is never present in this source. Existing internal OpenBao HTTP endpoint and controller custody behavior are retained, not upgraded into a TLS/isolation claim. + +## Type and structural boundary + +NetKingdom's proposed successor type 1.2 adds explicit case parameter consumer_namespace, one sixth structural object consumer-eso-role, and separate owner deliver-consumer-secret follow-through/readiness/rollback. All original five rendered structural bodies compare equal to current1.1. Existing acknowledged type 1.1 and accepted case 1.1 remain byte-preserved; the proposed 1.2 case has status proposed and no copied acceptance. Full proposed render is c0afd5576cdae0cbc807eccb2b9fa87ac668f15e32ec7cf79d295bdb23c75b35. Existing acceptance does not silently extend to the new role/runtime delivery. Owner review and exact type/case acknowledgment precede any build, metadata apply or materialized-status receipt. + +The current Mason builder gives every role-backed reader the ESO draft suffix. With two reader roles it silently overwrites one CCR. The new regression proves that defect against current source; the minimal staged fix retains ESO for the KeyCape reader and Forgejo for consumer-reader-policy, plus refuses duplicate output identities.42 actual focused builder/plan/structure tests pass with corrected source, including both independent CCRs, platform validation and a complete six-object structural request. Existing1.1 tests remain passing. No fake proposed 1.2 acceptance is used: Mason correctly refuses to build a merely proposed successor. Test fixtures alone temporarily acknowledge synthetic roles to exercise builder semantics. + +The proposed CCR42 body uses proposed status, bound_claims_confirmed=false, explicit pending1.2 acknowledgment and old CCR's exact historical hash/provenance. Owner validation passes with the expected apply-blocking warning. It is stored separately from the actual in-flight CCR42; no historical review or applied CCR41 is rewritten. Folding this reviewed body into CCR42 remains an owner reconciliation with retained original revision, not a fresh parallel request. + +## Admission, native proof and recovery + +Before an exact scoped native proposal, refresh ESO image/CRD/RBAC, Forgejo/default and runner identity, the three proposed object identities and target metadata through Kubernetes PartialObjectMetadata only. Classify absent, exact preexisting-matched or conflicting; refuse a legacy-token SA, unknown ownership, changed role/policy, audience mismatch or shared target. Never read Secret data. Preserve current UIDs/resourceVersions, authenticated owner change receipts and the exact type/case/render/source pins. No placeholder SA claim substitutes for actual owner admission. + +Platform materialization applies only the reviewed exact-path policy and dedicated Kubernetes-auth role after its own approved CCR path. Fresh owner metadata/capability receipts must prove exact binding and distinguish case-created from preexisting; no minted value or successful delivery is required to prove structure. Mason's existing typed structural contract then covers all six objects, including source layout and catalog pointer; source-only bodies are not authenticated native evidence. Warden native dispatch/descriptors/session receipt and per-owner provenance still need their existing accepted integration review. + +Consumer follow-through applies the three admitted objects, then checks SecretStore/ExternalSecret Ready status and exact target UID/ownerReference through metadata only. Shared/matched objects never acquire delete authority. If ESO reports failure, stop; no manual Secret seeding or controller/static-token fallback is admitted. Minting, KeyCape client/env/groups, and Forgejo Helm OIDC/group enforcement stay distinct existing first-case steps. Forgejo gitea.oauth secret-consumption settings were not changed or Helm-render-verified here; they must be prepared/reviewed before T02 handover, with current local login intact and T01 break-glass acceptance still a prerequisite. Delivery Ready alone does not complete browser/MFA/group checks or forge closure. + +Created-only rollback first reverts the consumer secret reference/auth source through its owner Helm revision. Remove only the admitted trial-created ExternalSecret and independently proven case-created target UID/ownerReference; preserve matched/shared targets. Then remove only created namespace SecretStore and SA. Remove only the created OpenBao role after revoking its issued tokens or proving expiry, then created policy. Role/SA/store deletion does not revoke already issued OpenBao tokens; deletion alone is not deactivation proof. Retain native/audit/source receipts and stop if references, UIDs or ownership changed. Do not infer delete authority from Mason's legacy planned rollback inventory. + +RAPPS-WP-0016-T02, current CCR42, MASON-WP-0010-T03 and first-case acceptance remain unfinished. ORGREF-WP-0002 holds this owner return. Human-only break-glass/account-link/MFA/first-case acknowledgment gates remain separate from source preparation; no new human login is requested by this packet. + +## Supplementary generated-CCR refinement after Platform conditional review + +Exact prior 271 packet is preserved at /tmp/forgejo-consumer-eso-refinement-original-271, alongside original c48. Platform found two additional generator gaps: hardcoded keycape target for the consumer, and delete-only consumer-role lifecycle. Both meaningful counterexamples fail against 271 source before the fix. Current staged generator derives target.workload from consuming_service only for consumer-reader-policy, preserving all KeyCape ESO output fields. A role-backed consumer CCR requires reverting consumer reference, authenticated case-created ownership, preservation of preexisting/shared objects, issued-token owner revocation or independently verified expiry before deactivation, and no new login/remint during recovery. Actual owner native authority remains required; generated wording is not an implemented revocation adapter. + +Forty-four focused builder/plan/structure tests now pass. Earlier 42 and original counterexample/failure history remain retained. The old committed 1.1 bundle still mislabels the consumer workload keycape and therefore correctly fails fresh exact target matching after this correction. That historical bundle is preserved; the checker is not weakened or grandfathered. Local fixtures alone use corrected/rehashed consumer metadata, with a separate regression proving old bundle refusal. Current 1.1 unbound consumer generation changes only its previously wrong target.workload; KeyCape reader output remains unchanged. The proposed 1.2 render/type/case/source remains pending exact acknowledgment, with identical six-object structural body and no native completion claim. Fresh source build/bundle owner reconciliation must occur after exact acceptance before any native apply/materialization; old CCR or bundle approval is not silently reassigned. New source/test hashes are in validation.json and generated projection receipt. diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/owner-source-pins.json b/docs/proposals/2026-10-11-forgejo-consumer-eso/owner-source-pins.json new file mode 100644 index 0000000..d64136b --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/owner-source-pins.json @@ -0,0 +1,37 @@ +[ + { + "repo": "net-kingdom", + "path": "security-case-types/oidc-browser-relying-party/type.yaml", + "sha256": "f705c64e4875e9b8e18a912fb51c135e77898926d6a451f5cb74ab3c0cbe4d1c" + }, + { + "repo": "net-kingdom", + "path": "security-cases/forgejo-netkingdom-oidc.yaml", + "sha256": "bcc2c6d5bae0d28242b3cdb71e74d8ca297c2afe04766b55d53597f86893351d" + }, + { + "repo": "ops-mason", + "path": "src/ops_mason/case_build.py", + "sha256": "249cc8574939d2c3193f968309781c472c96c199e04206c336463b4d65ea6659" + }, + { + "repo": "ops-mason", + "path": "src/ops_mason/case_structure.py", + "sha256": "71032a5ea613f5226f552f796656599c1a7447a38428f890595d8f252831ce80" + }, + { + "repo": "railiance-platform", + "path": "credential-change-requests/CCR-2026-0042-forgejo-netkingdom-oidc-consumer-reader.yaml", + "sha256": "a36ab567839c12660c8ca468c8e4c6a67e367c92ae2ae51851b8e34c35a277f3" + }, + { + "repo": "railiance-platform", + "path": "credential-change-requests/CCR-2026-0041-forgejo-netkingdom-oidc-keycape-eso-reader.yaml", + "sha256": "e69077ad29166770e4a57b4c4105615e272822d8fde11df591d0c127b158ac67" + }, + { + "repo": "railiance-apps", + "path": "workplans/RAPPS-WP-0016-forgejo-netkingdom-login.md", + "sha256": "a74f9fbc3300739bc0d8e31f1de88c0b7226ff2f6990e7cbb3ae433ac84fdb5e" + } +] diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/primary-source-provenance.json b/docs/proposals/2026-10-11-forgejo-consumer-eso/primary-source-provenance.json new file mode 100644 index 0000000..58952b4 --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/primary-source-provenance.json @@ -0,0 +1,11 @@ +[ + { + "url": "https://github.com/external-secrets/external-secrets/blob/v0.16.1/pkg/provider/vault/auth_kubernetes.go", + "sha256": "e53f43c0748e43f408b5a7c5323c12c0fb5fb4299a33718181f6d70945112071", + "finding": "serviceAccountRef requests TokenRequest with600second lifetime, then legacy-token fallback if unavailable; namespace local for SecretStore; no serviceAccountRef means mounted controller token path" + }, + { + "url": "https://external-secrets.io/latest/provider/hashicorp-vault/", + "finding": "Official current documentation cross-check; actual runtime compatibility established by v0.16.1 source and installed CRD/RBAC instead of assuming latest behavior." + } +] diff --git a/docs/proposals/2026-10-11-forgejo-consumer-eso/validation.json b/docs/proposals/2026-10-11-forgejo-consumer-eso/validation.json new file mode 100644 index 0000000..156d4ed --- /dev/null +++ b/docs/proposals/2026-10-11-forgejo-consumer-eso/validation.json @@ -0,0 +1,89 @@ +{ + "recorded_at": "2026-10-11T01:34:15.343635+00:00", + "native_metadata_only": true, + "native_auth_or_mutation": false, + "secret_value_reads": false, + "native_eso_image": "oci.external-secrets.io/external-secrets/external-secrets:v0.16.1", + "served_v1_crd_auth_schema_validation": true, + "actual_common_controller_tokenrequest_rbac": true, + "new_rbac_required": false, + "candidate_exact_kubernetes_objects": 3, + "old_materialization_objects": 5, + "new_materialization_objects": 6, + "original_five_object_bodies_unchanged": true, + "added_object_ids": [ + "consumer-eso-role" + ], + "old_full_render_digest": "sha256:e64b56e4590b57b858303fcda1a474693e3dd821bc83df26646fe78fc345498a", + "proposed_full_render_digest": "sha256:c0afd5576cdae0cbc807eccb2b9fa87ac668f15e32ec7cf79d295bdb23c75b35", + "net_type_and_proposed_case_validation_errors": 0, + "mason_tests": { + "current_source_negative_reproduced": { + "passed": 40, + "failed": 1, + "node": "test_two_role_backed_readers_keep_distinct_ccrs_and_complete_structure", + "cause": "Both role-backed readers used same CCR-ESO output identity, overwriting KeyCape request" + }, + "corrected_stage": { + "passed": 44, + "failed": 0, + "seconds": 5.85 + }, + "duplicate_overwrite_guard_verified": true, + "prior_corrected_stage": { + "passed": 42, + "failed": 0, + "seconds": 6.66 + } + }, + "mason_proposed_type_build_refused": true, + "mason_refusal_reason": "allow-unimplemented only applies to acknowledged type; proposed1.2 has no acceptance", + "platform_draft_validation_exit_code": 0, + "platform_draft_warning": "Bound claim is unconfirmed; apply blocked", + "draft_validation_preparation_history": "Initial in_flight draft used unsupported missing-field names. Corrected to ordinary proposed status with explicit pending owner/case review and bound_claims_confirmed=false.", + "source_statuses_unchanged": [ + "acknowledged type1.1", + "accepted case1.1", + "in_flight current CCR42", + "applied historical CCR41" + ], + "runtime_helm_handover_verified": false, + "native_materialized": false, + "native_followthrough_complete": false, + "founder_acknowledgment": null, + "driver": "ORGREF-WP-0002", + "kubernetes_names_bounded_by_type_parameters": true, + "consumer_role_rollback_precedes_policy_rollback": true, + "generated_ccr_refinement": { + "prior_packet_sha256": "271afd04512b3dac642b3385013cac03d28afde22ff97bcca8d0e262ab3d9279", + "prior_packet_preserved_at": "/tmp/forgejo-consumer-eso-refinement-original-271", + "platform_conditional_manifest_sha256": "9e4967bfff57694ceb12883f522edc405ef3fa316e46b88c9bdad47205a180b4", + "before_fix_counterexamples": [ + { + "node": "test_two_role_backed_readers_keep_distinct_ccrs_and_complete_structure", + "result": "failed wrong consumer target keycape vs forgejo" + }, + { + "node": "test_generated_consumer_lifecycle_requires_created_only_token_recovery", + "result": "failed missing created-only/token-revocation-or-expiry lifecycle" + } + ], + "intermediate_check_history": { + "tests": 9, + "passed": 7, + "failed": 2, + "cause": "case-sensitive test string matched lowercase revoke against capitalized sentence; assertions normalized case without removing required semantics" + }, + "candidate_builder_sha256": "c2b140f380407779ce5478ca57c97eb0e6375d6a9cad4591b0b29ddd177db686", + "candidate_build_tests_sha256": "aea22297178b5bb1eaaf5d59bbd1c71112dfc32bed2075b33ccc0598c8fc5be5", + "candidate_structure_tests_sha256": "2e7446eaf01d4bb59d7d8fb3c3cdc5e6e3007b3ae84d8ed0a49f11f745f2e386", + "keycape1.1_generated_output_unchanged": true, + "consumer1.1_generated_output_changes_only_target_workload": true, + "historical1.1_bundle_unchanged_and_now_refused_for_wrong_target": true, + "structural_checker_weakened": false, + "role_backed_consumer_created_only_token_recovery_required": true, + "proposed_type_or_case_new_acceptance": false, + "ruff_check": "not run; ruff executable absent in owner venv and PATH", + "syntax_checks": "all candidate Python AST parse passed" + } +}