diff --git a/.github/workflows/opentofu.yml b/.github/workflows/opentofu.yml
index 91f72ff..80400d0 100644
--- a/.github/workflows/opentofu.yml
+++ b/.github/workflows/opentofu.yml
@@ -10,6 +10,7 @@ on:
permissions:
contents: read
+ id-token: write
pull-requests: write
jobs:
diff --git a/.sops.yaml b/.sops.yaml
index 8967c45..44d4ef5 100644
--- a/.sops.yaml
+++ b/.sops.yaml
@@ -1,3 +1,4 @@
---
creation_rules:
- - age: age152ek83tm4fj5u70r3fecytn4kg7c5xca24erjchxexx4pfqg6das7q763l
+ - kms: arn:aws:kms:us-west-2:332355796717:key/0a45c0f6-71dc-4d54-ab33-9df4de1a9e91
+ age: age152ek83tm4fj5u70r3fecytn4kg7c5xca24erjchxexx4pfqg6das7q763l
diff --git a/README.md b/README.md
index 66d3d21..00ae4f7 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,9 @@ No modules.
| Name | Type |
| ---- | ---- |
| [aws_iam_access_key.admin_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource |
+| [aws_iam_openid_connect_provider.github_actions](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) | resource |
+| [aws_iam_role.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_user.admin](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource |
| [aws_iam_user_policy_attachment.admin_attach](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource |
| [aws_kms_alias.sops](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource |
@@ -45,6 +48,7 @@ No inputs.
| Name | Description |
| ---- | ----------- |
| [admin\_access\_keys](#output\_admin\_access\_keys) | Admin IAM user access keys |
+| [github\_actions\_sops\_kms\_role\_arn](#output\_github\_actions\_sops\_kms\_role\_arn) | IAM role ARN for GitHub Actions SOPS KMS access |
| [sops\_kms\_key\_arn](#output\_sops\_kms\_key\_arn) | KMS key ARN for future SOPS AWS KMS recipients |
| [web\_bucket\_endpoints](#output\_web\_bucket\_endpoints) | Website endpoints for public web S3 buckets |
diff --git a/aws-github-oidc.tf b/aws-github-oidc.tf
new file mode 100644
index 0000000..84a12f5
--- /dev/null
+++ b/aws-github-oidc.tf
@@ -0,0 +1,71 @@
+resource "aws_iam_openid_connect_provider" "github_actions" {
+ url = "https://token.actions.githubusercontent.com"
+
+ client_id_list = [
+ "sts.amazonaws.com"
+ ]
+
+ thumbprint_list = [
+ "6938fd4d98bab03faadb97b34396831e3780aea1"
+ ]
+
+ tags = {
+ ManagedBy = "Terraform"
+ }
+}
+
+resource "aws_iam_role" "github_actions_sops_kms" {
+ name = "github-actions-sops-kms"
+
+ assume_role_policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Effect = "Allow"
+ Principal = {
+ Federated = aws_iam_openid_connect_provider.github_actions.arn
+ }
+ Action = "sts:AssumeRoleWithWebIdentity"
+ Condition = {
+ StringEquals = {
+ "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
+ }
+ StringLike = {
+ "token.actions.githubusercontent.com:sub" = [
+ "repo:makeitworkcloud/tfroot-aws:*",
+ "repo:makeitworkcloud/tfroot-cloudflare:*",
+ "repo:makeitworkcloud/tfroot-github:*",
+ "repo:makeitworkcloud/tfroot-libvirt:*"
+ ]
+ }
+ }
+ }
+ ]
+ })
+
+ tags = {
+ ManagedBy = "Terraform"
+ }
+}
+
+resource "aws_iam_role_policy" "github_actions_sops_kms" {
+ name = "sops-kms"
+ role = aws_iam_role.github_actions_sops_kms.id
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Effect = "Allow"
+ Action = [
+ "kms:Decrypt",
+ "kms:DescribeKey",
+ "kms:Encrypt",
+ "kms:GenerateDataKey*",
+ "kms:ReEncrypt*"
+ ]
+ Resource = aws_kms_key.sops.arn
+ }
+ ]
+ })
+}
diff --git a/outputs.tf b/outputs.tf
index 5b710fb..e0ab005 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -19,3 +19,8 @@ output "sops_kms_key_arn" {
description = "KMS key ARN for future SOPS AWS KMS recipients"
value = aws_kms_key.sops.arn
}
+
+output "github_actions_sops_kms_role_arn" {
+ description = "IAM role ARN for GitHub Actions SOPS KMS access"
+ value = aws_iam_role.github_actions_sops_kms.arn
+}
diff --git a/secrets/secrets.yaml b/secrets/secrets.yaml
index 1a3d38e..1355baa 100644
--- a/secrets/secrets.yaml
+++ b/secrets/secrets.yaml
@@ -4,15 +4,20 @@ s3_region: ENC[AES256_GCM,data:eOyGm9ay1WsF,iv:wDUtASNnplZ76JJh54xlKKcXNKAtosepQ
s3_access_key: ENC[AES256_GCM,data:wQL1zGnkAtWM1EWgooXMMq5dRHI=,iv:wmRjCt28jloov7zKNs4TaZ9GeKhO6/nqPqY1sxFmaQ4=,tag:ZDIeBorMgwkVrdv80SYiGw==,type:str]
s3_secret_key: ENC[AES256_GCM,data:0EcLqSBTSCQ83LJ1RIuPjv7jzBm4KDeJVyUnGFVzxsCSCo1d+no7lQ==,iv:TuG8ATqDozZ5TsuBmHPNQC7pg1GXfrHgWxDIpjbFsWQ=,tag:46XhMpFqfNoPkteVcFZDeQ==,type:str]
sops:
+ kms:
+ - arn: arn:aws:kms:us-west-2:332355796717:key/0a45c0f6-71dc-4d54-ab33-9df4de1a9e91
+ created_at: "2026-06-19T03:45:00Z"
+ enc: AQICAHj1IggLFhM4nJnKEvmbEpk5E9RxZZoxpZYUW0taoyrz1AGxCPeJfC+xwLXrjLnKdN4HAAAAfjB8BgkqhkiG9w0BBwagbzBtAgEAMGgGCSqGSIb3DQEHATAeBglghkgBZQMEAS4wEQQMo4N0BsmO7344VxgjAgEQgDuroFMxzwYxFsG1vHaITeOI1tFQ1iC2KC9RZRe/Mu3rPA3u0A25tpqYXtpi7CBcUPgCMfg4gsWfY68pDQ==
+ aws_profile: ""
age:
- recipient: age152ek83tm4fj5u70r3fecytn4kg7c5xca24erjchxexx4pfqg6das7q763l
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
- YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBqa1V4WGpuWGVlY2xMcVk5
- QlJFeHk3ZjdrcVpnRDFRSmlRQ2FTZnE4WTFRCjQ5UmRONDV3a2xTc3Mya0wvekN6
- RmJrQ09nZzk4VkZxRGlremcrUWU1Z2MKLS0tIHpFZFVpV3lpOUJNSDFwdDRpazJK
- d2pVcDh3cDNqY1gzSVR5Z2NXcld1Qm8KRdv8vKhMBi1R8fGIphdmY4pfHV1sAqSb
- nAXWA6Ut5/KAPIluSnBtWFkcakulcXYT01XorziztVS0X4nJDzEvMg==
+ YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBMZ0xJeTIwSnZ4UzlKSDQ5
+ M1VQSjJaOGZ1aFNUN2l5Y1l4L1dnbUtGUGhRCnhsWi9pV21WdGNNVERaQXFaVzR6
+ TzhUWHllUUpQeTRaU2R3eU92S0hDVmcKLS0tIHhZeFZDRHdlU3Z2WERtdHJXM1dJ
+ MjBPNmJWWWpxY3pRd25IWldvUy9wS2sKHovTPwKIU1gsamMMlGxSvCiO6EwJ6pNO
+ UZF83FI1e+oEenynCmziC3BFaA0tpcYj12pEuiZAAroUPurEmJ+D0Q==
-----END AGE ENCRYPTED FILE-----
lastmodified: "2025-09-27T23:45:19Z"
mac: ENC[AES256_GCM,data:AcSRYq32GptizEXMujOIh4cC29BVNQj4lad85BWfHl6TGQA7oWN5A32xoXqCWqpWEuVDpndUAPZXvMRK8n53djXEXb/A6G9R8vEzaKOUB9zijItanUXkVK63d7KE9X8JrgtV7KQcWZx01qQitMzezMTkouvkfYpodPr1hkV2PIk=,iv:o5Q3VQG55jeCVHTNAAV1HgN91SU6jbx+IFZVGx2vN7o=,tag:GX/v69ggBNUpnLGFPBINdg==,type:str]