Skip to content

Commit 6293e46

Browse files
committed
add oidc support for AWS access
1 parent 8cbdaac commit 6293e46

10 files changed

Lines changed: 157 additions & 59 deletions

File tree

README.md

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,13 @@ Supports both workspace-level and organization-level (Enterprise) audit logs.
2020
- An Owner of the Oranization (Enterprise Plan)
2121
- Render Owner ID (`tea-xxx`) — workspace where the Cron Job will be deployed
2222
- [Terraform](https://www.terraform.io/downloads) >= 1.0
23-
- AWS account with permissions to create S3 buckets and IAM users
23+
- AWS account with permissions to create S3 buckets and IAM roles
24+
25+
## AWS Authentication
26+
27+
The Cron Job authenticates to AWS via [Render OIDC](https://render.com/docs/oidc) (currently in alpha): it exchanges a short-lived token for AWS credentials by assuming an IAM role. No long-lived secrets are stored. Render publishes a per-workspace OIDC issuer at `https://oidc.render.com/<render_deployment_workspace_id>`.
28+
29+
The Go application also supports long-lived `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` credentials as a fallback (if `AWS_ROLE_ARN` is unset.
2430

2531
## Quick Start
2632

@@ -33,7 +39,7 @@ cd render-auditlogs/terraform
3339

3440
### 2. Configure authentication
3541

36-
Set up authentication for both providers:
42+
Set up authentication for both providers for the Terraform providers:
3743

3844
```bash
3945
# AWS - use one of these methods:
@@ -57,35 +63,37 @@ terraform apply \
5763
-var='render_workspace_ids=["tea-xxxxx", "tea-yyyyy"]'
5864
```
5965

60-
For Enterprise customers with organization-level audit logs:
66+
This creates an IAM OIDC provider for `https://oidc.render.com/<render_deployment_workspace_id>` (if one does not already exist) and an IAM role the Cron Job assumes at runtime.
67+
68+
If you already have the OIDC provider registered in AWS add:
6169

6270
```bash
63-
terraform apply \
64-
-var="aws_s3_bucket_name=your-audit-logs-bucket" \
65-
-var="render_api_key=${RENDER_API_KEY}" \
66-
-var="render_organization_id=org-xxxxx" \
67-
-var='render_workspace_ids=["tea-xxxxx", "tea-yyyyy"]'
71+
-var="aws_oidc_provider_arn=arn:aws:iam::123456789012:oidc-provider/oidc.render.com/tea-xxxxx"
6872
```
6973

74+
For Enterprise customers with organization-level audit logs, add `-var="render_organization_id=org-xxxxx"`.
75+
7076
## Terraform Variables
7177

72-
| Variable | Required | Default | Description |
73-
| --------------------------- | -------- | ---------------------------- | ------------------------------------------------------ |
74-
| `aws_s3_bucket_name` | Yes | - | Name of the S3 bucket to create for storing audit logs |
75-
| `render_api_key` | Yes | - | Render API key for accessing audit logs |
76-
| `render_workspace_ids` | No | `[]` | List of workspace IDs to fetch audit logs from |
77-
| `render_organization_id` | No | `""` | Organization ID for Enterprise audit logs |
78-
| `aws_iam_user_name` | No | `render-audit-log-processor` | Name of the IAM user created for S3 access |
79-
| `aws_s3_bucket_key_enabled` | No | `false` | Enable S3 bucket key to reduce KMS calls |
80-
| `aws_s3_kms_key_id` | No | `""` | ARN for KMS key to use for encryption |
81-
| `aws_s3_use_kms` | No | `false` | Use KMS for encryption (instead of SSE-S3) |
82-
| `render_cronjob_name` | No | `render-auditlogs` | Name of the Render Cron Job |
83-
| `render_cronjob_schedule` | No | `1/15 * * * *` | Cron schedule (default: every 15 minutes) |
84-
| `render_cronjob_plan` | No | `starter` | Render plan for the Cron Job |
85-
| `render_cronjob_region` | No | `oregon` | Region to deploy the Cron Job |
86-
| `render_project_name` | No | `audit-logs` | Name of the Render project |
87-
88-
*Note*: If you use a KMS key, confirm that the AWS IAM User is setup with the User Permissions for the key.
78+
| Variable | Required | Default | Description |
79+
| --------------------------- | -------- | ---------------------------- | -------------------------------------------------------------------------------------------------- |
80+
| `aws_s3_bucket_name` | Yes | - | Name of the S3 bucket to create for storing audit logs |
81+
| `render_api_key` | Yes | - | Render API key for accessing audit logs |
82+
| `render_deployment_workspace_id` | Yes | - | Render workspace ID (`tea-xxx`) where the Cron Job is deployed; used to build the OIDC issuer URL `oidc.render.com/<render_deployment_workspace_id>` |
83+
| `render_workspace_ids` | No | `[]` | List of workspace IDs to fetch audit logs from |
84+
| `render_organization_id` | No | `""` | Organization ID for Enterprise audit logs |
85+
| `aws_oidc_provider_arn` | No | `""` | ARN of an existing AWS IAM OIDC provider; if empty, one is created |
86+
| `aws_iam_role_name` | No | `render-audit-log-processor` | Name of the IAM role the Cron Job assumes |
87+
| `aws_s3_bucket_key_enabled` | No | `false` | Enable S3 bucket key to reduce KMS calls |
88+
| `aws_s3_kms_key_id` | No | `""` | ARN for KMS key to use for encryption |
89+
| `aws_s3_use_kms` | No | `false` | Use KMS for encryption (instead of SSE-S3) |
90+
| `render_cronjob_name` | No | `render-auditlogs` | Name of the Render Cron Job |
91+
| `render_cronjob_schedule` | No | `1/15 * * * *` | Cron schedule (default: every 15 minutes) |
92+
| `render_cronjob_plan` | No | `starter` | Render plan for the Cron Job |
93+
| `render_cronjob_region` | No | `oregon` | Region to deploy the Cron Job |
94+
| `render_project_name` | No | `audit-logs` | Name of the Render project |
95+
96+
*Note*: If you use a KMS key, confirm that the IAM role is set up with User Permissions for the key.
8997

9098
Example:
9199
```
@@ -97,7 +105,7 @@ Example:
97105
"Sid": "Allow use of the key",
98106
"Effect": "Allow",
99107
"Principal": {
100-
"AWS": "arn:aws:iam::12345:user/render-audit-log-processor"
108+
"AWS": "arn:aws:iam::123456789012:role/render-audit-log-processor"
101109
},
102110
"Action": [
103111
"kms:Encrypt",
@@ -119,7 +127,8 @@ The Terraform configuration creates:
119127
**AWS Resources:**
120128

121129
- S3 bucket (versioned, encrypted, public access blocked)
122-
- IAM user with S3 write permissions
130+
- IAM role with S3 write permissions and an OIDC trust policy scoped to this Cron Job's service ID
131+
- IAM OIDC provider for `oidc.render.com/<render_deployment_workspace_id>` (skipped when `aws_oidc_provider_arn` is set)
123132

124133
**Render Resources:**
125134

@@ -147,6 +156,8 @@ S3_KMS_KEY_ID=arn:aws:kms:us-west-2:123456789012:key/your-key-id # Optional
147156
S3_BUCKET_KEY_ENABLED=true # Optional
148157
```
149158

159+
When `AWS_ROLE_ARN` is set, the application assumes that role via web-identity federation. When it is empty, the AWS SDK's default credential chain picks up `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY` (or your local AWS profile).
160+
150161
2. Run the application:
151162

152163
```bash

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ go 1.25.0
55
require (
66
github.com/aws/aws-sdk-go-v2 v1.39.6
77
github.com/aws/aws-sdk-go-v2/config v1.31.20
8+
github.com/aws/aws-sdk-go-v2/credentials v1.18.24
89
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
10+
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2
911
github.com/google/uuid v1.6.0
1012
github.com/joho/godotenv v1.5.1
1113
github.com/kelseyhightower/envconfig v1.4.0
@@ -14,7 +16,6 @@ require (
1416

1517
require (
1618
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
17-
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 // indirect
1819
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
1920
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
2021
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
@@ -26,7 +27,6 @@ require (
2627
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
2728
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
2829
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
29-
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
3030
github.com/aws/smithy-go v1.23.2 // indirect
3131
github.com/davecgh/go-spew v1.1.1 // indirect
3232
github.com/pmezard/go-difflib v1.0.0 // indirect

pkg/env/env.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66

77
"github.com/aws/aws-sdk-go-v2/aws"
88
awsconfig "github.com/aws/aws-sdk-go-v2/config"
9+
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
10+
"github.com/aws/aws-sdk-go-v2/service/sts"
911
"github.com/joho/godotenv"
1012
"github.com/kelseyhightower/envconfig"
1113

@@ -20,10 +22,12 @@ type Config struct {
2022
S3KMSKeyID string `required:"false" split_words:"true"`
2123
S3UseKMS bool `required:"false" split_words:"true"`
2224
RenderAPIKey string `required:"true" split_words:"true"`
23-
AWSAccessKeyID string `required:"true" split_words:"true"`
24-
AWSSecretAccessKey string `required:"true" split_words:"true"`
2525
AWSRegion string `required:"true" split_words:"true"`
2626

27+
// If AWSRoleARN is empty, the AWS SDK falls back to access keys from the environment
28+
AWSRoleARN string `required:"false" split_words:"true"`
29+
AWSWebIdentityTokenFile string `required:"false" split_words:"true" default:"/var/lib/render/oidc/aws.jwt"`
30+
2731
AWSConfig aws.Config
2832
}
2933

@@ -45,6 +49,14 @@ func LoadConfig(ctx context.Context, config *Config) error {
4549
return err
4650
}
4751

52+
if config.AWSRoleARN != "" {
53+
awscfg.Credentials = aws.NewCredentialsCache(stscreds.NewWebIdentityRoleProvider(
54+
sts.NewFromConfig(awscfg),
55+
config.AWSRoleARN,
56+
stscreds.IdentityTokenFile(config.AWSWebIdentityTokenFile),
57+
))
58+
}
59+
4860
config.AWSConfig = awscfg
4961

5062
return nil

terraform/main.tf

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,11 @@
11

2-
module "aws" {
3-
source = "./modules/aws"
4-
5-
aws_s3_bucket_name = var.aws_s3_bucket_name
6-
aws_iam_user_name = var.aws_iam_user_name
7-
aws_s3_use_kms = var.aws_s3_use_kms
8-
}
2+
data "aws_caller_identity" "current" {}
93

104
module "render" {
115
source = "./modules/render-audit-logs"
126

13-
aws_access_key = module.aws.aws_access_key
14-
aws_secret_access_key = module.aws.aws_secret_access_key
7+
aws_account_id = data.aws_caller_identity.current.account_id
8+
aws_iam_role_name = var.aws_iam_role_name
159
aws_s3_bucket_name = var.aws_s3_bucket_name
1610
aws_s3_bucket_key_enabled = var.aws_s3_bucket_key_enabled
1711
aws_s3_kms_key_id = var.aws_s3_kms_key_id
@@ -26,3 +20,14 @@ module "render" {
2620
render_cronjob_plan = var.render_cronjob_plan
2721
render_cronjob_schedule = var.render_cronjob_schedule
2822
}
23+
24+
module "aws" {
25+
source = "./modules/aws"
26+
27+
aws_s3_bucket_name = var.aws_s3_bucket_name
28+
aws_iam_role_name = var.aws_iam_role_name
29+
aws_s3_use_kms = var.aws_s3_use_kms
30+
render_deployment_workspace_id = var.render_deployment_workspace_id
31+
aws_oidc_provider_arn = var.aws_oidc_provider_arn
32+
render_cron_job_service_id = module.render.cron_job_service_id
33+
}

terraform/modules/aws/iam.tf

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,48 @@
11

2-
resource "aws_iam_user" "log_processor" {
3-
name = var.aws_iam_user_name
2+
locals {
3+
issuer_host = "oidc.render.com"
4+
issuer_url = "https://${local.issuer_host}/${var.render_deployment_workspace_id}"
5+
create_oidc_provider = var.aws_oidc_provider_arn == ""
6+
oidc_provider_arn = var.aws_oidc_provider_arn != "" ? var.aws_oidc_provider_arn : aws_iam_openid_connect_provider.render[0].arn
47
}
58

6-
resource "aws_iam_access_key" "log_processor" {
7-
user = aws_iam_user.log_processor.name
9+
resource "aws_iam_openid_connect_provider" "render" {
10+
count = local.create_oidc_provider ? 1 : 0
11+
12+
url = local.issuer_url
13+
client_id_list = ["sts.amazonaws.com"]
14+
thumbprint_list = []
15+
}
16+
17+
data "aws_iam_policy_document" "assume_role_with_oidc" {
18+
statement {
19+
effect = "Allow"
20+
actions = ["sts:AssumeRoleWithWebIdentity"]
21+
22+
principals {
23+
type = "Federated"
24+
identifiers = [local.oidc_provider_arn]
25+
}
26+
27+
condition {
28+
test = "StringEquals"
29+
variable = "${local.issuer_host}/${var.render_deployment_workspace_id}:aud"
30+
values = ["sts.amazonaws.com"]
31+
}
32+
33+
condition {
34+
test = "StringLike"
35+
variable = "${local.issuer_host}/${var.render_deployment_workspace_id}:sub"
36+
values = ["workspace:${var.render_deployment_workspace_id}:env:*:service:${var.render_cron_job_service_id}"]
37+
}
38+
}
839
}
940

10-
output "aws_access_key" {
11-
value = aws_iam_access_key.log_processor.id
12-
sensitive = true
41+
resource "aws_iam_role" "log_processor" {
42+
name = var.aws_iam_role_name
43+
assume_role_policy = data.aws_iam_policy_document.assume_role_with_oidc.json
1344
}
1445

15-
output "aws_secret_access_key" {
16-
value = aws_iam_access_key.log_processor.secret
17-
sensitive = true
46+
output "role_arn" {
47+
value = aws_iam_role.log_processor.arn
1848
}

terraform/modules/aws/s3.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ resource "aws_s3_bucket_policy" "render_audit_logs" {
3131
Sid = "AllowAuditLogUpload",
3232
Effect = "Allow",
3333
Principal = {
34-
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/render-audit-log-processor"
34+
AWS = aws_iam_role.log_processor.arn
3535
},
3636
Action = [
3737
"s3:ListBucket",

terraform/modules/aws/variables.tf

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ variable "aws_s3_bucket_name" {
33
default = ""
44
}
55

6-
variable "aws_iam_user_name" {
6+
variable "aws_iam_role_name" {
77
type = string
88
default = "render-audit-log-processor"
99
}
@@ -12,3 +12,17 @@ variable "aws_s3_use_kms" {
1212
type = bool
1313
default = false
1414
}
15+
16+
variable "render_deployment_workspace_id" {
17+
type = string
18+
}
19+
20+
variable "aws_oidc_provider_arn" {
21+
type = string
22+
default = ""
23+
}
24+
25+
variable "render_cron_job_service_id" {
26+
type = string
27+
description = "Render Cron Job service ID (crn-xxx); pins the OIDC trust policy sub claim."
28+
}

terraform/modules/render-audit-logs/render.tf

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11

2+
locals {
3+
# Computed from inputs (no reference to aws_iam_role) so the cron job does not
4+
# depend on the role resource; the role's trust policy can then reference this
5+
# cron job's service ID without creating a cycle.
6+
computed_aws_role_arn = "arn:aws:iam::${var.aws_account_id}:role/${var.aws_iam_role_name}"
7+
}
8+
29
resource "render_cron_job" "render-audit-logs" {
310
name = var.render_cronjob_name
411
plan = var.render_cronjob_plan
@@ -20,8 +27,11 @@ resource "render_cron_job" "render-audit-logs" {
2027

2128
env_vars = {
2229
"LOCAL" = { value = "false" },
23-
"AWS_ACCESS_KEY_ID" = {value = var.aws_access_key},
24-
"AWS_SECRET_ACCESS_KEY" = {value = var.aws_secret_access_key}
30+
"AWS_ROLE_ARN" = { value = local.computed_aws_role_arn }
31+
# To use an IAM user access key instead of OIDC, create the user manually,
32+
# set these in the Render dashboard, and clear AWS_ROLE_ARN:
33+
# "AWS_ACCESS_KEY_ID" = { value = "..." }
34+
# "AWS_SECRET_ACCESS_KEY" = { value = "..." }
2535
"AWS_REGION" = {value = var.aws_region}
2636
"ORGANIZATION_ID" = {value = var.render_organization_id}
2737
"WORKSPACE_IDS" = { value = join(",", var.render_workspace_ids) }
@@ -42,3 +52,7 @@ resource "render_project" "audit-logs" {
4252
},
4353
}
4454
}
55+
56+
output "cron_job_service_id" {
57+
value = render_cron_job.render-audit-logs.id
58+
}

terraform/modules/render-audit-logs/variables.tf

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,14 @@ variable "aws_s3_use_kms" {
1717
default = false
1818
}
1919

20-
variable "aws_access_key" {
20+
variable "aws_account_id" {
2121
type = string
22-
sensitive = true
22+
description = "AWS account ID; used to construct the IAM role ARN the cron job assumes via OIDC"
2323
}
2424

25-
variable "aws_secret_access_key" {
25+
variable "aws_iam_role_name" {
2626
type = string
27-
sensitive = true
27+
default = "render-audit-log-processor"
2828
}
2929

3030
variable "aws_region" {

terraform/variables.tf

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,21 @@ variable "aws_s3_use_kms" {
1717
default = false
1818
}
1919

20-
variable "aws_iam_user_name" {
20+
variable "aws_iam_role_name" {
2121
type = string
2222
default = "render-audit-log-processor"
23+
description = "Name of the IAM role the Cron Job assumes via OIDC"
24+
}
25+
26+
variable "render_deployment_workspace_id" {
27+
type = string
28+
description = "Render workspace ID (tea-xxx) where the Cron Job is deployed; used to build the OIDC issuer URL (oidc.render.com/<workspace-id>)"
29+
}
30+
31+
variable "aws_oidc_provider_arn" {
32+
type = string
33+
default = ""
34+
description = "ARN of an existing AWS IAM OIDC provider for Render. If empty, one is created."
2335
}
2436

2537
variable "render_api_key" {

0 commit comments

Comments
 (0)