Tokens
This page describes the use of scoped tokens for storage access on an in-depth conceptual level.
Scoped Tokens for S3 Storage Access
Motivation
AWS STS AssumeRoleWithWebIdentity and MinIO STS AssumeRoleWithWebIdentity allow requesting temporary, limited-privilege credentials for users. To get fine-grained control access to S3 storage, we use OIDC access tokens scoped to one vault and use them to get access to one bucket (i.e. one vault) only. To keep our components zero-trust, we use no privileged broker to update IAM roles when vaults are created or users are given access to vaults, i.e. we use a static mapping to exchange OIDC access tokens for temporary S3 credentials. The static mapping uses a claim in the access token to issue temporary credentials with a dynamic role giving access to the vault's bucket only.
AWS imposes size limits on session policies1 and rejects OIDC tokens that are too large2. So the OIDC token must not grow with the number of vaults. Therefore, we use RFC 8693 token exchange to get fine-grained access tokens before we go to STS.
High-Level Description
Katta S3 STS is based on the following components and their responsibilities:
- Katta Server: synchronizes vault access to Keycloak
- Keycloak: provides tokens based on the user's roles
- AWS/MinIO IAM: gives trust to Keycloak realms and defines the mapping from claims issued to dynamic roles
- AWS/MinIO STS: issues temporary credentials with privileges defined in IAM
- AWS/MinIO S3: checks the access right of the temporary credentials to give access to S3 buckets
Katta S3 STS combines the following standard APIs:
- OAuth 2.0 Authorization Code Grant: the user enters user and password in Keycloak to get OIDC access and refresh tokens
- OAuth 2.0 Token Exchange: the OIDC access token is exchanged for vault-specific OIDC access token
- AssumeRoleWithWebIdentity: the OIDC access token is exchanged for temporary credentials giving access to one vault only
- S3 API: S3 evaluates the credentials before data can be retrieved
Intermediate-Level Description
At an intermediate level, the following diagrams show the token flow from login to S3 access:
- User opens vault in Katta Desktop, client opens browser.
- Keycloak redirects user to login and authorization prompt.
- User enters user name and password.
- Keycloak redirects user back to Katta Desktop with single-use authorization code.
- Katta Desktop calls
/tokenendpoint with authorization code. - Keycloak returns OIDC access token and refresh token for client
cryptomator - Katta Desktop sends OIDC access token for client
cryptomatorexchange toaudience: cryptomatorvaultsclient using/tokenendpoint withgrant_type: urn:ietf:params:oauth:grant-type:token-exchange, requestingscope: <vaultId>. - Keycloak returns access token for OIDC access token with vault-specific claims added by protocol mappers in the requested scope.
- Katta Desktop sends scoped OIDC access token to STS AssumeRoleWithWebIdentity.
- STS returns temporary
AccessKeyId,SecretAccessKeyandSessionToken.- AWS: the temporary role is tagged with the
vaultId. - MinIO: the credentials allow access to one bucket.
- AWS: the temporary role is tagged with the
- AWS only: Katta Desktop sends AWS credentials to STS in order to assume role.
- AWS only: AWS sends credentials to access giving access to one bucket from the session tags.
- Katta Desktop accesses S3 storage with temporary
AccessKeyId,SecretAccessKeyandSessionToken.
IAM Data Model
MinIO IAM Data Model
The following diagram shows the data model we use for Policy-Based Access Control with MinIO STS:
Commands and policy documents annotating this model
Attached to MinIO IDP — one identity provider per role_policy:
mc idp openid add myminio cryptomator \
config_url="http://localhost:8180/realms/cryptomator/.well-known/openid-configuration" \
client_id="cryptomator" \
client_secret="ignore-me" \
role_policy="katta-createbucketpolicy"
mc idp openid add myminio cryptomator \
config_url="http://localhost:8180/realms/cryptomator/.well-known/openid-configuration" \
client_id="cryptomator" \
client_secret="ignore-me" \
role_policy="katta-accessbucketpolicy"
Attached to MinIO Policy — the canned policies the providers bind to:
mc admin policy create myminio katta-createbucketpolicy setup/minio_sts/createbucketpolicy.json
mc admin policy create myminio katta-accessbucketpolicy setup/minio_sts/accessbucketpolicy.json
createbucketpolicy.json, the statement of the bucket creation policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:GetBucketPolicy",
"s3:PutBucketVersioning",
"s3:GetBucketVersioning"
],
"Resource": [
"arn:aws:s3:::katta-*/"
]
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::katta-*/*/",
"arn:aws:s3:::katta-*/vault.uvf"
]
}
]
}
accessbucketpolicy.json, the statement of the vault access policy, evaluated against the client_id claim:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:GetBucketVersioning",
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
],
"Resource": [
"arn:aws:s3:::katta-${jwt:client_id}"
]
},
{
"Effect": "Allow",
"Action": [
"s3:AbortMultipartUpload",
"s3:DeleteObject",
"s3:GetObject",
"s3:ListMultipartUploadParts",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::katta-${jwt:client_id}/*"
]
}
]
}
OpenID Identities and Policies are installed once during Katta Server Setup (or before the corresponding storage profile(s) for a new storage location are uploaded).
An STS request with a token issued by a configured OpenID Identity (defined by config_url, client_id, client_secret) returns credentials giving access to
the linked
role_policy, i.e.
- Bucket creation: allows to create a new bucket within a certain prefix and to upload the vault template incl. the
vault.uvffile, and to set bucket versioning. - Vault access: allows reading and writing operations in the bucket as specified by the
client_idclaim in the JWT access token.
MinIO has only a limited list of OpenID Policy Variables that can be evaluated in Policy-Based Access Control.
Refer to Keycloak on how the corresponding claim is added only to the access tokens of users which have access to the corresponding vault.
AWS IAM Data Model
The following diagram show the data model we use for OIDC Federation to request temporary security credentials in AssumeRoleWithWebIdentity:
Commands and policy documents annotating this model
Attached to AWS OIDC Provider:
aws iam create-open-id-connect-provider \
--url https://keycloak.example.com/realms/cryptomator \
--client-id-list cryptomator cryptomatorhub \
--thumbprint-list <thumbprint>
The provider it creates:
{
"Url": "keycloak.example.com/realms/cryptomator",
"ClientIDList": [
"cryptomatorhub",
"cryptomator"
],
"ThumbprintList": [
"<thumbprint>"
],
"CreateDate": "2023-11-13T13:51:32.729000+00:00",
"Tags": []
}
Attached to AWS IAM Role — the Federated trust relationship:
aws iam create-role \
--role-name katta-create-bucket \
--assume-role-policy-document file://.../aws_stscreatebuckettrustpolicy.json
Trust policy granting web identity federation with session tagging:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": [
"arn:aws:iam::<account-id>:oidc-provider/keycloak-staging.example.com/realms/cryptomator",
"arn:aws:iam::<account-id>:oidc-provider/keycloak.example.com/realms/cryptomator"
]
},
"Action": [
"sts:AssumeRoleWithWebIdentity",
"sts:TagSession"
],
"Condition": {}
}
]
}
Trust policy granting web identity federation without session tagging:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": [
"arn:aws:iam::<account-id>:oidc-provider/keycloak.example.com/realms/cryptomator",
"arn:aws:iam::<account-id>:oidc-provider/keycloak-staging.example.com/realms/cryptomator"
]
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {}
}
]
}
Trust policy for the second role in the chain, requiring the tag to be transitive:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::<account-id>:role/katta-access-bucket-web-identity-role"
},
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Condition": {
"ForAnyValue:StringEquals": {
"sts:TransitiveTagKeys": "${aws:RequestTag/Vault}"
}
}
}
]
}
Attached to AWS Role Policy:
aws iam put-role-policy \
--role-name katta-create-bucket \
--policy-name katta-create-bucket \
--policy-document file://.../aws_stscreatebucketpermissionpolicy.json
Permission policy for bucket creation and vault template upload:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:GetBucketPolicy",
"s3:PutBucketVersioning",
"s3:GetBucketVersioning",
"s3:GetAccelerateConfiguration",
"s3:PutAccelerateConfiguration",
"s3:GetEncryptionConfiguration",
"s3:PutEncryptionConfiguration"
],
"Resource": "arn:aws:s3:::katta-*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::katta-*/vault.uvf",
"arn:aws:s3:::katta-*/*/"
]
}
]
}
Permission policy allowing the first role in the chain to assume the second:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"sts:AssumeRole",
"sts:TagSession"
],
"Resource": "arn:aws:iam::<account-id>:role/katta-access-bucket-tagged-session-role"
}
]
}
Permission policy for vault access, scoped by the session tag:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketLocation",
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketVersioning"
],
"Resource": "arn:aws:s3:::katta-${aws:PrincipalTag/Vault}"
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
"Resource": "arn:aws:s3:::katta-${aws:PrincipalTag/Vault}/*"
}
]
}
An STS request with token issued by a configured OpenID Connect Provider (defined by url, client_id, thumbprint) returns credentials from Role Policies
attached (role-name) to roles trusting the OIDC Provider (Federated):
- Bucket creation: allows to create a new bucket within a certain prefix and to upload the vault template incl. the
vault.uvffile, and to set bucket versioning. - Vault Access:
- First call in chain: credentials allow to assume second role and
to tag the session with a tag from the
https://aws.amazon.com/tagsclaim in the OIDC token. - Second call in chain: allows reading and writing operations in the bucket by passing session tags in the session of the credentials from the first call.
- First call in chain: credentials allow to assume second role and
to tag the session with a tag from the
Tokens with Inline Policy to Create S3 Bucket and Upload Vault Template
The following only applies to Katta Web. Katta Desktop is not subject to browser CORS restrictions.
Zero-knowledge covers the vault data and keys. For storage management, Katta Server is almost zero trust as well: it holds no storage credentials of its own. The only moment it acts on storage is bucket creation for Katta Web with scoped credentials — a browser cannot create a bucket and use it right away, as S3 does not offer bucket creation and setting CORS as a joint operation (see Troubleshooting). For this single operation, Katta Web hands Katta Server temporary credentials that are:
- short-lived: requested with the minimal
DurationSecondsof 900 seconds, - role-restricted: issued for the create-bucket role of the storage profile, whose permission policy is limited to the configured bucket prefix (see Storage Profiles),
- downscoped by an inline session policy to exactly the new vault's bucket and the template objects.
The effective permissions are the intersection of the role's permission policy and the inline session policy.
Notably, the credentials contain no read permission on object contents (s3:GetObject) at all — even for the new bucket, Katta Server can only write the
(client-side encrypted) vault template.
Create S3 Bucket
Scoped Credentials
The following steps describe how Katta Web creates a new S3 bucket with scoped credentials.
-
Katta Web calls AssumeRoleWithWebIdentity directly at the STS endpoint of the storage profile (AWS or MinIO), with the user's OIDC access token as web identity, the storage profile's
stsRoleCreateBucketHubrole ARN,DurationSeconds: 900, the vault ID asRoleSessionName— and the following inline session policy, with<bucket>replaced by the new vault's bucket name (<bucketPrefix><vaultId>):{"Version": "2012-10-17","Statement": [{"Effect": "Allow","Action": ["s3:CreateBucket","s3:GetBucketPolicy","s3:PutBucketVersioning","s3:GetBucketVersioning"],"Resource": "arn:aws:s3:::<bucket>"},{"Effect": "Allow","Action": ["s3:PutObject"],"Resource": ["arn:aws:s3:::<bucket>/*.uvf","arn:aws:s3:::<bucket>/*/"]}]} -
STS service returns temporary credentials whose permissions are the intersection of the create-bucket role's permission policy and this session policy.
-
Katta Web sends the temporary credentials together with the client-side encrypted vault template (
vault.uvf, root directory hashdir.uvf), the region, and the storage profile reference to Katta Server (PUT /api/storage/{vaultId}). -
Katta Server creates the bucket using S3 API.
Katta Desktop assumes the katta-create-bucket role from the storage profile itself and creates the bucket and uploads the vault template directly.
Here the create-bucket role's permission policy (bucket prefix) is the effective restriction. This is also why the storage profile carries two create-bucket
role ARNs:
stsRoleCreateBucketHub(assumed by Katta Web, credentials passed to Katta Server)stsRoleCreateBucketClient(assumed by the Katta Desktop directly).
Static Credentials
The bucket must already exist and requires the bucket CORS settings described in Troubleshooting)
Katta Desktop creates the S3 bucket; does not require the bucket to exist or any bucket CORS settings.
Vault Template Upload
The vault template is encrypted prior to upload.
Scoped Credentials
The upload uses the same credentials from creating the bucket described above. The session policy's s3:PutObject statement matches exactly the template objects (vault.uvf, dir.uvf, and the root directory placeholder ending in /) and nothing else.
Static Credentials
Katta Web uploads the template directly with the static credentials provided by the user.
This requires the bucket CORS settings described in Troubleshooting).