Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 38 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,13 @@ Supports both workspace-level and organization-level (Enterprise) audit logs.
- An Owner of the Oranization (Enterprise Plan)
- Render Owner ID (`tea-xxx`) — workspace where the Cron Job will be deployed
- [Terraform](https://www.terraform.io/downloads) >= 1.0
- AWS account with permissions to create S3 buckets and IAM users
- AWS account with permissions to create S3 buckets and IAM roles

## AWS Authentication

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>`.

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.

## Quick Start

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

### 2. Configure authentication

Set up authentication for both providers:
Set up authentication for both providers for the Terraform providers:

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

For Enterprise customers with organization-level audit logs:
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.

If you already have the OIDC provider registered in AWS add:

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

For Enterprise customers with organization-level audit logs, add `-var="render_organization_id=org-xxxxx"`.

## Terraform Variables

| Variable | Required | Default | Description |
| --------------------------- | -------- | ---------------------------- | ------------------------------------------------------ |
| `aws_s3_bucket_name` | Yes | - | Name of the S3 bucket to create for storing audit logs |
| `render_api_key` | Yes | - | Render API key for accessing audit logs |
| `render_workspace_ids` | No | `[]` | List of workspace IDs to fetch audit logs from |
| `render_organization_id` | No | `""` | Organization ID for Enterprise audit logs |
| `aws_iam_user_name` | No | `render-audit-log-processor` | Name of the IAM user created for S3 access |
| `aws_s3_bucket_key_enabled` | No | `false` | Enable S3 bucket key to reduce KMS calls |
| `aws_s3_kms_key_id` | No | `""` | ARN for KMS key to use for encryption |
| `aws_s3_use_kms` | No | `false` | Use KMS for encryption (instead of SSE-S3) |
| `render_cronjob_name` | No | `render-auditlogs` | Name of the Render Cron Job |
| `render_cronjob_schedule` | No | `1/15 * * * *` | Cron schedule (default: every 15 minutes) |
| `render_cronjob_plan` | No | `starter` | Render plan for the Cron Job |
| `render_cronjob_region` | No | `oregon` | Region to deploy the Cron Job |
| `render_project_name` | No | `audit-logs` | Name of the Render project |

*Note*: If you use a KMS key, confirm that the AWS IAM User is setup with the User Permissions for the key.
| Variable | Required | Default | Description |
| --------------------------- | -------- | ---------------------------- | -------------------------------------------------------------------------------------------------- |
| `aws_s3_bucket_name` | Yes | - | Name of the S3 bucket to create for storing audit logs |
| `render_api_key` | Yes | - | Render API key for accessing audit logs |
| `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>` |
| `render_workspace_ids` | No | `[]` | List of workspace IDs to fetch audit logs from |
| `render_organization_id` | No | `""` | Organization ID for Enterprise audit logs |
| `aws_oidc_provider_arn` | No | `""` | ARN of an existing AWS IAM OIDC provider; if empty, one is created |
| `aws_iam_role_name` | No | `render-audit-log-processor` | Name of the IAM role the Cron Job assumes |
| `aws_s3_bucket_key_enabled` | No | `false` | Enable S3 bucket key to reduce KMS calls |
| `aws_s3_kms_key_id` | No | `""` | ARN for KMS key to use for encryption |
| `aws_s3_use_kms` | No | `false` | Use KMS for encryption (instead of SSE-S3) |
| `render_cronjob_name` | No | `render-auditlogs` | Name of the Render Cron Job |
| `render_cronjob_schedule` | No | `1/15 * * * *` | Cron schedule (default: every 15 minutes) |
| `render_cronjob_plan` | No | `starter` | Render plan for the Cron Job |
| `render_cronjob_region` | No | `oregon` | Region to deploy the Cron Job |
| `render_project_name` | No | `audit-logs` | Name of the Render project |

*Note*: If you use a KMS key, confirm that the IAM role is set up with User Permissions for the key.

Example:
```
Expand All @@ -97,7 +105,7 @@ Example:
"Sid": "Allow use of the key",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::12345:user/render-audit-log-processor"
"AWS": "arn:aws:iam::123456789012:role/render-audit-log-processor"
},
"Action": [
"kms:Encrypt",
Expand All @@ -119,7 +127,8 @@ The Terraform configuration creates:
**AWS Resources:**

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

**Render Resources:**

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

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).

2. Run the application:

