Add helpful error message for S3 403 Forbidden in cache extractor
What does this MR do?
When using S3 with RoleARN, if the file doesn't exist, the Attributes call returns a 403 Forbidden error instead of 404 Not Found. This can be confusing because it may indicate either a missing file or incorrect IAM permissions.
Add special handling for S3 403 errors to provide guidance that the error may be expected if the file doesn't exist, and reference the documentation about HeadObject permissions and the s3:ListBucket IAM policy requirement.
This helps users distinguish between a missing cache file and actual permission issues with their RoleARN configuration.
For now we use a string comparison because GoCloud returns an Unknown code until https://github.com/google/go-cloud/pull/3663 is merged.
Why was this MR needed?
A number of customers were confused by 403 Forbidden errors
What's the best way to test this MR?
Configure a RoleARN and run a CI job that uses a cache for the first time.
Alternatively you can:
- Create a
policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "*"
}
]
}
- Run this script:
#!/usr/bin/env bash
# Usage: source ./assume_role.sh <ROLE_ARN>
# Must be sourced (not executed) for exports to persist in your current shell.
set -euo pipefail
ROLE_ARN="${1:?Usage: source ./assume_role.sh <ROLE_ARN>}"
SESSION_NAME="${2:-MyRestrictedSession}"
POLICY_FILE="${3:-policy.json}"
if [[ ! -f "$POLICY_FILE" ]]; then
echo "Error: Policy file '$POLICY_FILE' not found." >&2
return 1 2>/dev/null || exit 1
fi
echo "Assuming role: $ROLE_ARN"
CREDENTIALS=$(aws sts assume-role \
--role-arn "$ROLE_ARN" \
--role-session-name "$SESSION_NAME" \
--policy file://"$POLICY_FILE" \
--query "Credentials" \
--output json)
export AWS_ACCESS_KEY_ID
export AWS_SECRET_ACCESS_KEY
export AWS_SESSION_TOKEN
AWS_ACCESS_KEY_ID=$(echo "$CREDENTIALS" | jq -r '.AccessKeyId')
AWS_SECRET_ACCESS_KEY=$(echo "$CREDENTIALS" | jq -r '.SecretAccessKey')
AWS_SESSION_TOKEN=$(echo "$CREDENTIALS" | jq -r '.SessionToken')
EXPIRATION=$(echo "$CREDENTIALS" | jq -r '.Expiration')
echo "✅ Credentials exported successfully."
echo " Session expires: $EXPIRATION"
echo ""
echo " AWS_ACCESS_KEY_ID = ${AWS_ACCESS_KEY_ID:0:4}... (truncated)"
echo " AWS_SECRET_ACCESS_KEY = ******* (hidden)"
echo " AWS_SESSION_TOKEN = ******* (hidden)"
- Rebuild the helper:
rm out/binaries/gitlab-runner-helper/gitlab-runner-helper.linux-amd64; make out/binaries/gitlab-runner-helper/gitlab-runner-helper.linux-amd64
- Create some ZIP file and try to run the helper:
zip test.zip README.md
./out/binaries/gitlab-runner-helper/gitlab-runner-helper.linux-amd64 cache-extractor --file test.zip --gocloud-url "s3://<YOUR BUCKET>/random-non-existent-file.txt?region=us-west-2"
You should see something like:
WARNING: blob (key "test222.txt") (code=Unknown): operation error S3: HeadObject, https response error StatusCode: 403, RequestID: ED2QEERNPA1AKTR6, HostID: suPZ52iMQwitlQfuCkI4XRb5k2DKnaEge5BFgvNHpF0gNoIwMMGUFG9ha8XXE3Lyig2vtATrIxk=, api error Forbidden: Forbidden: This 403 is expected if the file doesn't exist. See the behavior of HeadObject without s3::ListBucket permissions (https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html).
What are the relevant issue numbers?
Relates to #39105 (closed)