Skip to content

fix(catalog/rest): sign SigV4 with credentials from catalog properties - #1999

Merged
laskoviymishka merged 6 commits into
apache:mainfrom
iremcaginyurtturk:fix/rest-sigv4-static-creds
Oct 1, 2026
Merged

laskoviymishka merged 6 commits into
apache:mainfrom
iremcaginyurtturk:fix/rest-sigv4-static-creds

Conversation

@iremcaginyurtturk

Copy link
Copy Markdown
Contributor

Problem

The REST catalog's SigV4 signer builds its aws.Config only from config.LoadDefaultConfig (the AWS default credential chain), so the s3.* credential properties passed to the catalog are ignored for request signing.

As a result, connecting to an AWS SigV4 REST catalog (S3 Tables at s3tables.<region>.amazonaws.com/iceberg, or the Glue Iceberg REST endpoint) fails with no EC2 IMDS role found unless AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY/AWS_SESSION_TOKEN are also present in the environment — even when the caller already supplied those credentials via s3.access-key-id / s3.secret-access-key / s3.session-token.

This is the same gap tracked for the Python client in apache/iceberg-python#2070.

Fix

In createSession, when no explicit aws.Config was provided (WithAwsConfig), build a static credentials provider from the s3.* credential properties when a key pair is present, and fall back to the default chain otherwise. An explicitly provided aws.Config still takes precedence.

if creds, ok := staticCredsFromProps(opts.additionalProps); ok {
    cfg.Credentials = creds
}

Testing

  • New unit test TestStaticCredsFromProps (full key pair → provider with session token; lone access key → no provider; empty → no provider).
  • Verified end-to-end against a live AWS S3 Tables bucket: create/replace/merge succeed with credentials supplied only via catalog properties and no AWS_* environment variables.
  • go test ./catalog/rest/ passes.

The REST catalog's SigV4 signer only used the AWS default credential chain
(config.LoadDefaultConfig), ignoring the s3.* credential properties passed to
the catalog. This forced callers targeting AWS SigV4 REST catalogs (S3 Tables,
Glue) to export AWS_* environment variables even when they supplied credentials
via catalog properties.

Build a static credentials provider from s3.access-key-id / s3.secret-access-key
/ s3.session-token when present, and only fall back to the default chain
otherwise. An explicitly provided aws.Config (WithAwsConfig) still wins.

This mirrors the same gap tracked for the Python client in
apache/iceberg-python#2070.

Signed-off-by: iremcaginyurtturk <cagin.yurtturk@getbruin.com>

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SigV4-from-properties fix genuinely works and does not silently downgrade to unsigned, but the wiring line is pinned by no test (package stays green when it is deleted) and a partial credential pair silently signs as the ambient identity instead of erroring.

Re-review verification: 0 of 1 prior findings confirmed fixed at ffa55bb (each verified by mutating the fix and observing the suite go red, not by taking the claim on trust).

Verification performed
go build ./catalog/... (OK); go vet ./catalog/rest/ (clean); go test ./catalog/rest/ -timeout=300s -> ok 5.908s; go test ./catalog/rest/ -run TestStaticCredsFromProps -v -> PASS. Mutation 1: neutralized `cfg.Credentials = creds` -> full package STILL ok 5.946s (fix unpinned). Mutation 2: swapped access/secret args in NewStaticCredentialsProvider -> TestStaticCredsFromProps FAILS (helper test is non-vacuous). Throwaway probe catalog/rest/pr1999_probe_test.go (5 tests: props-reach-signer, no-creds-anywhere, partial-creds, precedence, server-override) all ran and were then deleted. Final: git status --porcelain empty, go vet ./catalog/hive/ clean.

This review was drafted by an AI-assisted tool and confirmed by an Apache Iceberg Go maintainer. After you've addressed the points above and pushed an update, an Apache Iceberg Go maintainer — a real person — will take the next look at the PR. The findings cite the project's review criteria; if you think one of them is mis-applied, please reply on the PR and a maintainer will weigh in.

More on how Apache Iceberg Go handles maintainer review: CONTRIBUTING.md.

Comment thread catalog/rest/rest.go
Comment thread catalog/rest/rest.go Outdated
Comment thread catalog/rest/rest.go
Comment thread catalog/rest/rest.go
Address review feedback:
- Add TestSigV4SignsWithPropsCredentials, which asserts the SigV4 Authorization
  header is signed with the credentials from catalog properties. This pins the
  wiring: removing the cfg.Credentials assignment makes the test fail.
- staticCredsFromProps now returns an error when only one of the access-key /
  secret-access-key pair is set, instead of silently falling back to the ambient
  default identity. Neither set still falls back to the default chain.

Signed-off-by: iremcaginyurtturk <cagin.yurtturk@getbruin.com>
@iremcaginyurtturk

