Skip to content

feat(crossplane): PoC — Crossplane ObjectStore capability for pack infrastructure (AWS/EKS) - #502

Draft
oldsj wants to merge 13 commits into
mainfrom
crossplane-operator
Draft

feat(crossplane): PoC — Crossplane ObjectStore capability for pack infrastructure (AWS/EKS)#502
oldsj wants to merge 13 commits into
mainfrom
crossplane-operator

Conversation

@oldsj

@oldsj oldsj commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Proof-of-concept implementation of the Crossplane capability boundary from
ADR-0012 (#501),
addressing the research/PoC request in
#453.

It lets an authorized cluster provision the external cloud infrastructure a
software pack needs (the motivating case: an S3 bucket + keyless access for
MLflow artifacts) in-cluster via Crossplane, while OpenTofu keeps ownership of
the foundational cluster. Deliberately narrow per the ADR: one ObjectStore
capability, one AWS/EKS implementation, one consumer
— not a capability
marketplace.

Warning

PoC, not production. Opening as a draft. The ObjectStore Composition is
still hardcoded to the phase-0 PoC cluster (account/region/cluster/prefix), and
the ADR's validation gates
(functional, security, lifecycle/ops) are not yet all met. See Not in scope below.

What's included

  • Day-0 IAM (OpenTofu)crossplane-iam.tf: opt-in AWS permissions for the
    Crossplane provider controllers, provisioned only when a capability is enabled.
    No static credentials — EKS Pod Identity throughout.
  • Conditional install + gating — Crossplane core, the providers Application,
    and the shared provider config install only when at least one capability is
    opted into (ADR-0006 conditional-foundational path); provider packages are
    gated per-capability.
  • Config surfacecrossplane_capabilities: [s3] on the AWS cluster;
    enabling s3 also pulls in the IAM and EKS providers needed for per-workload
    Pod Identity bindings.
  • ObjectStore example (examples/crossplane/objectstore/) — the platform
    XRD + Composition, a consumer claim, and a probe pod that proves scoped
    read/write. Marked PoC-only/hardcoded.

IAM security model

Implements ADR-0012's recommended dedicated-account model: all provider
controllers share one broad account-local role (assumed via Pod Identity),
with outer denies for IAM privilege-escalation containment (writes confined to
the workload-role path, permissions boundary required on role creation, scoped
iam:PassRole) plus a best-effort collision guard on the foundational EKS
control plane. Provisioning identity stays separate from runtime identity: each
ObjectStore gets its own bucket-scoped, boundary-constrained workload role.

An earlier revision of this branch implemented the stricter per-capability-role
variant; it was refactored to the dedicated-account baseline the ADR proposes for
initial support (simpler OpenTofu→Crossplane handoff, single aws ProviderConfig).

Not in scope (follow-ups)

  • Parameterizing the hardcoded Composition (EnvironmentConfig + a verbatim-copy
    writer change) so it's a portable NIC-managed contract.
  • End-to-end verification of the IAM against the live Composition, and the full
    ADR validation gates (Access Analyzer, isolation tests, destroy/recovery drills).
  • Supply-chain digest pinning, retention/inventory, nic destroy pre-destroy phase.
  • Azure/GCS Compositions, a second capability, admission policy hardening (#480).

Testing

go build ./..., go vet ./..., and go test ./... all pass; terraform fmt
clean on the templates. IAM guardrails covered by
pkg/providers/cluster/aws/crossplane_iam_test.go; gating by
TestWriteAllToGit_CrossplaneCapabilities.

Related

  • ADR: #501
  • Request: #453
  • Admission policy (defense-in-depth): #480

oldsj added 13 commits July 15, 2026 09:49
Add the foundational, out-of-cluster half of the Crossplane pack-infra PoC
(ADR-0012). Clusters opt into named capabilities via `crossplane_capabilities`
(bare keys "s3"/"rds", validated at config load); each enabled key provisions a
scoped EKS Pod Identity role and least-privilege policy in crossplane-iam.tf so
enabling a capability is an explicit, per-capability authorization rather than a
blanket grant.

- config: crossplane_capabilities field, validation against the supported set,
  and EnabledCrossplaneCapabilities() returning provider-qualified ids (aws-s3).
- tofu: thread sorted capability keys into tfvars for deterministic output.
- crossplane-iam.tf: one name-prefixed role/policy per capability, scoped to
  project-prefixed S3/RDS ARNs and guarded by RequestTag/ResourceTag on the
  crossplane-providerconfig tag; crossplane_iam_test.go locks those guardrails.
- cluster.InfraSettings gains CrossplaneCapabilities (the shared, provider-
  agnostic contract) plus the CrossplaneEnabled() predicate consumed in commit 2.
…ties

Add the in-cluster half of the Crossplane PoC (ADR-0012): the core chart and
providers ArgoCD Applications, plus per-capability provider packages, provider
configs, and post-install config Applications for aws-s3 and aws-rds.

Crossplane is provider/profile-conditional foundational software (ADR-0012 §3):
gate the entire tree on CrossplaneEnabled() so a cluster that opts into no
capability receives none of it -- the core chart, providers Application, and
every manifest are pruned from the gitops repo, not just skipped. When at least
one capability is enabled, foundational manifests are written and the existing
per-capability gate trims the un-opted-in layers.

Drop the bundled provider-keycloak package: it is out of scope for this PoC.
Opting into the s3 Crossplane capability now also enables the narrowly
scoped aws-iam and aws-eks provider controllers needed to create
per-workload Pod Identity bindings for managed S3 object stores.

- EnabledCrossplaneCapabilities expands s3 -> {iam, eks} internal deps
- add aws-eks / aws-iam provider + provider-config manifests and ArgoCD
  app templates, gated on the same opt-in capability set
- crossplane-iam.tf provisions one scoped Pod Identity role per provider
- register aws-iam / aws-eks in crossplaneCapabilities gating list
Verifying the ObjectStore Composition end-to-end (ADR-0012) surfaced four
issues in crossplane-iam.tf, all fixed here.

OpenTofu render (blocked any apply once S3 was enabled):
- Split the per-provider tag/deny statements from one mixed-shape 4-element
  tuple into four single-element conditionals so they unify with the empty
  branch.
- Replace crossplane_provider_boundary's {}/object ternary for_each with a
  filtered comprehension.

IAM scoping (blocked the IAM/EKS providers creating workload roles):
- iam:GetRole on a not-yet-created role authorizes against role/<name> with no
  path, so observe-before-create never matched the path-scoped ARN. Add a
  name-only read ARN; writes stay path-, boundary-, and tag-scoped.
- CreatePodIdentityAssociation validates the target role via iam:GetRole, but
  the EKS provider had only iam:PassRole. Add a read-only GetRole grant.
Show how an application pack (e.g. nebari-mlflow) consumes the platform
XObjectStore API: an XObjectStore claim naming a ServiceAccount, and a probe
pod that reads/writes the provisioned bucket via EKS Pod Identity (proving the
access is scoped to just that bucket). The XObjectStore XRD + Composition it
depends on are a platform capability, provided separately.
Persist the XObjectStore XRD + Composition + composition functions under
examples/crossplane/objectstore/platform as reference alongside the consumer
example. Values (account/region/cluster/prefix) are hardcoded to the phase-0
PoC and the Composition/README say so loudly.

Making this a portable, NIC-managed platform contract (consumed by packs like
nebari-mlflow across projects) needs EnvironmentConfig parameterization plus a
verbatim-copy path in the writer -- the Composition's {{ }} go-templating
collides with NIC's own text/template pass. Tracked as a follow-up; applied by
hand until then.
Replace the strict-separation IAM (per-capability provider-aws-s3/-iam/-eks
roles, individually scoped policies, per-provider permissions boundaries) with
the single broad account-local provider role ADR-0012 recommends. All provider
controllers share one role via Pod Identity; the role uses a broad Allow with
outer denies for IAM privilege-escalation containment (writes confined to the
workload-role path, boundary required on create, scoped iam:PassRole) and a
best-effort collision guard on the foundational EKS control plane. The workload
permissions boundary and provisioning-vs-runtime identity separation are kept.

Collapse the four ClusterProviderConfigs (aws-s3/iam/eks/rds) into a single
foundational `aws` config and one config Application; provider packages stay
gated per-capability. Update the composition providerConfigRefs, guardrail
tests, writer gating, and docs/comments accordingly.
# Conflicts:
#	pkg/providers/cluster/aws/templates/variables.tf
#	pkg/providers/cluster/aws/tofu.go
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants