Documentation
¶
Overview ¶
Package s3accesspoint implements a CAS backend that targets a single AWS S3 Access Point per tenant. Multiple tenants share one physical bucket; per-tenant isolation is provided by:
- The Access Point's resource policy, which gates who can address the AP and may further restrict s3:prefix.
- A per-request sts:AssumeRole that mints a scoped session whose RoleSessionName is derived from the authenticated requesting org. The AP's resource policy enforces a StringEquals on aws:userid so that a session minted for org A cannot read or write to org B's AP — even if org A's secret blob has been tampered with to point at org B's ARN.
- A per-tenant key prefix derived from the requesting org UUID: every object is keyed as <orgUUID>/sha256:<digest> and the AssumeRole session policy's Resource is scoped to ${apARN}/object/<orgUUID>/*. The prefix shares its source of truth with the session name, so a tampered secret cannot reroute a tenant's writes into a different namespace.
The session name MUST come from the request context, not from the secret blob: a secrets-store compromise alone must not let an attacker reroute uploads to another tenant's AP.
Index ¶
- Constants
- Variables
- type Backend
- func (b *Backend) CheckWritePermissions(ctx context.Context) error
- func (b *Backend) Describe(ctx context.Context, digest string) (*pb.CASResource, error)
- func (b *Backend) Download(ctx context.Context, w io.Writer, digest string) error
- func (b *Backend) Exists(ctx context.Context, digest string) (bool, error)
- func (b *Backend) Upload(ctx context.Context, r io.Reader, resource *pb.CASResource) error
- type BackendProvider
- type Credentials
Constants ¶
const DevModeEnvVar = "CHAINLOOP_S3_ACCESS_POINT_DEV_MODE"
DevModeEnvVar when set to a truthy value, short-circuits sts:AssumeRole and routes S3 calls through whatever ambient AWS identity the SDK's default credential chain produced (env vars, ~/.aws/credentials, instance profile, IRSA, …). The fail-closed check on a missing requesting-org context is still enforced.
DEV ONLY. This bypasses the per-tenant isolation guarantees that the AssumeRole + session-policy + AP-policy chain provides; objects addressed via this backend are limited only by whatever the developer's IAM identity allows. NEVER set this in a multi-tenant deployment.
const ProviderID = "AWS-S3-ACCESS-POINT"
ProviderID is the stable identifier used by the CASBackend table's enum and by every other place that needs to disambiguate this provider from the regular s3 one.
const SessionDuration = time.Hour
SessionDuration is the STS token lifetime. STS allows up to 12h; 1h keeps blast radius of a leaked token small while still giving the credential cache useful reuse across consecutive uploads.
Variables ¶
var ErrMissingRequestingOrg = errors.New("s3accesspoint: requesting org missing from claims")
ErrMissingRequestingOrg is returned when a request reaches the backend without an org UUID in its context. The backend fails closed in this case rather than minting a session with a default/empty name that would be useless against an AP policy condition.
Functions ¶
This section is empty.
Types ¶
type Backend ¶
type Backend struct {
// contains filtered or unexported fields
}
Backend is the per-tenant uploader/downloader. One *Backend instance is bound to one access point; the actual AWS credentials are minted per-request via STS using the org UUID found in the request context.
func NewBackend ¶
func NewBackend(ctx context.Context, creds *Credentials) (*Backend, error)
NewBackend constructs a *Backend wired to an STS-backed credentials provider. ctx is used only for the initial AWS config load (DNS lookups, IMDS, IRSA token reads); it is not retained for later operations.
func (*Backend) CheckWritePermissions ¶
CheckWritePermissions verifies that the calling org can actually mint a scoped session and put/get an object through its AP. Unlike the regular s3 backend's variant this MUST be invoked with a context carrying the org
type BackendProvider ¶
type BackendProvider struct {
// contains filtered or unexported fields
}
BackendProvider implements backend.Provider for the access-point-backed managed CAS. Construction takes only the credentials reader; everything the provider needs at request time lives in the per-tenant secret blob.
func NewBackendProvider ¶
func NewBackendProvider(cReader credentials.Reader) *BackendProvider
NewBackendProvider constructs the provider. A nil credentials reader is a programmer error and surfaces as a startup failure.
func (*BackendProvider) FromCredentials ¶
func (p *BackendProvider) FromCredentials(ctx context.Context, secretName string) (backend.UploaderDownloader, error)
FromCredentials reads the per-tenant Credentials blob from the secrets manager and constructs a *Backend bound to that tenant's AP.
The returned UploaderDownloader is safe to reuse across requests; each request must enrich its context with org claim so the STS-minted session name matches the AP's resource-policy condition.
func (*BackendProvider) ID ¶
func (p *BackendProvider) ID() string
func (*BackendProvider) ValidateAndExtractCredentials ¶
func (p *BackendProvider) ValidateAndExtractCredentials(location string, credsJSON []byte) (any, error)
ValidateAndExtractCredentials decodes credsJSON into a Credentials struct and optionally cross-checks it against the location passed by the caller. This is invoked when a managed row is being created or revalidated; the returned value is what gets persisted in the secrets manager by upstream callers.
Unlike the regular s3 provider, this does NOT exercise live S3 permissions during validation: the credentials by themselves can't be tested without a request-context org UUID, so a proper end-to-end check belongs in the upload path. PerformValidation in the controlplane still calls this method for managed rows; it will succeed as long as the blob is well-formed.
type Credentials ¶
type Credentials struct {
// AccessPointARN, e.g.
// arn:aws:s3:us-east-1:123456789012:accesspoint/chainloop-org-<uuid>
// The provider passes this string verbatim as the Bucket parameter on
// every S3 SDK call.
AccessPointARN string
// Region the AP lives in.
Region string
// BaseRoleARN is the IAM role assumed via STS to mint per-request,
// per-tenant scoped credentials. Stored per-tenant (not per-deployment)
// so a single chainloop install can serve tenants across multiple AWS
// accounts without a config change. Required unless DevModeEnvVar is
// set on the running binary.
BaseRoleARN string
}
Credentials is the per-tenant blob stashed in the secrets manager under CASBackend.SecretName. Despite the name it carries no access keys — only tenant-identifying coordinates used to construct a scoped S3 client.
The per-tenant key prefix is intentionally NOT a field here: it's derived at request time from the authenticated requesting org carried in ctx via org claim. Both the bucket-layer key namespace and the AssumeRole session-name binding therefore come from the same untamperable source, so a secrets-store compromise that rewrites this blob still can't reroute a tenant's writes into another tenant's namespace.
func (*Credentials) Validate ¶
func (c *Credentials) Validate() error