Copy link
Copy Markdown
Contributor Author

Thanks for the review — both points addressed in cca7c83:

  1. Wiring now pinned by a test. Added TestSigV4SignsWithPropsCredentials, which drives a request through a test server and asserts the SigV4 Authorization header contains Credential=<props access key>/. Neutralizing the cfg.Credentials = creds assignment makes the test fail (falls back to the default chain → no EC2 IMDS role found), so the wiring is no longer deletable while green.

  2. Partial credentials now error. staticCredsFromProps returns an error when only one of s3.access-key-id / s3.secret-access-key is set, instead of silently signing as the ambient identity. Neither set still returns (nil, nil) so the default chain is used (e.g. instance role); both set builds the static provider. Covered by the updated TestStaticCredsFromProps.

Address review feedback (minor): the SigV4 signing identity resolves as
WithAwsConfig > s3.* catalog properties > AWS default credential chain. Document
this on WithSigV4 and in the rest.sigv4-enabled configuration reference.

Signed-off-by: iremcaginyurtturk <cagin.yurtturk@getbruin.com>
@iremcaginyurtturk

Copy link
Copy Markdown
Contributor Author

All four review findings addressed:

1. (major) Fix pinned by no test — cca7c83 adds TestSigV4SignsWithPropsCredentials: it stands up an httptest server, passes creds only via WithAdditionalProps, and asserts the captured Authorization header contains the props access key (Credential=AKIDEXAMPLEPROPS/). Verified by mutation — deleting cfg.Credentials = creds makes it fail (no EC2 IMDS role found).

2. (major) Partial s3.* pair signs as a different principal — cca7c83: staticCredsFromProps now returns an error when exactly one of s3.access-key-id / s3.secret-access-key is set, naming the required properties. Fully-empty still falls through to the default chain. TestStaticCredsFromProps updated to assert the error cases instead of enshrining the old behavior.

3. (minor) Undocumented precedence — 48164f5: documented the WithAwsConfig > s3.* props > default-chain order on WithSigV4 (godoc) and in the rest.sigv4-enabled row of website/src/configuration.md.

4. (minor) Server /v1/config override can select the signing identity — confirming intent: this is acceptable. The REST control plane is a trusted input per SECURITY-THREAT-MODEL.md and no secret reaches a new audience (the server already knows any creds it injected). If you'd prefer the signing identity be pinned to client-supplied properties regardless of server overrides, I'm happy to capture the client creds before fetchConfig folds in the overrides — just let me know.

Full catalog/rest suite passes; go vet clean.

@laskoviymishka laskoviymishka left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, this is basically there. The props-credential path works, and the two things flagged last round are both handled now:

  • TestSigV4SignsWithPropsCredentials pins the wiring end to end. It asserts the Authorization header carries Credential=AKIDEXAMPLEPROPS/, which only holds when the props creds are actually signing rather than the default chain.
  • staticCredsFromProps now errors on a lone access-key or secret-key instead of silently signing as a different identity, and both cases are covered.

The one thing I'd still fix is the token-only case: a lone s3.session-token with no access/secret falls into the empty-pair branch and returns (nil, nil), so it silently drops to the default chain. We already have internal/awsconfig.ValidateStaticCredentials for exactly this check. It's what io/gocloud/s3 and catalog/glue use, and it returns the ErrIncompleteStaticCredentials sentinel, so delegating to it closes the gap and makes the error errors.Is-detectable in one move instead of a bare fmt.Errorf. Worth a token-only test case too.

Not blocking, but while we're here: sourcing the signing creds from s3.* diverges from Java, which uses rest.access-key-id / rest.secret-access-key. You've documented s3.* as intentional so I'm fine with it, but an operator coming from the Java client configuring rest.* will silently fall through to the default chain. Might be worth a line in the godoc calling that out, or accepting rest.* as an alias down the line. wdyt?

The token-only fix is the one I'd still like to see; everything else is optional. This is close.

Comment thread catalog/rest/rest.go
Comment thread catalog/rest/options.go
Comment thread catalog/rest/rest_internal_test.go
…Credentials

Delegate staticCredsFromProps to internal/awsconfig.ValidateStaticCredentials so
a token-only s3.session-token (no access/secret key) returns the
ErrIncompleteStaticCredentials sentinel instead of silently dropping to the
default credential chain. Add a token-only test case and assert the sentinel via
errors.Is. Document the s3.* vs Java rest.* divergence in the WithSigV4 godoc and
configuration.md.
Accept rest.access-key-id / rest.secret-access-key / rest.session-token as
aliases for the s3.* signing-credential properties, resolved per field with the
s3.* keys taking precedence. This lets operators migrating from the Java client
configure SigV4 signing with the property names they already use instead of
silently falling through to the AWS default credential chain.

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST_CHANGES. The 09-08 wiring and documentation feedback is addressed: TestSigV4SignsWithPropsCredentials pins the fix end to end, the lone-access/lone-secret/token-only cases now return ErrIncompleteStaticCredentials, and precedence is documented — WithAwsConfig wins, complete property credentials override the default chain, and an absent tuple preserves the ambient chain. The identity cutover for users who already set s3.* while relying on ambient catalog credentials is explicit in both the godoc and the configuration reference, which is what I asked for. Server /v1/config overrides selecting the post-bootstrap identity stays within the trusted-control-plane model.

