picatz / picatz/flowstate

Security posture for the plugin ecosystem: separation of compute/network/storage, JIT credentials, no secrets at rest or in memory longer than needed

Open
#100 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

design kind/design-record security
Dominant language
Go
Stars
9
Forks
0
Avg merge
3h 3m
Merged PRs (30d)
509

Description

A worker holds credentials and can reach internal networks; a plugin is someone else's code running inside it. As the plugin ecosystem grows (#95) and outbound identity federation lands (#94), the security posture is the gating concern for the whole thing, not a follow-up once plugins are popular. This issue is the umbrella; #94 and #95 should be read against it rather than shipped independently of it.

Where the repo already agrees with this, so the design has a floor to stand on:

  • Invariant 7 (ARCHITECTURE.md): a secret reaches a worker as a reference and is resolved only inside the activity that needs the value — never in workflow history.
  • secrets.Secret already holds material in a closure rather than a field, specifically because Go strings are immutable and cannot be zeroed (CLAUDE.md, "Secrets never enter workflow history"). Reflection through unexported fields and Temporal's failure-converter unwrap chain are both named as leak classes already fixed once.
  • A plugin process launches with a deliberately minimal environment — see pluginEnv in https://github.com/picatz/flowstate/blob/fa83d8ba827de38783181e4a20028f8438604907/pkg/flowstate/v1/plugin/launch.go — only protocol variables plus operator-named Config.Env, not a copy of the worker's own environment. That is real isolation of one resource (env vars) and the template for the others.
  • exec: is specified as built-in and denied by default, gated on a policy that does not exist yet (docs/DSL.md) — process execution was deliberately not shipped ahead of its sandbox story.

What is not yet true and this issue is about:

  1. Compute isolation stops at the process boundary. A plugin is a separate OS process (real, and the right first move — panics and dependency conflicts stay contained), but there is no filesystem confinement, no network confinement, no syscall confinement. A plugin that resolves secrets and a plugin that only formats strings run with the same access to the host today. Consider what a plugin declares it needs (network egress, filesystem paths, nothing) as part of its manifest, enforced by the host at launch — a plugin's own netpolicy-equivalent scoped narrower than the worker's.

  2. JIT credentials, not standing grants. The outbound federation design in #94 already shapes this correctly — a broker mints a short-lived, audience-scoped assertion per step, exchanged for a downstream credential that exists only inside the activity call. The missing piece is making that the only path: no plugin or task should be able to hold a credential across steps or cache one past the call that needed it. secrets.Store.For(identity) already returns a scoped Resolver rather than a bag of values — extend the same shape to federated credentials once #94 wires the broker into the worker.

  3. Zeroing secret material. Worth checking concretely rather than assumed: does the Go toolchain version this repo pins (go.mod's go 1.25.4 / toolchain go1.26.5) have a landed API for explicit memory zeroing of sensitive byte slices? If yes, secrets.Secret's closure-based value should use it on release; if no, say so in the package doc as a known limitation rather than implying more protection than a closure over an immutable string actually gives. Either way this is a doc-and-maybe-one-function change, not a redesign — the containment shape is already right.

  4. Secure transport for the resolution path. The plugin protocol runs over a Unix domain socket per launch, capability-scoped by a random token (internal/protocol) — that is already the right shape for local plugin RPC. External secret backends (Vault/OpenBao, #95) need their own transport story: mTLS or the backend's own short-lived auth (Vault's Kubernetes/AppRole login), never a long-lived token sitting in worker config. The vault provider ancestor already in tree (secrets/vault) is where this gets proven.

  5. Storage: nothing at rest that does not have to be. No plugin, task, or resolved secret value should be written to disk by the worker outside of what the plugin process's own launch directory needs (its socket, already 0700 — see launch.go). Worth an explicit audit once #91 (secrets resolution) lands: trace every byte a resolved secret touches from Resolve to the HTTP request that consumes it, and confirm none of it is written, logged, or serialized anywhere but the one header.

Composability framing, since the ask was specifically about composing compute/network/storage separation rather than bolting on one global sandbox: these should be three independently statable policies — what a plugin/task may execute, what it may reach over the network (already netpolicy, #92 gives it an operator surface), and what it may read or write on disk — so a deployment can grant a plugin network access without filesystem access, or vice versa, rather than an all-or-nothing trust decision per plugin.

Not proposing an implementation here — this is the umbrella issue naming the concern precisely so #94, #95, and the exec: policy don't each invent a partial answer independently.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Invariant 7 in ARCHITECTURE.md, pluginEnv in pkg/flowstate/v1/plugin/launch.go, docs/DSL.md, secrets.Secret, secrets/vault, internal/protocol, and go.mod. Read #94, #95, #91, and #92 to map the dependent designs. Done means the compute, network, storage, credential-lifetime, transport, and zeroing policies are independently specified and their secret-handling paths are audited.

Written by the indexing model from the issue text.

Assessment

Tech stack
go
Domain
backend, security
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Needs clarification
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.