Skip to content

Why these nineteen PRs exist: two passes over credential handling #1585

Description

@GeiserX

Why I opened nineteen pull requests

I've sent a run of PRs over the past couple of days and they probably look disconnected from the outside — a file permission here, a cache key there, a log message somewhere else. They aren't. They came out of one exercise, and I think the whole reads better than the parts, so this issue is the index and the reasoning behind them.

Short version: we run Executor with real credentials, in a place where it matters to us if one leaks. So I went looking, systematically, for every spot where a credential lives longer than it needs to, travels further than it needs to, or ends up somewhere nobody meant it to go. Eighteen of the PRs are what I found. The other one (#1564) is a small capability change that came out of the same look. Sixteen came from the first pass; a second pass over the same question afterwards turned up three more, which are marked below.

None of this is a criticism of the project. Most of what I found is the ordinary kind of thing that accumulates in any codebase handling secrets — a value that was fine where it was written and became a problem two hops away. I found them because I was specifically hunting for them, with a fairly paranoid threat model, not because they were lying around obviously.

What I'd like from you

Putting this first, because the rest is context and you may not want all of it:

  • Review them individually, not as a batch. Each is independent and mergeable alone. If some are wrong or unwanted, I'd genuinely rather they were closed than merged out of politeness.
  • Let a credential provider own the OAuth refresh grant #1564 is the only one that's a design discussion. The other eighteen are ordinary fixes.
  • If approving CI runs for the fork is easy on your side, that would help. None of these have had checks run, because workflow runs from a fork need a maintainer's approval — so you're currently being asked to take my word that the tests pass, which isn't a reasonable thing to ask. If there's a reason not to, that's fine; I'd just stop expecting checks to appear.

Nothing here is urgent, and none of it is a report of something broken in production.

The direction behind it, briefly

Worth saying, because it explains the shape of the whole set. Where we're going is running Executor with credentials held in a TEE — a hardware-isolated environment the host process can't read into. What that buys is a credential the software can use without ever holding: if the machine is compromised, what an attacker finds is ciphertext and a handle, not the key. "We can't read it" is a much stronger thing to be able to say than "we promise not to look".

#1564 is the only PR that moves toward that directly — it lets a provider perform the OAuth refresh itself, so the host never has to be handed the refresh token in order to spend it.

The other eighteen are the groundwork, and they matter more than they look. A sealed store is worth very little while the same credential is also sitting in a cache key, an error message, a world-readable file, or a browser's localStorage — a secret that never leaves the enclave through the front door is no safer if it left through a log line an hour earlier. Most of what I found is exactly that: not the credential store failing, but copies of the credential accumulating around it.

That's also why I'd rather these were judged as ordinary fixes than as a strategy. Each one is worth doing whether or not anyone ever puts a TEE behind it.

One thing worth adding, since it changes how much weight to put on the above: the TEE half is not hypothetical. Executor's credential paths have been exercised against a real hardware-attested enclave — GCP Confidential Space, production image, attestation verified against Google's live JWKS rather than a fixture. The findings themselves came from driving Executor directly rather than out of that run — the enclave is what they are groundwork for, not where they were caught. It is still why several of them are about what an operator can observe: in that setting the interesting failure is not a credential leaking, it is a control that quietly did not engage while everything still looked fine.

The question I was asking

For each credential Executor touches, I asked three things:

  1. Where does it end up? Not where it's meant to go — where it actually ends up. Error messages, cache keys, log lines, browser storage, module-level globals, files on disk. 2. How long does it stay? A secret that's correct to hold for one call is a different thing when something keeps it for the lifetime of the process. 3. Who can read it once it's there? File modes, and what a person with access to the machine or the logs would see.

That framing is worth stating because it explains why the PRs look scattered. They're scattered because credentials are scattered; the question was the same every time.

Credit where it's due

A good part of the thinking behind this — particularly the idea that a credential should be usable without ever being held, and that "we simply cannot read it" is a stronger promise than "we promise not to look" — came out of conversations with @alexboone29. The framing is his. The bugs are mine to have found and, where I got something wrong in a PR, mine to have got wrong.

The pull requests

A capability, and the only one that's a design question rather than a fix:

  • Let a credential provider own the OAuth refresh grant #1564
    Let a credential provider perform the OAuth refresh exchange itself, instead of handing the refresh token to Executor to spend. Optional; providers that don't implement it are completely unaffected. This is the one that needs a direction from you, and the rest don't depend on it.

Credentials ending up somewhere they weren't meant to:

Credentials outliving what they were for:

Files anyone on the machine could read:

Telling the truth about state:

Finally

Happy to split, rebase, or rework any of it. Thanks for building this — it's genuinely good software, which is why we're using it somewhere that matters.

Activity

  1. changed the title [-]Why these thirteen PRs exist: one pass over credential handling[/-] [+]Why these sixteen PRs exist: one pass over credential handling[/+] on Aug 14, 2026
  2. RhysSullivan commented on Aug 15, 2026

    @RhysSullivan
    Collaborator

    @GeiserX Thanks for the PRs - have been traveling this week so haven't gotten to look at them yet will review Sunday / Monday

    Can you do a non ai write up though about how these PRs have been generated and tested, there's too much ai slop in the writing atm

  3. GeiserX commented on Aug 15, 2026

    @GeiserX
    ContributorAuthor

    Hey @RhysSullivan

    Here I can share a plain version of the goal I've been following, in my own writing (yeah everything so far has been AI, but I recommend you to read this issue message above, as it's the most edited-by-me one - PRs have been lightly edited if at all):

    • We have the goal to protect executor from rogue agents exfiltrating credentials after a hack, especially Oauth credentials in this case. So when we saw it's handling sensible info inside executor, even when configuring an external CredentialProvider, we decided to start working on it.

    • So the first PR was in this area of adding a refreshGrant to your credential provider (1Password, or any TEE-backed vault) to perform the refresh instead of this software handling it itself (I tested with my own developed vault, which is part of the solution I'm building, but you could do it with any credential provider)

    • I swept your codebase to find places in which passwords are handled, not only in this refresh mechanism area, and started building the motto of "use without revealing" credentials. So I went asking the same three things of every credential: where it ends up, how long it stays, who can read it once it's there... I might have overstepped and done a bit more than that (Especially the section I wrote about "Telling the truth about state" here in this issue above, but still i think it's valuable nevertheless... Also found a CVE btw)

    • Ran all the 16 PRs "merged" locally with my own vault and I could see it all working fine. Proofs (with my Vault deployed in a GCP Confidential Space.

    Here showing my vault configured with my provider in the GCP Confidential Space, can't show much:
    Image

    Here #1576 - I just asked if we could show at least anything I did... and this is the only thing that could be "felt" haha...
    Image

    PS. I've seen you working and here answering in my local time (EU time) so happy travelling around this part of the world! (just assuming, maybe you're in Asia TZ) :)

  4. changed the title [-]Why these sixteen PRs exist: one pass over credential handling[/-] [+]Why these nineteen PRs exist: two passes over credential handling[/+] on Aug 15, 2026
  5. GeiserX commented on Sep 20, 2026

    @GeiserX
    ContributorAuthor

    @RhysSullivan thanks for taking the seventeen in August.

    Three loose ends, then I'll leave this issue alone:

    1. Delete the credential a connection minted when the connection is removed #1568 stopped applying after the removal cascade in Cascade integration removal to every member and stop serving orphaned rows #1991, so I closed it and redid both halves on top of the cascade in fix(sdk): delete the credentials a connection minted when it or its integration is removed #2083: connection removal and integration removal now delete the credentials those connections minted.

    2. Let a credential provider own the OAuth refresh grant #1564 had drifted too far to rebase, so I closed it as well and re-sent it rebuilt on current main as feat(sdk): let a credential provider perform the OAuth refresh grant itself #2084. It is the same optional seam: a credential provider may perform the refresh exchange itself, and nothing changes for providers that don't implement it.

    3. The private security report I filed on 13 August is still in triage. When you have a moment, a first read from you would help.

  6. RhysSullivan commented on Oct 8, 2026

    @RhysSullivan
    Collaborator

    We're clearing the backlog ahead of the v2 launch, so we're closing this. If it still applies to v2, please open a new issue or PR against v2.

    Sent from my Claude

  7. GeiserX commented on Oct 9, 2026

    @GeiserX
    ContributorAuthor

    Checked the two closed ones against the v2 export at 44ec94c. Credential cleanup on removal (#2083) already holds there, so it is not re-filed; what still outlives its resource on v2 is an event subscription and its signing secret, fixed in #2242
    Provider-owned refresh (#2084) still applies, since the host decrypts the refresh token and client secret at every renewal. It is redone in the shape of v2 Credentials in #2243 with the tests stacked on it in #2244

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions