← Projects
Multi-Cloud Secure Document Platform, AWS and Azure What it is A build that answers one question: how does a workload in one cloud authenticate to storage in another without an access key sitting in a config file?
The answer is OIDC workload identity federation. Everything else in the project exists to make that answer defensible - a storage layer worth protecting, two API tiers with genuinely different threat models, and negative tests that prove the boundaries rather than assuming them.
Multi-cloud secure document platform: Azure identity and API tiers federating into an encrypted AWS document store
A Terraform control plane deploys into an Azure subscription and an AWS account from a single configuration. On the Azure side, Entra ID issues tokens, a user-assigned managed identity holds Key Vault access, and two API Management tiers sit in a virtual network: an external tier that validates JWTs and an internal VNet-injected tier that proxies Key Vault using its own managed identity. On the AWS side, an OIDC provider trusts the Entra issuer and an IAM role accepts AssumeRoleWithWebIdentity when the audience and subject claims match, granting read access scoped to one S3 prefix and decrypt on one customer-managed KMS key. Four negative tests confirm that a wrong identity, an expired token, an out-of-scope prefix and a missing KMS grant are all denied.
Multi-cloud secure document platform
Azure identity and API tiers federating into an encrypted AWS document store - no stored access keys on either side
Terraform control plane
One configuration, two providers, one state file
azurerm 4.x · aws 5.x · azuread 3.x
Remote state in an Azure Storage blob, leased on write so two
applies cannot collide. Azure resource groups split by lifecycle
so disposable workloads tear down without touching identity.
Operator workstation - outside both clouds
Azure subscription - australiaeast
Entra ID
App roles, signed JWTs
mcdp-documents-api
Tenant
State backend
Versioned, lease locked
container: tfstate
rg-mcdp-tfstate-aue
Key Vault
RBAC authorisation, not legacy access policies
kv-mcdp-dev-nh01
User-assigned managed identity
Key Vault Secrets User · subject of the AWS trust
id-mcdp-reader
rg-mcdp-identity-aue
Virtual network
10.1.0.0/16 · workload and APIM subnets
vnet-mcdp-dev-aue · snet-apim-internal
Network security group
Inbound 3443 control plane, 6390 load balancer
nsg-apim-internal-aue
rg-mcdp-platform-aue
API Management - external tier
validate-jwt: issuer, audience, Api.Access role
apim-mcdp-ext-dev-aue · mock backend
API Management - internal tier
VNet-injected · managed identity to Key Vault
apim-mcdp-int-dev-aue
rg-mcdp-workload-aue
AWS account - ap-southeast-2
OIDC identity provider
Trusts the Entra v1.0 issuer for this tenant
sts.windows.net/<tenant>/
Federated IAM role
sts:AssumeRoleWithWebIdentity, no stored keys
mcdp-databricks-federated-role
Federation trust
S3 document store
Versioned · all four public access blocks on
mcdp-documents-nh01/inbound/
Customer-managed KMS key
SSE-KMS default · bucket keys · annual rotation
alias/mcdp-documents
Bucket policy - three explicit denies
Unencrypted upload · wrong key · plain HTTP
Deny wins over any IAM allow
Document store
Negative tests - all four denied
Different managed identity assumes the role
AccessDenied at STS
Expired temporary credentials reused
ExpiredToken
Read attempted outside the inbound prefix
AccessDenied at S3
kms:Decrypt removed, s3:GetObject kept
AccessDenied - KMS is a separate gate
Verification
1
2
3
4
5
6
internal tier reads a secret via its own managed identity
Trust policy - what AWS checks
Principal.Federated
the OIDC provider ARN
Issuer
enforced structurally
Condition on :aud
api://<api-app-id>
Condition on :sub
managed identity principal
There is no :iss condition key - the issuer is pinned by which provider is named.
Federated role - what it may do
s3:GetObject
inbound/* only
s3:ListBucket
s3:prefix condition
kms:Decrypt
one key ARN
No write, no delete
read path only
ListBucket is scoped by condition, not by ARN - the bucket ARN has no prefix.
Legend
Identity
Data
Security
Edge and API
Outside both clouds
Token or credential path
Synchronous
Asynchronous or out-of-band
Identity is an added category, in the same muted family as the rest.
Flow
1
The workload asks the local instance metadata endpoint for a token. No credential is sent with the request.
2
Entra ID returns a signed JWT whose subject claim is the managed identity's principal ID.
3
The token is presented to AWS STS as AssumeRoleWithWebIdentity against the registered OIDC provider.
4
The trust policy checks the audience and subject claims, then STS returns short-lived credentials.
5
Those credentials read objects under the inbound prefix only; listing is scoped by an s3:prefix condition.
6
S3 asks KMS to unwrap the data key - a second, independent grant.
Cross-cloud OIDC workload identity federation, step by step
A vertical path showing how an Azure workload reads an encrypted object in AWS without any stored access key. The workload asks the local metadata endpoint for a token, Entra ID issues a signed JWT, AWS STS validates the audience and subject claims against the role's trust policy, and returns short-lived credentials. Those credentials read only the inbound prefix, and S3 must separately ask KMS to unwrap the data key. Four negative tests confirm a wrong identity, an expired token, an out-of-scope prefix and a missing KMS grant are all denied.
Cross-cloud federation
No AWS access key exists on the Azure side
Azure subscription
Azure workload
User-assigned managed identity
id-mcdp-reader
1
Entra ID
Signed JWT, subject is the identity
sts.windows.net issuer
2
AWS account
AWS STS
Signature checked offline via JWKS
AssumeRoleWithWebIdentity
3
Federated IAM role
Short-lived credentials, nothing stored
mcdp-databricks-federated-role
4
S3 document store
Read scoped to one prefix only
mcdp-documents-nh01/inbound/
5
Customer-managed KMS key
A second, independent grant
alias/mcdp-documents
Trust policy conditions
Principal
OIDC provider ARN
Issuer
pinned structurally
:aud equals
api://<api-app-id>
:sub equals
identity principal ID
There is no :iss condition key in IAM.
Negative tests - all denied
A different managed identity
AccessDenied at STS
Credentials past expiry
ExpiredToken
A prefix outside inbound/
AccessDenied at S3
kms:Decrypt removed
AccessDenied, cause is KMS
Legend
Identity
Data
Security
Token or credential path
Synchronous
Flow
1
Token requested with no credential sent
2
Signed JWT presented to AWS STS
3
Audience and subject claims must match
4
Read inbound/ only
5
KMS unwraps the key
Two independent authorisation gates guard every encrypted object read
A read request from the federated role is checked twice, in parallel-reading side-by-side gates: the S3 bucket policy decides whether the role may touch that prefix, and the KMS key policy decides whether it may decrypt. Both must allow for the object to be returned. Removing either one denies the read, and when the KMS grant is the missing piece the error still surfaces as an S3 AccessDenied.
Two independent gates
A correct bucket policy is not enough on its own
Federated role
Reading an SSE-KMS encrypted object
s3:GetObject
1
Both must allow
Bucket policy
Prefix scoped
inbound/* only
KMS key policy
Decrypt grant
one key ARN
2
3
Object returned
Data key unwrapped, bytes decrypted
bucket key enabled
Remove either gate
Key disabled, bucket policy untouched
KMS.DisabledException
kms:Decrypt removed from the role
AccessDenied, cause is KMS
Bucket policy - three explicit denies
Not SSE-KMS
PutObject denied
Not this key ARN
PutObject denied
Plain HTTP
all actions denied
Explicit deny beats any IAM allow.
Legend
Identity
Data
Security
Credential path
Failure path
Flow
1
A read arrives with temporary credentials
2
S3 checks prefix
3
KMS checks decrypt
One Terraform configuration, two clouds, and a token exchange that leaves no access key on either side. Every read has to pass the bucket policy and the KMS key policy. Four negative tests check that the boundaries hold. What I built Cross-cloud OIDC workload identity federation. An Azure managed identity gets a token from Entra ID and exchanges it at AWS STS for short-lived credentials. No AWS key is stored on the Azure side. A trust policy that does real work. Conditioned on the audience and subject claims, with the issuer pinned structurally by which OIDC provider is named. The federated role can read one S3 prefix, list only that prefix, and decrypt with one key. Four negative tests, all denied. A different managed identity is refused at STS; expired credentials return ExpiredToken; a read outside the permitted prefix is refused at S3; and removing kms:Decrypt breaks the read even with s3:GetObject intact. An S3 store with two independent authorisation gates. SSE-KMS with a customer-managed key, versioning, all four public access blocks, and a bucket policy with three explicit denies - unencrypted uploads, uploads under the wrong key, and plain HTTP. Two Azure API Management tiers with different security models. The external tier validates JWTs against Entra ID - issuer, audience and a required app role. The internal tier is VNet-injected and reaches Key Vault with its own system-assigned managed identity, so no secret exists in the policy. One Terraform configuration across both clouds, with remote state in an Azure Storage blob, lease-based locking, and hand-built resources brought under management by terraform import.