feat: polymorphic auth with OIDC federation on the target config - #179
Merged
Conversation
Config now carries the nested Auth JSON block alongside the flat Region/Profile fields the Pkl schema renders it beside. effectiveAuth resolves the two into one discriminated block: an explicit Auth wins, an absent/null/whitespace-only Auth synthesises a DefaultChain from the flat Profile, and setting both is rejected in Go too, mirroring the Pkl-level constraint for callers that bypass the schema. ToAwsConfig keeps its signature and dispatches on DefaultChain | Oidc; an unknown discriminator errors. The Oidc arm requires a non-nil OidcDeps.Source and fails closed rather than ever falling back to ambient credentials; the actual STS/web-identity credential composition is a follow-up and currently returns a clear not-yet-implemented error. OidcDeps is owned per plugin instance (threaded via WithOidcDeps), not process-global, so its warn-once deprecation notice and future credentials cache stay instance-scoped and test order stays independent. Config values with no OidcDeps at all still see the flat-profile deprecation warning once per process via a package-level fallback.
OidcDeps.caches has no reader until the follow-up credential-composition work lands, so golangci-lint's unused check flags it. The field's shape is mandated by the auth plan and gains its consumer next, so suppress rather than delete it.
Plugin now implements plugin.OidcAware: SetOidcTokenSource wires an
OidcDeps (backed by the production STS factory) onto the plugin instance,
and every FromTargetConfig call site threads it onto the parsed Config via
WithOidcDeps before the config is used. A nil deps value (no broker paired,
or an old agent) flows through unchanged: WithOidcDeps(nil) is a no-op and
Oidc auth keeps failing closed rather than falling back to ambient
credentials.
Also documents NewOidcDeps as the required constructor (a bare OidcDeps{}
literal with Source set panics on first Oidc use, since stsFactory is left
nil), and notes why oidcCacheKey omits the token source's identity.
CHANGELOG: polymorphic auth (DefaultChainAuth | OidcAuth), the flat profile deprecation (works unchanged, warns once per process), the OidcAuth agent-broker requirement, and the one-time target-metadata update existing targets see on first reconcile after upgrade. formae-plugin.pkl: note beside minFormaeVersion that 0.89.0 must be the release that ships the oidc-credential broker, to be confirmed at release time rather than assumed from the current dev line.
Migrating a target from the flat profile to an auth block removes the top-level Profile key from the rendered config. Profile carried no config field hint, so the agent classified that as an immutable change and planned a target replace, which destroys and recreates every resource on the target. Hinting profile mutable, as auth already is, makes the deprecation's own migration an ordinary target update.
Give the AssumeRoleWithWebIdentity exchange a fixed role session name so the assumed-role principal is attributable in the customer's CloudTrail instead of a random string. Read the STS client factory into a local and fall back to the production one when it is nil, so an OidcDeps built as a bare literal works without a write that would race a concurrent reader of the same struct.
Say that moving a target from flat profile to an auth block is a normal update because both fields are mutable, and that an agent older than the release carrying those hints plans a target replace instead, so the migration has to follow the upgrade. Note that the OIDC token exchange runs on a default-configured STS client, so AWS_USE_FIPS_ENDPOINT, AWS_ENDPOINT_URL_STS and custom CA bundles are not honoured on it.
The previous pins pointed at a formae feature branch head, which is not reachable from main. Both now resolve from main: pkg/plugin at the commit that gives its own credential requirement a resolvable version, and pkg/credential at the commit on main that carries the module. The stale pre-merge notes are gone; the ergo fork replace stays, since a replace is not transitive and both modules need the -pel.6 revision.
JeroenSoeters
force-pushed
the
feat/oidc-auth
branch
from
August 21, 2026 22:48
81b4764 to
aef65f4
Compare
JeroenSoeters
marked this pull request as ready for review
August 22, 2026 01:40
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds federated (OIDC) authentication as a second option alongside the existing
default credential chain on the AWS target config.
Authis now a discriminated sum on the target config —DefaultChainAuth(env vars, shared config, IMDS/IRSA, optionally pinned toa shared-config
profile) orOidcAuth(roleArnonly; issuer, key andtoken endpoint all belong to the paired broker, not the target). The legacy
flat
profilefield is deprecated but unchanged; setting bothauthandprofileis rejected at eval.Config.ToAwsConfigkeeps its signature and dispatches onDefaultChain | Oidc; an unknown discriminator errors.effectiveAuthresolves flat
profileand the newauthblock into one value, mirroringthe Pkl-level mutual-exclusion constraint for callers that bypass the
schema.
oidcCredentialsProviderexchanges the broker-issued identity token for STS credentials by assuming
OidcAuth.roleArn. The refresh deliberately runs on its own boundedcontext, derived from the operation's ctx with
context.WithoutCancelplusa 30s deadline: that keeps the request-scoped values the token source needs
to reach the right broker, while not pretending to honour caller
cancellation. Caller cancellation is honoured by
aws.CredentialsCache'sown select around the refresh, not by propagation into it — the cache
already suppresses cancellation on the refresh context it hands down, so
propagating it here would be an illusion. Credentials are cached per
(region, roleArn, sha256(rawAuth))key, scoped to the owningPlugininstance (
OidcDeps) — never process-global.OidcAwarethreading:Pluginnow implementsplugin.OidcAware;SetOidcTokenSourcewires the broker-backed token source onto the plugininstance, and all 6 per-operation call sites (consolidated; across
Create/Update/Status/Delete/Read/List) thread it onto the parsedConfigviaWithOidcDepsbefore use. A plugin instance with no brokerpaired (or an old agent that never calls
SetOidcTokenSource) flows throughunchanged:
OidcAuthtargets fail closed with an explicit error rather thanever falling back to ambient credentials.
Flat-path compatibility
A target that declares only the legacy flat
profile(or nothing at all)evaluates byte-identical old schema vs. new — verified by a dual-install
eval diff of the same forma against the pre-change and post-change schema.
The flat path continues to authenticate exactly as before; the only visible
change is a single deprecation warning, logged at most once per plugin
process, pointing at
auth = new DefaultChainAuth { profile = ... }.One-time metadata update, not drift
Adding the
Authfield hint to the schema is a change formae's schema-changebookkeeping records against every existing target's stored metadata,
regardless of whether that target's declared configuration changed. Expect
exactly one resource-inert target-metadata update per target on the first
reconcile after upgrading to a formae agent + aws plugin pair that carries
this change — no cloud resource is read, created, updated, or destroyed by
it, and it does not recur. Called out here and in the CHANGELOG so it isn't
mistaken for drift.
Dependency: cannot merge before formae PR #661
OidcAuthrequires agent-side support that has not landed yet:minFormaeVersion = "0.89.0"informae-plugin.pklnames the formaerelease expected to carry the oidc-credential broker this plugin depends
on; the comment beside it says to confirm the real number at release time
rather than trust the dev-line guess made now.
plugin.OidcAware,plugin.OidcTokenSource, the credential-broker wiring) lives on formae PR#661, which has not merged. This PR must not merge before it.
go.modcurrently carries pre-merge pins that must be re-pinned to realtags before this PR merges (release-path follow-up work, not done here):
github.com/platform-engineering-labs/formae/pkg/plugin—v0.4.2-0.20260821030446-6aa3db2765ec(pseudo-version at thecredential-broker branch head)
github.com/platform-engineering-labs/formae/pkg/credential—v0.0.0-20260821030437-d18cdc5fa355(indirect, pseudo-version, same branch)replace ergo.services/ergo => github.com/JeroenSoeters/ergo v1.999.320-pel.6— a fork revision required transitively by the two pinsabove
(The
ergo.services/actor/statemachinereplace is unchanged frommainandneeds no re-pin.)
Both
go.modpin comments already flag themselves for re-pinning beforemerge.
Deprecation policy
Flat
profilekeeps working with no forced migration. It will be removed ata posted future major version, not in this release and not on any date fixed
today.
Migrating flat
profileto anauthblock is non-destructive.profilenow carries
@formae.ConfigFieldHint { createOnly = false }, matchingauth. Without it the migration drops the top-levelProfilekey from therendered config, the agent classifies an unhinted key change as immutable,
and the target is replaced — destroying and recreating every resource on
it. With both fields hinted mutable, the rewrite is an ordinary target
update.
The hints only take effect on an agent at or above the release that carries
them. An older agent still plans a replace. The infrastructure repo's own
flat-target migration must therefore run only after its agents are on that
release or newer, and the CHANGELOG says so.
Out of scope (tracked separately)
infra PR fix(elbv2): mark Action.forwardConfig as hasProviderDefault #48)
release)
local-only; follow-up if cheap)
Test plan
make verify-schemamake verify-examplesgo test -tags=unit -race ./...go vet ./...make lintmake buildpkg/plugin/pkg/credential/ergo-fork to real tags once formaePR #661 merges, before merging this PR
minFormaeVersion = "0.89.0"still matches the release thatactually ships the broker, at release time