Two credential-boundary problems remain; details inline. The first is in scope under SECURITY-THREAT-MODEL.md: "a secret or delegated credential reaching a new audience" is an explicit reassess trigger that overrides the trusted-control-plane downgrade.

Non-blocking: catalog/rest/rest_internal_test.go:138 discards the response without closing the body.

Comment thread catalog/rest/rest.go Outdated
Comment thread catalog/rest/rest.go
Two credential-boundary fixes for the props-based SigV4 signing path:

- Resolve the credential tuple atomically per namespace. Resolving each field
  independently let a partial s3.* pair be completed with a rest.* field (or
  inherit an unrelated rest.* token), signing as a hybrid identity that still
  passed validation. Now if any s3.* field is set we validate and use only the
  s3.* tuple, otherwise only the rest.* tuple; never backfill across namespaces.

- Do not re-sign a cross-origin redirect. sessionTransport signed every hop, so
  a redirect to a different origin exposed the SigV4 Authorization header and
  session token to an unconfigured host. Signing is now skipped when the request
  origin (scheme/host/effective port) differs from the configured catalog origin.

Tests cover the mixed-partial and s3-pair-plus-rest-token cases and assert a
cross-origin redirect target receives neither the Authorization header nor the
session token. Also closes a response body left open in an existing test.

Signed-off-by: iremcaginyurtturk <cagin.yurtturk@getbruin.com>

@zeroshade zeroshade left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at d401049. Both blockers from my 09-21 review are fixed:

  • staticCredsFromProps (rest.go:1180-1198) now takes one namespace whole and never fills a missing field from the other. The mixed-partial and s3-pair + rest-token cases are covered.
  • Cross-origin redirect hops are no longer signed (sameOrigin guard, rest.go:327). I removed the guard locally and TestSigv4DoesNotSignCrossOriginRedirect failed with the second server receiving Credential=AKIDEXAMPLEPROPS/..., so the test pins it.

The response-body close and @laskoviymishka's token-only / ValidateStaticCredentials / rest.* alias points are in too. What remains is inline and non-blocking: credential precedence vs Java/pyiceberg, stale "resolved per field" wording, optional test style.

Merge order vs #2060: this lands first. #2060 removes the inline SigV4 path from createSession, so it will need to carry the property credentials through SignerConfig into catalog/rest/sigv4, keep the signingOrigin check in core RoundTrip around SignRequest, and port these tests.

