Files
seaweedfs/weed/credential
Chris Lu e77f8ae204 fix(s3api): route STS GetFederationToken to STS handler (#9157) (#9167)
* fix(s3api): route STS GetFederationToken requests to STS handler (#9157)

The STS GetFederationToken handler was implemented but never reachable.
Three routing gaps sent requests to the S3/IAM path instead of STS:

- No explicit mux route for Action=GetFederationToken in the URL query
- iamMatcher did not exclude GetFederationToken, so authenticated POSTs
  with Action in the form body were matched and dispatched to IAM
- UnifiedPostHandler only dispatched AssumeRole* and GetCallerIdentity
  to STS, leaving GetFederationToken to fall through to DoActions and
  return NotImplemented

Add the missing route, the matcher exclusion, and the dispatch branch.

Also wire TestSTS, TestAssumeRoleWithWebIdentity, and TestServiceAccount
into the s3-iam-tests workflow as a new "sts" matrix entry. Before this
change, none of test/s3/iam/s3_sts_get_federation_token_test.go's four
test functions ran in CI, which is why this regression shipped.

* test(iam): make orphaned STS/service-account tests pass under auth-enabled CI

Follow-up to wiring STS tests into CI: fixes several pre-existing issues
that made the newly-included tests fail locally.

Server fixes:
- weed/s3api/s3api_sts.go: handleGetFederationToken no longer 500s when
  the caller is a legacy S3-config identity (not in the IAM user store).
  Previously any GetPoliciesForUser error short-circuited to InternalError,
  which hard-failed every SigV4 caller using keys from -s3.config.
- weed/s3api/s3api_embedded_iam.go: CreateServiceAccount now generates IDs
  in the sa:<parent>:<uuid> format required by
  credential.ValidateServiceAccountId. The old "sa-XXXXXXXX" format failed
  the persistence-layer regex and caused every CreateServiceAccount call
  to return 500 once a filer-backed credential store validated the ID.

Test helpers:
- test/s3/iam/s3_sts_assume_role_test.go: callSTSAPIWithSigV4 no longer
  sets req.Header["Host"]. aws-sdk-go v1 v4.Signer already signs Host from
  req.URL.Host, and a manual Host header made the signer emit host;host in
  SignedHeaders, producing SignatureDoesNotMatch. Updated missing_role_arn
  subtest to match the existing SeaweedFS behavior (user-context
  assumption).
- test/s3/iam/s3_service_account_test.go: callIAMAPI now SigV4-signs
  requests when STS_TEST_{ACCESS,SECRET}_KEY env vars are set. Unsigned
  IAM writes otherwise fall through to the STS fallback and return
  InvalidAction.

CI matrix:
- .github/workflows/s3-iam-tests.yml: skip
  TestServiceAccountLifecycle/use_service_account_credentials only. The
  rest of the service-account suite passes; that one subtest depends on a
  separate credential-reload issue where new ABIA keys briefly register
  into accessKeyIdent but aren't persisted to the filer, so they vanish
  on the next reload. Out of scope for the #9157 GetFederationToken fix.

* fix(credential): accept AWS IAM username chars in service-account IDs

Gemini review on #9167 pointed out that ServiceAccountIdPattern's
parent-user segment was more restrictive than an AWS IAM username:
`[A-Za-z0-9_-]` vs. IAM's `[\w+=,.@-]`. Realistic usernames with
`@`, `.`, `+`, `=`, or `,` (e.g. email-style principals) would fail
validation at the filer store even though the embedded IAM API
happily created them.

Broaden the regex to `[A-Za-z0-9_+=,.@-]` (matching the AWS IAM spec
at https://docs.aws.amazon.com/IAM/latest/APIReference/API_User.html)
and add a table-driven test that locks the expansion in.

* address PR review feedback on #9167

All five review items were valid; changes keyed to review bullets:

- weed/s3api/s3api_sts.go: handleGetFederationToken no longer swallows
  arbitrary policy-lookup failures. Only credential.ErrUserNotFound is
  treated leniently (the legacy-config SigV4 path); any other error now
  returns InternalError so we don't mint tokens with an incomplete
  policy set.
- weed/credential/grpc/grpc_identity.go: GetUser translates gRPC
  NotFound back to credential.ErrUserNotFound so errors.Is(...) above
  matches for gRPC-backed stores, not just memory/filer-direct.
- weed/s3api/s3api_embedded_iam.go: CreateServiceAccount now validates
  the generated saId against credential.ValidateServiceAccountId before
  returning. Surfaces a client 400 with the offending ID instead of the
  opaque 500 that used to bubble up from the persistence layer.
- weed/s3api/s3api_server_routing_test.go: seed a routing-test identity
  with a known AK/SK, sign TestRouting_GetFederationTokenAuthenticatedBody
  with aws-sdk-go v4.Signer so the request actually passes
  AuthSignatureOnly. Assert 503 ServiceUnavailable (from STSHandlers
  with no stsService) instead of just NotEqual(501) — 503 proves the
  dispatch reached STSHandlers.HandleSTSRequest.
- test/s3/iam/s3_service_account_test.go: callIAMAPI signs with
  service="iam" instead of "s3" (SeaweedFS verifies against whichever
  service the client signed with, but "iam" is semantically correct).
- weed/credential/validation_test.go: add positive rows for an
  uppercase parent (sa:ALICE:...) and a canonical hyphenated UUID
  suffix (sa:alice:123e4567-e89b-12d3-a456-426614174000).
2026-04-20 19:33:22 -07:00
..

Credential Store Integration

This document shows how the credential store has been integrated into SeaweedFS's S3 API and IAM API components.

Quick Start

  1. Generate credential configuration:

    weed scaffold -config=credential -output=.
    
  2. Edit credential.toml to enable your preferred store (filer_etc is enabled by default)

  3. Start S3 API server - it will automatically load credential.toml:

    weed s3 -filer=localhost:8888
    

Integration Overview

The credential store provides a pluggable backend for storing S3 identities and credentials, supporting:

  • Filer-based storage (filer_etc) - Uses existing filer storage (default)
  • PostgreSQL - Shared database for multiple servers
  • Memory - In-memory storage for testing

Configuration

Using credential.toml

Generate the configuration template:

weed scaffold -config=credential

This creates a credential.toml file with all available options. The filer_etc store is enabled by default:

# Filer-based credential store (default, uses existing filer storage)
[credential.filer_etc]
enabled = true


# PostgreSQL credential store (recommended for multi-node deployments)
[credential.postgres]
enabled = false
hostname = "localhost"
port = 5432
username = "seaweedfs"
password = "your_password"
database = "seaweedfs"

# Memory credential store (for testing only, data is lost on restart)
[credential.memory]
enabled = false

The credential.toml file is automatically loaded from these locations (in priority order):

  • ./credential.toml
  • $HOME/.seaweedfs/credential.toml
  • /etc/seaweedfs/credential.toml

Server Configuration

Both S3 API and IAM API servers automatically load credential.toml during startup. No additional configuration is required.

Usage Examples

Filer-based Store (Default)

[credential.filer_etc]
enabled = true

This uses the existing filer storage and is compatible with current deployments.

PostgreSQL Store

[credential.postgres]
enabled = true
hostname = "localhost"
port = 5432
username = "seaweedfs"
password = "your_password"
database = "seaweedfs"
schema = "public"
sslmode = "disable"
table_prefix = "sw_"
connection_max_idle = 10
connection_max_open = 100
connection_max_lifetime_seconds = 3600

Memory Store (Testing)

[credential.memory]
enabled = true

Environment Variables

All credential configuration can be overridden with environment variables:

# Override PostgreSQL password
export WEED_CREDENTIAL_POSTGRES_PASSWORD=secret


# Override PostgreSQL hostname
export WEED_CREDENTIAL_POSTGRES_HOSTNAME=db.example.com

# Enable/disable stores
export WEED_CREDENTIAL_FILER_ETC_ENABLED=true

Rules:

  • Prefix with WEED_CREDENTIAL_
  • Convert to uppercase
  • Replace . with _

Implementation Details

Components automatically load credential configuration during startup:

// Server initialization
if credConfig, err := credential.LoadCredentialConfiguration(); err == nil && credConfig != nil {
    credentialManager, err := credential.NewCredentialManager(
        credConfig.Store,
        credConfig.Config,
        credConfig.Prefix,
    )
    if err != nil {
        return nil, fmt.Errorf("failed to initialize credential manager: %v", err)
    }
    // Use credential manager for operations
}

Benefits

  1. Easy Configuration - Generate template with weed scaffold -config=credential
  2. Pluggable Storage - Switch between filer_etc, PostgreSQL without code changes
  3. Backward Compatibility - Filer-based storage works with existing deployments
  4. Scalability - Database stores support multiple concurrent servers
  5. Performance - Database access can be faster than file-based storage
  6. Testing - Memory store simplifies unit testing
  7. Environment Override - All settings can be overridden with environment variables

Error Handling

When a credential store is configured, it must initialize successfully or the server will fail to start:

if credConfig != nil {
    credentialManager, err = credential.NewCredentialManager(...)
    if err != nil {
        return nil, fmt.Errorf("failed to initialize credential manager: %v", err)
    }
}

This ensures explicit configuration - if you configure a credential store, it must work properly.