Repository navigation
Per-workspace sandbox environment Secret, writable only by admins - #771
Conversation
…dmins
When sandbox.env_secret names a Secret, the sandbox template adds
envFrom: [{secretRef: {name: <name>, optional: true}}] to the main
container. Every key of that Secret in the workspace namespace becomes an
environment variable of every sandbox there, by reference: values are not
in the Workload or the Pod spec, a workspace without the Secret is
unaffected, and explicit env entries (the template's own, a Workload's)
keep precedence. Adding a variable is adding a key; no chart or code
change. Empty (the default) renders the template exactly as before.
Because every key reaches every sandbox in the workspace, the apiserver
refuses create, update and delete of a Secret with that name unless the
caller is a system admin or administers every workspace it is bound to;
the ordinary role otherwise lets any member create one.
Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ding Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
…main Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
The apiserver reads its config once at start-up, so an upgrade that sets sandbox.env_secret left running apiserver pods guarding no name while new sandboxes already read the Secret. The name is now also a pod-template annotation of the apiserver Deployment, rendered only when set. A name helm parses as a number or boolean (0, false) is no longer taken as unset. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
The scan checks every changed file whole, and secret.go has always set an image secret's Password from the decoded request parameter; image.go is already listed for the same pattern. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
- resource-manager never takes over a sandbox-env already in a workspace namespace without the mirror label, even for an explicit ids grant, so a workspace admin cannot overwrite (or, by unbinding, delete) one an administrator made there. - The name is fixed to sandbox-env and reserved by the apiserver whether or not the feature is on, so turning the sandbox template on opens no window in which an apiserver pod with the old config lets a member take the name. The apiserver config key and pod-template annotation are gone; the chart and upgrade.sh refuse any other name. - Deleting the reserved Secret retries a lost precondition on the re-read, re-checked Secret and explains a persistent conflict. - A nil user is never an admin of the reserved name. - The patch error log no longer dereferences a Secret the conflict path may have cleared. - The secret scan exempts only the Password: password line of secret.go, not the whole file. - Docs: key names must be valid environment variable names; which of the two ways wins when mixed. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
- Document that an API PATCH of params replaces every key, and how to add one key with kubectl without dropping the others. - Only a system admin may change or delete a sandbox-env that a workspace selector mirrors (selector or selector.applied present). - Bind sandbox-env through the image-secret API with an optimistic lock; on a conflict re-read and re-authorize. - A MirrorConflict on sandbox-env fails the reconcile (retried with backoff); every Secret records conflicting workspaces in the primus-safe.workspace.mirror.conflicts annotation. - A sandbox-env deleted concurrently during a delete retry is success. - The delete retry and its message apply to sandbox-env only; other Secrets are deleted exactly as on main. - The refusal no longer names the Secret's workspaces; they are logged. - authSecretUpdate returns the user it resolved. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
The ordinary role check's 403 lists the workspaces it checked. The name sandbox-env is known to everyone, so for that name the get, patch and delete refusals are replaced by one that names none; the role check still logs them. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: zoroyihan7 <Yihan.Wang@amd.com>
|
Second review round addressed in 81ea08d and d019016:
|
bindSecretToWorkspace retried on a conflict, but only the sandbox-env branch re-read the Secret. For any other image Secret the first attempt had already added the workspace to the in-memory annotation, so the retry found it "already bound", wrote nothing and returned nil: the workspace listed the pull Secret, the API answered 200, and the Secret's primus-safe.workspace.ids never named the workspace, so it was never mirrored there. The ids are now changed on a copy and copied back only once the patch succeeded, and a conflict re-reads the Secret for every name, so the retry writes onto the stored bindings and keeps one added meanwhile. A binding that keeps conflicting returns commonerrors.NewConflict with a "try again" message, as deleteSandboxEnvSecret does. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A sandbox-env made in the workspace namespace winning over the mirrored one is a state the docs support, so failing the reconcile on it turned a stable configuration into an endless backoff that logged an error and raised a MirrorConflict event on every retry. The conflict is already in primus-safe.workspace.mirror.conflicts. A mirror conflict is no longer a reconcile error for any name. The warning log and the MirrorConflict event move from syncSecretToWorkspace to recordMirrorConflicts and are raised only for a workspace the record does not list yet, as InvalidWorkspaceSelector is raised once per reason. A sandbox-env with a conflict is checked again after ten minutes, because removing the namespace's own Secret changes nothing the controller watches and the copy is then due; other names keep their behaviour. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… docs install.sh renders values.yaml itself and never read sandbox_env_secret or sandbox_extra_ca_secret, so a fresh install silently ignored both. It now takes them from its environment (it has no .env to read), checks sandbox_env_secret before anything is installed (case-sensitively, before nocasematch is set), applies the same sed as upgrade.sh, and writes both to the .env it generates, so the next upgrade.sh does not turn them off. docs/apis/secret.md: - The workspace image-secret API takes image Secrets only, so a general sandbox-env gets 400 there before any role check; only an image Secret of that name reaches the admin check (403). - A mirror conflict is recorded, raised once and rechecked every 10 minutes; it is no longer a reconcile retried with backoff. - Only writes from this version on are checked: give the commands that show who made an existing sandbox-env, in primus-safe and on each data plane, before turning the feature on. - A fresh install may turn the feature on through install.sh. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Third review round addressed in e90e17a, b413405 and 1821b01 (head 1 (blocking): an image binding that lost a conflict reported success. Fixed in
2: a sandbox-env mirror conflict was a reconcile error. Changed in b413405. A mirror conflict is no longer a reconcile error for any name. The warning log and the
3: 400 rather than 403 on the image-secret API. The type check stays as defense in depth, and 4: an exhausted retry on the reserved binding. It is now
5: install.sh ignored both sandbox settings. Fixed in 1821b01. install.sh has no
6: a Why not here:
Proposed design:
Trade-offs:
Meanwhile, Local runs on
|
What
A generic per-workspace sandbox environment Secret, named
sandbox-env.sandbox.env_secret(chart value,.envkeysandbox_env_secret) acceptssandbox-envor empty. Empty (the default) renders the sandbox template exactly as before. Any other value makes the chart (andupgrade.sh/install.sh) refuse.envFrom: [{secretRef: {name: sandbox-env, optional: true}}]. Every key of that Secret in the workspace namespace becomes an environment variable of every sandbox in that workspace.env, or the Pod spec.enventries (the template's own, or a Workload'senv) keep their value; the Secret only adds variables.InvalidEnvironmentVariableNamesevent. An APIPATCHwithparamsreplaces every key of a general Secret (unchanged API contract), so through the API send the full set (GET, add the key, PATCH all); the docs show akubectl patch --type=jsonthat adds one key and keeps the others.sandbox-envunless the caller is a system admin or an admin of every workspace it is (or will be) bound to (403). The same check covers binding an image Secret of that name through the workspace image-secret API (which takes image Secrets only, so a generalsandbox-envgets 400 there before any role check), is repeated on each conflict retry of an update, delete or binding, and treats a missing user as no admin. Once the Secret carries a workspace selector (primus-safe.workspace.selector, or a non-emptyselector.appliedrecord), only a system admin may change, delete or rebind it, because the selector reaches workspaces the ids list does not name. No 403 forsandbox-envnames its workspaces: neither the guard's nor the ordinary role check's on get, patch and delete, which is replaced for that name only (the workspaces are logged server-side). The role check's message for other names is unchanged. The name is reserved whether or not the feature is on and needs no apiserver configuration, so turning the feature on opens no window in which an apiserver pod would let a member take it. That is why the name is fixed: a site-chosen name could only be guarded after the apiserver restarted with it, while the sandbox template may already read it.sandbox-envalready in a workspace namespace without the mirror label, even for an explicitprimus-safe.workspace.idsgrant. It leaves it as it is, so it is neither overwritten nor deleted when the grant is withdrawn, and makes the conflict visible: the conflicting workspaces in the newprimus-safe.workspace.mirror.conflictsannotation on the source Secret (written for any Secret, removed when there are none), and aMirrorConflictWarning event raised once for each workspace that enters it, not on every reconcile. A conflict is not a reconcile error for any name (the Secret in the namespace winning is a supported state); asandbox-envwith one is checked again every 10 minutes (RequeueAfter), so its copy is made once the namespace's own Secret is removed, which changes nothing the controller watches. Other names are not rechecked on a timer, as before. Copies the mirror made itself are synced and removed as before. Other Secret names keep today's ids-grant adoption.sandbox-envis also patched with an optimistic lock and re-authorized on each retry; other names are patched without a version, as before. A binding that keeps conflicting is a 409 (Primus.00012) that says to try again, as the reserved delete is.docs/apis/secret.md#sandbox-environment-secret(operator how-to: the two ways to provide the Secret, kubectl in the workspace namespace or the API mirror, must not be mixed in one workspace; which one wins if they are),docs/installation/upgrade.md(on a site running an older version, turn it on in an upgrade after the one that installs this version),docs/installation/install.md(a fresh install can turn it on:sandbox_env_secret=sandbox-env ./install.sh).authSecretUpdatereturns the user it resolved, so the patch handler resolves it once. The patch handler's failure log no longer dereferences a Secret the conflict path may have cleared..github/workflows/block-sensitive-secrets.ymlgains a line-level exemption, used only for thePassword: password,line ofresources/secret.go; the rest of that file is still scanned.install.sh, which renders its own values and never read either sandbox setting, takessandbox_env_secretandsandbox_extra_ca_secretfrom its environment, checks the first before installing anything, renders both asupgrade.shdoes, and writes both to the.envit generates so the nextupgrade.shkeeps them.Tests
common/pkg/config: the default renders noenvFrom.sandbox-envrendersenvFromwith the name,optional: true, and the explicit env is identical to the unset render. Every other value (other names,0,false, invalid or over-long names) makes the chart refuse. The apiserver Deployment and ConfigMap render identically with and without the feature.helm templatewith defaults is identical to the merge base.job-manager/pkg/dispatcher: dispatching through the chart's own template keepsenvFromon the main container, in the workspace namespace. A Workload-set variable stays an explicit env entry. A sentinel value stored in the workspace Secret appears nowhere in the marshalled Workload or dispatched object. The default template references no Secret.resource-manager/pkg/resource: asandbox-envconflict inws-adoes not fail the reconcile, asks forRequeueAfter10m, records["ws-a"]in the conflicts annotation, raises oneMirrorConflict, and still serves the other workspaces; reconciling again raises nothing; a new conflict inws-craises one event, forws-conly; once both foreign Secrets are removed the copies are made, the annotation is removed and no requeue is asked. A selector conflict on another name is recorded, raised once over two reconciles, and not rechecked. Asandbox-envmade inws-ais not adopted when an API Secret of that name is bound tows-a(data and labels unchanged,MirrorConflictraised), and survives the unbind. Reverse proof: the same sequence with another name still adopts and then deletes. The mirror's ownsandbox-envcopy is still synced and removed on unbind.apiserver/pkg/handlers/resources: uses the roles the chart installs, not hand-written ones.sandbox-env, even when they own it, with no configuration set, and nothing is written.sandbox-env: the patch carries the read resourceVersion; on a conflict with a concurrent rebind to a workspace the caller does not administer the retry is refused and not written; with one they do administer, the retry carries the new resourceVersion and keeps the concurrent binding. Another image Secret's patch carries no resourceVersion.ws-c) is written again from the re-read Secret: throughPATCH /workspaces/ws-b, 200, two patches, stored ids["ws-a","ws-c","ws-b"](red on the previous head: one patch, stored["ws-a","ws-c"], still 200). A patch that fails leaves the caller's Secret unbound. A binding that keeps conflicting returns, for either name, a 409Primus.00012that says to try again, also through the HTTP error mapping.sandbox-envwith a selector, a blank selector, or aselector.appliedrecord; a system admin is not; without a selector, or with an empty record, the admin is let through.sandbox-env. Reverse proof: for another name the role check's 403 still names them.NEW_VARleaves onlyNEW_VAR(the documented replace semantics).sandbox-envafter a binding conflict (loses the concurrent binding); changing the caller's Secret instead of a copy; not recording conflicts; allowing the mirror to adoptsandbox-env; keying the guard on an (empty) config value; deleting without retry; dropping the nil-user check (panic); loggingsecret.Nameafter a vanished Secret (panic).secret.go(passes), on a copy with an extraPassword:literal (caught; the old whole-file exemption let it pass), and with no exemption (the existing line is flagged); replayed again from the workflow file on this head.install.sh: with stubkubectl/helm,other,SANDBOX-ENVandsandbox-env2exit 1 before any kubectl call; its twoseds applied to the chart'svalues.yamlrenderenvFromand the extra-CA volume withhelm template; no new shellcheck warning.Known limitations (deliberately not built)
primus-safe) can still write the Secret. This PR adds no webhook or RBAC change; only the SaFE API is guarded.sandbox-envwritten by a non-admin before this version is installed is still mirrored, and its keys would reach the sandboxes of its workspaces once the feature is on. The docs give the commands that show who made each one; check before enabling. A code-level guard (an approval annotation the apiserver stamps on admitted writes and the mirror requires) is proposed as a follow-up.envFrom. If it cannot, the Pod may be refused even withoptional: true. Verify per site before setting the value there; this is why the default stays empty.Not verified
No deployment. Not yet run on a live site: arbitrary key injection, user-override precedence, the cross-workspace boundary, rotation, the 403 through the real API path, the mirror refusal against a real data plane, and the external-node behaviour. All rely on Kubernetes
envFrom/namespace semantics and the unit tests above.🤖 Generated with Claude Code