diff --git a/README.md b/README.md
index 4afa6ae..867cc3a 100644
--- a/README.md
+++ b/README.md
@@ -32,6 +32,7 @@ No modules.
| [aws_iam_role.github_actions_channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.github_actions_sops_kms](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.github_actions_twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role.github_actions_xnoto_dev_site_deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role.opencode_mcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_iam_role_policy.bedrock_batch_invoke](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.bedrock_batch_s3](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
@@ -43,6 +44,7 @@ No modules.
| [aws_iam_role_policy.github_actions_channel_project_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | 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_role_policy.github_actions_twilio_state](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
+| [aws_iam_role_policy.github_actions_xnoto_dev_site_deploy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.opencode_mcp_agent_pipe](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy.opencode_mcp_secrets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy) | resource |
| [aws_iam_role_policy_attachment.opencode_mcp_oauth](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
@@ -111,6 +113,7 @@ No inputs.
| [twilio\_github\_actions\_state\_role\_arn](#output\_twilio\_github\_actions\_state\_role\_arn) | GitHub OIDC role for tfroot-twilio OpenTofu state and SOPS decrypt access |
| [twilio\_state\_bucket\_name](#output\_twilio\_state\_bucket\_name) | Versioned private S3 bucket for tfroot-twilio OpenTofu state |
| [web\_bucket\_endpoints](#output\_web\_bucket\_endpoints) | Website endpoints for public web S3 buckets |
+| [xnoto\_dev\_site\_deploy\_role\_arn](#output\_xnoto\_dev\_site\_deploy\_role\_arn) | GitHub OIDC role for publishing xnoto.dev static-site assets from main |
## AWS Bedrock batch inference (50% discount)
diff --git a/aws-github-oidc.tf b/aws-github-oidc.tf
index c793498..cb73207 100644
--- a/aws-github-oidc.tf
+++ b/aws-github-oidc.tf
@@ -406,3 +406,63 @@ resource "aws_iam_role_policy" "github_actions_channel_project_site_deploy" {
]
})
}
+
+# Site publication is separate from infrastructure management and is trusted
+# only from xnoto.dev's main branch. It can publish assets but cannot manage
+# state, infrastructure resources, or CloudFront distributions.
+resource "aws_iam_role" "github_actions_xnoto_dev_site_deploy" {
+ name = "github-actions-xnoto-dev-site-deploy"
+
+ 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"
+ "token.actions.githubusercontent.com:sub" = "repo:xnoto@121333299/xnoto.dev@1358381234:ref:refs/heads/main"
+ }
+ }
+ }
+ ]
+ })
+
+ tags = {
+ ManagedBy = "Terraform"
+ Purpose = "xnoto-dev-site-deployment"
+ }
+}
+
+resource "aws_iam_role_policy" "github_actions_xnoto_dev_site_deploy" {
+ name = "xnoto-dev-site-deployment"
+ role = aws_iam_role.github_actions_xnoto_dev_site_deploy.id
+
+ policy = jsonencode({
+ Version = "2012-10-17"
+ Statement = [
+ {
+ Sid = "ListSiteAssetBucket"
+ Effect = "Allow"
+ Action = ["s3:ListBucket"]
+ Resource = "arn:aws:s3:::xnoto.dev"
+ },
+ {
+ Sid = "PublishSiteAssets"
+ Effect = "Allow"
+ Action = [
+ "s3:AbortMultipartUpload",
+ "s3:DeleteObject",
+ "s3:GetObject",
+ "s3:ListMultipartUploadParts",
+ "s3:PutObject"
+ ]
+ Resource = "arn:aws:s3:::xnoto.dev/*"
+ }
+ ]
+ })
+}
diff --git a/main.tf b/main.tf
index 23d5a13..581ec3e 100644
--- a/main.tf
+++ b/main.tf
@@ -30,5 +30,6 @@ locals {
"makeitwork.cloud",
"onion.makeitwork.cloud",
"orthodox.channel",
+ "xnoto.dev",
])
}
diff --git a/outputs.tf b/outputs.tf
index 7d97b37..2e69e88 100644
--- a/outputs.tf
+++ b/outputs.tf
@@ -54,6 +54,11 @@ output "channel_project_site_deploy_role_arn" {
value = aws_iam_role.github_actions_channel_project_site_deploy.arn
}
+output "xnoto_dev_site_deploy_role_arn" {
+ description = "GitHub OIDC role for publishing xnoto.dev static-site assets from main"
+ value = aws_iam_role.github_actions_xnoto_dev_site_deploy.arn
+}
+
output "twilio_state_bucket_name" {
description = "Versioned private S3 bucket for tfroot-twilio OpenTofu state"
value = aws_s3_bucket.private[local.twilio_state_bucket].bucket