s3: audit the assumed-role principal and the STS caller (#10519)

* s3: log the requester's principal ARN in the audit entry

An STS session authenticates as an opaque session subject, so requester
alone gave an operator no way back to the assumed role or the session
name. Record the principal ARN next to the identity name and emit it as
requester_arn.

* s3: record the caller identity in the STS handlers

AssumeRole, GetFederationToken and GetCallerIdentity verify the caller
themselves and are not wrapped by the auth middleware that records the
identity, so every audit entry for minting a session had an empty
requester.

* s3: resolve the audit principal ARN the way policy evaluation does

A JWT-authenticated identity carries no PrincipalArn — the auth layer
hands the principal over in a request header — so reading the field
directly left requester_arn empty for OIDC callers. buildPrincipalARN is
the resolver the policy path already uses: header first, then the
identity's own ARN, then a synthesized user ARN for legacy identities
that have none.
This commit is contained in:
Chris Lu authored and GitHub committed 2026-07-31 19:51:03 -07:00
1 parent fa432f9a6a
commit 1ce106e69d
9 files changed
+220 -27

No files matched your search

+9 -7
View File
@@ -23,13 +23,14 @@ type AccessLogExtend struct {
}
type AccessLog struct {
Bucket string `msg:"bucket" json:"bucket"` // awsexamplebucket1
Time int64 `msg:"time" json:"time"` // [06/Feb/2019:00:00:38 +0000]
RemoteIP string `msg:"remote_ip" json:"remote_ip,omitempty"` // 192.0.2.3
Requester string `msg:"requester" json:"requester,omitempty"` // IAM user id
RequestID string `msg:"request_id" json:"request_id,omitempty"` // 3E57427F33A59F07
Operation string `msg:"operation" json:"operation,omitempty"` // REST.HTTP_method.resource_type REST.PUT.OBJECT
Key string `msg:"key" json:"key,omitempty"` // /photos/2019/08/puppy.jpg
Bucket string `msg:"bucket" json:"bucket"` // awsexamplebucket1
Time int64 `msg:"time" json:"time"` // [06/Feb/2019:00:00:38 +0000]
RemoteIP string `msg:"remote_ip" json:"remote_ip,omitempty"` // 192.0.2.3
Requester string `msg:"requester" json:"requester,omitempty"` // IAM user id
RequesterArn string `msg:"requester_arn" json:"requester_arn,omitempty"` // arn:aws:sts::0:assumed-role/Role/session
RequestID string `msg:"request_id" json:"request_id,omitempty"` // 3E57427F33A59F07
Operation string `msg:"operation" json:"operation,omitempty"` // REST.HTTP_method.resource_type REST.PUT.OBJECT
Key string `msg:"key" json:"key,omitempty"` // /photos/2019/08/puppy.jpg
ErrorCode string `msg:"error_code" json:"error_code,omitempty"`
HostId string `msg:"host_id" json:"host_id,omitempty"`
HostHeader string `msg:"host_header" json:"host_header,omitempty"` // s3.us-west-2.amazonaws.com
@@ -170,6 +171,7 @@ func GetAccessLog(r *http.Request, HTTPStatusCode int, s3errCode ErrorCode) *Acc
RequestID: request_id.GetFromRequest(r),
RemoteIP: remoteIP,
Requester: s3_constants.GetIdentityNameFromContext(r), // Get from context, not header (secure)
RequesterArn: s3_constants.GetPrincipalArnFromContext(r),
SignatureVersion: r.Header.Get(s3_constants.AmzAuthType),
UserAgent: r.Header.Get("user-agent"),
HostId: hostname,
+17
View File
@@ -112,6 +112,23 @@ func TestGetAccessLogRequesterAnonymous(t *testing.T) {
log := GetAccessLog(req, http.StatusOK, ErrNone)
assert.Empty(t, log.Requester, "anonymous request must not report a requester")
assert.Empty(t, log.RequesterArn, "anonymous request must not report a principal ARN")
}
// An STS session's identity name is an opaque session subject, so the audit
// entry must also carry the principal ARN — that is where the assumed role and
// the role session name are recoverable from.
func TestGetAccessLogRequesterArnForAssumedRole(t *testing.T) {
outer := s3_constants.EnsureIdentityHolder(httptest.NewRequest(http.MethodGet, "/bucket/object", nil))
// Auth writes into the holder from a request copy the audit path never sees.
ctx := s3_constants.SetIdentityNameInContext(outer.Context(), "47ad4828c45b3f337bc3146081ba8f0f")
s3_constants.SetPrincipalArnInContext(ctx, "arn:aws:sts::000000000000:assumed-role/ClientRole/dev-session")
log := GetAccessLog(outer, http.StatusOK, ErrNone)
assert.Equal(t, "47ad4828c45b3f337bc3146081ba8f0f", log.Requester)
assert.Equal(t, "arn:aws:sts::000000000000:assumed-role/ClientRole/dev-session", log.RequesterArn)
}
func TestAuditTrackingFlag(t *testing.T) {