```bash
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ go 1.25.0
require (
github.com/aws/aws-sdk-go-v2 v1.39.6
github.com/aws/aws-sdk-go-v2/config v1.31.20
github.com/aws/aws-sdk-go-v2/credentials v1.18.24
github.com/aws/aws-sdk-go-v2/service/s3 v1.90.2
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1
github.com/kelseyhightower/envconfig v1.4.0
Expand All @@ -14,7 +16,6 @@ require (

require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.3 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.18.24 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.13 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.13 // indirect
Expand All @@ -26,7 +27,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.13 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.3 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.40.2 // indirect
github.com/aws/smithy-go v1.23.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
Expand Down
19 changes: 17 additions & 2 deletions pkg/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (

"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/joho/godotenv"
"github.com/kelseyhightower/envconfig"

Expand All @@ -20,10 +22,14 @@ type Config struct {
S3KMSKeyID string `required:"false" split_words:"true"`
S3UseKMS bool `required:"false" split_words:"true"`
RenderAPIKey string `required:"true" split_words:"true"`
AWSAccessKeyID string `required:"true" split_words:"true"`
AWSSecretAccessKey string `required:"true" split_words:"true"`
AWSRegion string `required:"true" split_words:"true"`

// If AWSWebIdentityTokenFile is set, OIDC is used. Otherwise the AWS SDK's
// default credential chain falls back to AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY.
AWSRoleARN string `required:"false" split_words:"true"`
// variable is auto-set by Render when OIDC is enabled.
AWSWebIdentityTokenFile string `required:"false" split_words:"true"`

AWSConfig aws.Config
}

Expand All @@ -45,6 +51,15 @@ func LoadConfig(ctx context.Context, config *Config) error {
return err
}

if config.AWSWebIdentityTokenFile != "" {
logger.FromContext(ctx).Info("Using OIDC authentication")
awscfg.Credentials = aws.NewCredentialsCache(stscreds.NewWebIdentityRoleProvider(
sts.NewFromConfig(awscfg),
config.AWSRoleARN,
stscreds.IdentityTokenFile(config.AWSWebIdentityTokenFile),
))
}

config.AWSConfig = awscfg

return nil
Expand Down
23 changes: 14 additions & 9 deletions terraform/main.tf
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@

module "aws" {
source = "./modules/aws"

aws_s3_bucket_name = var.aws_s3_bucket_name
aws_iam_user_name = var.aws_iam_user_name
aws_s3_use_kms = var.aws_s3_use_kms
}
data "aws_caller_identity" "current" {}

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

aws_access_key = module.aws.aws_access_key
aws_secret_access_key = module.aws.aws_secret_access_key
aws_account_id = data.aws_caller_identity.current.account_id
aws_iam_role_name = var.aws_iam_role_name
aws_s3_bucket_name = var.aws_s3_bucket_name
aws_s3_bucket_key_enabled = var.aws_s3_bucket_key_enabled
aws_s3_kms_key_id = var.aws_s3_kms_key_id
Expand All @@ -26,3 +20,14 @@ module "render" {
render_cronjob_plan = var.render_cronjob_plan
render_cronjob_schedule = var.render_cronjob_schedule
}

module "aws" {
source = "./modules/aws"

aws_s3_bucket_name = var.aws_s3_bucket_name
aws_iam_role_name = var.aws_iam_role_name
aws_s3_use_kms = var.aws_s3_use_kms
render_deployment_workspace_id = var.render_deployment_workspace_id
aws_oidc_provider_arn = var.aws_oidc_provider_arn
render_cron_job_service_id = module.render.cron_job_service_id
}
50 changes: 40 additions & 10 deletions terraform/modules/aws/iam.tf
Original file line number Diff line number Diff line change
@@ -1,18 +1,48 @@

resource "aws_iam_user" "log_processor" {
name = var.aws_iam_user_name
locals {
issuer_host = "oidc.render.com"
issuer_url = "https://${local.issuer_host}/${var.render_deployment_workspace_id}"
create_oidc_provider = var.aws_oidc_provider_arn == ""
oidc_provider_arn = var.aws_oidc_provider_arn != "" ? var.aws_oidc_provider_arn : aws_iam_openid_connect_provider.render[0].arn
}

resource "aws_iam_access_key" "log_processor" {
user = aws_iam_user.log_processor.name
resource "aws_iam_openid_connect_provider" "render" {
count = local.create_oidc_provider ? 1 : 0

url = local.issuer_url
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = []
}

data "aws_iam_policy_document" "assume_role_with_oidc" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]

principals {
type = "Federated"
identifiers = [local.oidc_provider_arn]
}

condition {
test = "StringEquals"
variable = "${local.issuer_host}/${var.render_deployment_workspace_id}:aud"
values = ["sts.amazonaws.com"]
}

condition {
test = "StringLike"
variable = "${local.issuer_host}/${var.render_deployment_workspace_id}:sub"
values = ["workspace:${var.render_deployment_workspace_id}:environment:*:service:${var.render_cron_job_service_id}"]
}
}
}

output "aws_access_key" {
value = aws_iam_access_key.log_processor.id
sensitive = true
resource "aws_iam_role" "log_processor" {
name = var.aws_iam_role_name
assume_role_policy = data.aws_iam_policy_document.assume_role_with_oidc.json
}

output "aws_secret_access_key" {
value = aws_iam_access_key.log_processor.secret
sensitive = true
output "role_arn" {
value = aws_iam_role.log_processor.arn
}
2 changes: 1 addition & 1 deletion terraform/modules/aws/s3.tf
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ resource "aws_s3_bucket_policy" "render_audit_logs" {
Sid = "AllowAuditLogUpload",
Effect = "Allow",
Principal = {
AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:user/render-audit-log-processor"
AWS = aws_iam_role.log_processor.arn
},
Action = [
"s3:ListBucket",
Expand Down
16 changes: 15 additions & 1 deletion terraform/modules/aws/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ variable "aws_s3_bucket_name" {
default = ""
}

variable "aws_iam_user_name" {
variable "aws_iam_role_name" {
type = string
default = "render-audit-log-processor"
}
Expand All @@ -12,3 +12,17 @@ variable "aws_s3_use_kms" {
type = bool
default = false
}

variable "render_deployment_workspace_id" {
type = string
}

variable "aws_oidc_provider_arn" {
type = string
default = ""
}

variable "render_cron_job_service_id" {
type = string
description = "Render Cron Job service ID (crn-xxx); pins the OIDC trust policy sub claim."
}
15 changes: 13 additions & 2 deletions terraform/modules/render-audit-logs/render.tf
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@

locals {
computed_aws_role_arn = "arn:aws:iam::${var.aws_account_id}:role/${var.aws_iam_role_name}"
}

resource "render_cron_job" "render-audit-logs" {
name = var.render_cronjob_name
plan = var.render_cronjob_plan
Expand All @@ -20,8 +24,11 @@ resource "render_cron_job" "render-audit-logs" {

env_vars = {
"LOCAL" = { value = "false" },
"AWS_ACCESS_KEY_ID" = {value = var.aws_access_key},
"AWS_SECRET_ACCESS_KEY" = {value = var.aws_secret_access_key}
"AWS_ROLE_ARN" = { value = local.computed_aws_role_arn }
# To use an IAM user access key instead of OIDC, create the user manually,
# set these in the Render dashboard, and clear AWS_ROLE_ARN:
# "AWS_ACCESS_KEY_ID" = { value = "..." }
# "AWS_SECRET_ACCESS_KEY" = { value = "..." }
"AWS_REGION" = {value = var.aws_region}
"ORGANIZATION_ID" = {value = var.render_organization_id}
"WORKSPACE_IDS" = { value = join(",", var.render_workspace_ids) }
Expand All @@ -42,3 +49,7 @@ resource "render_project" "audit-logs" {
},
}
}

output "cron_job_service_id" {
value = render_cron_job.render-audit-logs.id
}
7 changes: 3 additions & 4 deletions terraform/modules/render-audit-logs/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,13 @@ variable "aws_s3_use_kms" {
default = false
}

variable "aws_access_key" {
variable "aws_account_id" {
type = string
sensitive = true
}

variable "aws_secret_access_key" {
variable "aws_iam_role_name" {
type = string
sensitive = true
default = "render-audit-log-processor"
}

variable "aws_region" {
Expand Down
14 changes: 13 additions & 1 deletion terraform/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,21 @@ variable "aws_s3_use_kms" {
default = false
}

variable "aws_iam_user_name" {
variable "aws_iam_role_name" {
type = string
default = "render-audit-log-processor"
description = "Name of the IAM role the Cron Job assumes via OIDC"
}

variable "render_deployment_workspace_id" {
type = string
description = "Render workspace ID (tea-xxx) where the Cron Job is deployed; used to build the OIDC issuer URL (oidc.render.com/<workspace-id>)"
}

variable "aws_oidc_provider_arn" {
type = string
default = ""
description = "ARN of an existing AWS IAM OIDC provider for Render. If empty, one is created."
}

variable "render_api_key" {
Expand Down
Loading