From 03f9430df61516ea53675e54601b6bcce9c0932d Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Thu, 15 Jan 2026 17:29:13 -0800 Subject: [PATCH] Add S3 Policy Variables documentation Document policy variables feature including: - AWS context variables (username, userid, principaltype, PrincipalAccount) - JWT claim variables - LDAP claim variables - Principal ARN parsing behavior - Clarify that IAM Roles do NOT have aws:userid or aws:PrincipalAccount - Usage examples and best practices --- S3-Policy-Variables.md | 379 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 S3-Policy-Variables.md diff --git a/S3-Policy-Variables.md b/S3-Policy-Variables.md new file mode 100644 index 0000000..e39dcf9 --- /dev/null +++ b/S3-Policy-Variables.md @@ -0,0 +1,379 @@ +# S3 Policy Variables + +SeaweedFS supports AWS S3 policy variables, allowing you to create dynamic, flexible bucket policies that adapt based on the requester's identity and context. + +## Overview + +Policy variables enable you to write a single policy that applies to multiple users by substituting values at request time. Instead of creating separate policies for each user, you can use variables like `${aws:username}` that get replaced with the actual username during policy evaluation. + +## Supported Variable Types + +### AWS Context Variables + +These variables are automatically extracted from the principal ARN: + +| Variable | Description | Available For | +|----------|-------------|---------------| +| `${aws:username}` | Username or role name | IAM Users, IAM Roles, Assumed Roles | +| `${aws:userid}` | User ID | IAM Users, Assumed Roles only | +| `${aws:principaltype}` | Type of principal | IAM Users, IAM Roles, Assumed Roles | +| `${aws:PrincipalAccount}` | AWS account ID | IAM Users, Assumed Roles only | + +#### Principal Type Values + +- `IAMUser` - For IAM user ARNs (`arn:aws:iam::account:user/username`) +- `IAMRole` - For IAM role ARNs (`arn:aws:iam::account:role/rolename`) +- `AssumedRole` - For assumed role ARNs (`arn:aws:sts::account:assumed-role/role/session`) + +> **Important**: IAM Roles do NOT have `aws:userid` or `aws:PrincipalAccount` variables. These are only available for IAM Users and Assumed Roles. + +### JWT Claim Variables + +Access any JWT claim using the `${jwt:claim-name}` syntax: + +| Variable | Description | +|----------|-------------| +| `${jwt:preferred_username}` | Preferred username from JWT | +| `${jwt:sub}` | Subject (user ID) from JWT | +| `${jwt:email}` | Email address from JWT | +| `${jwt:*}` | Any custom JWT claim | + +### LDAP Claim Variables + +Access LDAP attributes using the `${ldap:attribute}` syntax: + +| Variable | Description | +|----------|-------------| +| `${ldap:username}` | LDAP username | +| `${ldap:dn}` | LDAP distinguished name | +| `${ldap:*}` | Any custom LDAP attribute | + +### S3 Request Variables + +Standard S3 condition variables can also be used: + +| Variable | Description | +|----------|-------------| +| `${s3:prefix}` | Prefix parameter from ListBucket | +| `${aws:SourceIp}` | Source IP address | +| `${aws:SecureTransport}` | Whether request uses HTTPS | + +--- + +## Usage Examples + +### User Isolation with ${aws:username} + +Allow each user to access only their own folder: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject", "s3:PutObject"], + "Resource": "arn:aws:s3:::my-bucket/${aws:username}/*" + } + ] +} +``` + +When user `alice` makes a request, the policy evaluates as: +- Resource: `arn:aws:s3:::my-bucket/alice/*` + +When user `bob` makes a request: +- Resource: `arn:aws:s3:::my-bucket/bob/*` + +### Combining Allow and Deny + +Explicitly allow own folder and deny everything else: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "AllowOwnFolder", + "Effect": "Allow", + "Principal": "*", + "Action": ["s3:GetObject", "s3:PutObject"], + "Resource": "arn:aws:s3:::my-bucket/${aws:username}/*" + }, + { + "Sid": "DenyOtherFolders", + "Effect": "Deny", + "Principal": "*", + "Action": ["s3:GetObject", "s3:PutObject"], + "NotResource": "arn:aws:s3:::my-bucket/${aws:username}/*" + } + ] +} +``` + +### JWT Claims in Resources + +Use JWT claims for dynamic path isolation: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::uploads/${jwt:preferred_username}/*" + } + ] +} +``` + +### Variables in Conditions + +Use variables in condition blocks: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:ListBucket", + "Resource": "arn:aws:s3:::my-bucket", + "Condition": { + "StringLike": { + "s3:prefix": ["${aws:username}/*"] + } + } + } + ] +} +``` + +### Account-Based Access Control + +Restrict access to specific AWS accounts: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Deny", + "Principal": "*", + "Action": "s3:*", + "Resource": "arn:aws:s3:::my-bucket/*", + "Condition": { + "StringNotEquals": { + "aws:PrincipalAccount": ["123456789012"] + } + } + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:*", + "Resource": "arn:aws:s3:::my-bucket/*" + } + ] +} +``` + +### LDAP Integration + +Use LDAP attributes for access control: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:PutObject", + "Resource": "arn:aws:s3:::data/${ldap:username}/*" + }, + { + "Effect": "Allow", + "Principal": "*", + "Action": "s3:GetObject", + "Resource": "arn:aws:s3:::data/*", + "Condition": { + "StringEquals": { + "ldap:dn": "cn=manager,dc=example,dc=org" + } + } + } + ] +} +``` + +--- + +## Principal ARN Parsing + +SeaweedFS automatically extracts variables from principal ARNs: + +### IAM User ARN +``` +arn:aws:iam::123456789012:user/alice +``` +Extracts: +- `aws:username` = `alice` +- `aws:userid` = `alice` +- `aws:principaltype` = `IAMUser` +- `aws:PrincipalAccount` = `123456789012` + +### IAM User with Path +``` +arn:aws:iam::123456789012:user/division/team/alice +``` +Extracts: +- `aws:username` = `alice` (last segment) +- `aws:userid` = `alice` +- `aws:principaltype` = `IAMUser` +- `aws:PrincipalAccount` = `123456789012` + +### IAM Role ARN +``` +arn:aws:iam::123456789012:role/MyRole +``` +Extracts: +- `aws:username` = `MyRole` +- `aws:principaltype` = `IAMRole` + +**Note**: IAM Roles do NOT have `aws:userid` or `aws:PrincipalAccount`. + +### Assumed Role ARN +``` +arn:aws:sts::123456789012:assumed-role/MyRole/session-alice +``` +Extracts: +- `aws:username` = `session-alice` (session name) +- `aws:userid` = `session-alice` +- `aws:principaltype` = `AssumedRole` +- `aws:PrincipalAccount` = `123456789012` + +--- + +## Best Practices + +### 1. Use Explicit Deny for Security + +Combine Allow and Deny statements to prevent unauthorized access: + +```json +{ + "Statement": [ + { + "Effect": "Allow", + "Action": "s3:*", + "Resource": "arn:aws:s3:::bucket/${aws:username}/*" + }, + { + "Effect": "Deny", + "Action": "s3:*", + "NotResource": "arn:aws:s3:::bucket/${aws:username}/*" + } + ] +} +``` + +### 2. Test Policies Thoroughly + +Test with different users to ensure variables substitute correctly: + +```bash +# Test as alice +aws s3 cp file.txt s3://bucket/alice/file.txt --profile alice + +# Test as bob +aws s3 cp file.txt s3://bucket/bob/file.txt --profile bob + +# Verify isolation +aws s3 ls s3://bucket/alice/ --profile bob # Should fail +``` + +### 3. Use Conditions for Complex Logic + +Combine variables with conditions for fine-grained control: + +```json +{ + "Condition": { + "StringLike": { + "s3:prefix": ["${aws:username}/*"] + }, + "StringEquals": { + "aws:principaltype": "IAMUser" + } + } +} +``` + +### 4. Document Variable Expectations + +Clearly document which JWT/LDAP claims your policies expect: + +```json +{ + "Comment": "Requires JWT claims: preferred_username, department", + "Statement": [ + { + "Resource": "arn:aws:s3:::data/${jwt:department}/${jwt:preferred_username}/*" + } + ] +} +``` + +--- + +## Troubleshooting + +### Variables Not Substituting + +**Problem**: Variables appear as literal strings in logs + +**Solution**: Ensure the variable exists in the request context. Check: +- Principal ARN format is correct +- JWT claims are present in the token +- LDAP attributes are mapped correctly + +### Access Denied Despite Matching Path + +**Problem**: User can't access their own folder + +**Solution**: Check for conflicting Deny statements. Remember that Deny always wins: + +```json +{ + "Statement": [ + { + "Effect": "Allow", + "Resource": "arn:aws:s3:::bucket/${aws:username}/*" + }, + { + "Effect": "Deny", + "NotResource": "arn:aws:s3:::bucket/${aws:username}/*" + } + ] +} +``` + +### IAM Role Variables Missing + +**Problem**: `aws:userid` or `aws:PrincipalAccount` not available for IAM Roles + +**Solution**: This is expected behavior. IAM Roles only have `aws:username` and `aws:principaltype`. Use Assumed Roles if you need these variables. + +--- + +## Related Documentation + +- [[Amazon S3 API]] - S3 API compatibility +- [[S3 Credentials]] - Managing S3 credentials +- [[OIDC Integration]] - JWT-based authentication +- [[Amazon IAM API]] - IAM API support