Comment thread catalog/rest/rest.go
Comment on lines +1181 to +1184
namespaces := [][3]string{
{iceio.S3AccessKeyID, iceio.S3SecretAccessKey, iceio.S3SessionToken},
{keyRestAccessKeyID, keyRestSecretAccessKey, keyRestSessionToken},
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking, but worth settling before this ships because it is user-visible. With both namespaces set, s3.* signs and rest.* is ignored (pinned by the "s3.* keys take precedence over rest.* aliases" case in the test).

Java's AwsProperties.restCredentialsProvider() signs only with rest.access-key-id / rest.secret-access-key / rest.session-token, then client.credentials-provider, then the default chain. It never reads s3.*. pyiceberg signs with client.access-key-id etc. So if someone sets s3.* for FileIO (MinIO, or a scoped data principal) and rest.* for the catalog, catalog requests get signed with the data credentials. The same happens if a server returns s3.* in /v1/config defaults/overrides, since those end up in additionalProps.

I'd flip this to rest.* > s3.* > default chain: swap these two entries and invert the precedence assertion in TestStaticCredsFromProps. This builds on @laskoviymishka's Java-parity note. Fine here if you prefer, otherwise a follow-up.

Comment thread catalog/rest/options.go
Comment on lines +96 to +98
// The Java-client property names (rest.access-key-id / rest.secret-access-key /
// rest.session-token) are accepted as aliases, resolved per field with the s3.*
// keys taking precedence when both are set.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is stale after d401049: resolution is no longer per field. staticCredsFromProps uses the s3.* tuple if any s3.* key is set (and errors if that tuple is incomplete), otherwise the rest.* tuple. Fields never mix across namespaces. The same wording is in the const comment at rest.go:97-99 and in configuration.md:59.

Suggested text: "If any s3.* credential key is set, only the s3.* tuple is used and it must be complete; otherwise the rest.* tuple is used."

| `catalog.<name>.sql-driver` | `database/sql` driver name for the SQL catalog. Maps to the `sql.driver` property. The default CLI binary only compiles in `sqliteshim`; other drivers require a custom build. |
| `catalog.<name>.sql-dialect` | SQL dialect for the SQL catalog (`postgres`, `mysql`, `sqlite`, `mssql`, `oracle`). Maps to the `sql.dialect` property. The default CLI binary only ships `sqlite` via `sqliteshim`; other dialects need a custom build with their drivers. |
| `catalog.<name>.rest.sigv4-enabled` | Enable AWS SigV4 signing for REST. |
| `catalog.<name>.rest.sigv4-enabled` | Enable AWS SigV4 signing for REST. When enabled, requests are signed with the `s3.*` credential properties if set (`s3.access-key-id` / `s3.secret-access-key` / `s3.session-token`), otherwise with the AWS default credential chain. The Java-client names (`rest.access-key-id` / `rest.secret-access-key` / `rest.session-token`) are accepted as aliases, resolved per field with the `s3.*` keys taking precedence. |

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apart from the stale "resolved per field" wording (see options.go), this row is in the CLI YAML table, and the CLI config cannot set s3.* or rest.* credentials. config.RestOptions only has sigv4-enabled / signing-name / signing-region, and cmd/iceberg/main.go:392-398 only maps those. I'd keep this row as "Enable AWS SigV4 signing for REST." and move the credential-resolution text to the REST catalog options section (the AWS SigV4 row at line 74), where WithAdditionalProps can actually supply these keys.

"golang.org/x/sync/errgroup"
)

func TestStaticCredsFromProps(t *testing.T) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional, as @laskoviymishka noted earlier: this reads better as a table-driven test with t.Parallel(), which matches the rest of the file. Not blocking.

@laskoviymishka
laskoviymishka merged commit f7e2667 into apache:main Oct 1, 2026
15 checks passed
laskoviymishka added a commit to laskoviymishka/iceberg-go that referenced this pull request Oct 2, 2026
Carry the property-credential signing from apache#1999 into the optional sigv4
backend, now that catalog/rest no longer links the AWS SDK.

SignerConfig gains a Props field holding the post-override catalog properties,
and resolveSigner passes opts.additionalProps through it. staticCredsFromProps
and the rest.* alias keys move into catalog/rest/sigv4, where the registered
signer factory builds a static credentials provider from the s3.* / rest.*
properties and falls back to the AWS default chain. An explicit
sigv4.WithAwsConfig fully specifies the credentials and never consults the
properties.

The cross-origin signing guard stays in core RoundTrip: it is signer-agnostic,
so it now also covers a signer installed verbatim via WithSigner. Tests move
with the code - the staticCredsFromProps table and the props-credential
end-to-end test live in the sigv4 package, while the cross-origin redirect test
stays in core and drives the guard through a marking signer.
laskoviymishka added a commit that referenced this pull request Oct 2, 2026
* feat(rest): make SigV4 backend optional

Move AWS SigV4 signing out of `catalog/rest` into `catalog/rest/sigv4`, so plain REST users no longer pull in the AWS SDK.

Breaking change: `rest.WithAwsConfig` is removed. Use `sigv4.WithAwsConfig(...)` and add a blank import of `catalog/rest/sigv4` to enable SigV4 signing.

Follows the driver pattern established for cloud IO in #696.

* fix(rest): resolve SigV4 signing scope through a signer factory

Addresses review feedback on #2060.

* Update website/src/configuration.md

Co-authored-by: Matt Topol <zotthewizard@gmail.com>

* feat(rest): sign SigV4 with catalog-property credentials via the backend

Carry the property-credential signing from #1999 into the optional sigv4
backend, now that catalog/rest no longer links the AWS SDK.

SignerConfig gains a Props field holding the post-override catalog properties,
and resolveSigner passes opts.additionalProps through it. staticCredsFromProps
and the rest.* alias keys move into catalog/rest/sigv4, where the registered
signer factory builds a static credentials provider from the s3.* / rest.*
properties and falls back to the AWS default chain. An explicit
sigv4.WithAwsConfig fully specifies the credentials and never consults the
properties.

The cross-origin signing guard stays in core RoundTrip: it is signer-agnostic,
so it now also covers a signer installed verbatim via WithSigner. Tests move
with the code - the staticCredsFromProps table and the props-credential
end-to-end test live in the sigv4 package, while the cross-origin redirect test
stays in core and drives the guard through a marking signer.

---------

Co-authored-by: Matt Topol <zotthewizard@gmail.com>
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.

3 participants