Skip to content
Closed
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
107 changes: 107 additions & 0 deletions terraform/circleci-ossec-publisher/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# CircleCI OSSEC publisher role

Terraform configuration for the CircleCI OIDC role that publishes OSSEC Debian
packages to:

```text
s3://td-ossec-agents/installer/
```

The AWS account is restricted by the provider configuration to account
`113345163213`.

## What this creates

- A CircleCI organization-specific IAM OIDC provider.
- IAM role `CircleCI-OSSEC-Packages-Publisher`.
- An inline policy that permits package uploads only under:

```text
s3://td-ossec-agents/installer/*
```

The role cannot delete objects and has no access to objects outside the
`installer/` prefix.

The trust policy is organization-wide because the current requirement selected
organization-only trust. Any project in the CircleCI organization can assume
this role with a valid CircleCI OIDC token. Restricting the role to a specific
CircleCI project is recommended for production.

## Prerequisites

The operator running Terraform needs AWS credentials with permission to manage:

- `iam:CreateOpenIDConnectProvider`
- `iam:CreateRole`
- `iam:PutRolePolicy`
- `iam:TagOpenIDConnectProvider`
- `iam:TagRole`

The CircleCI organization ID is required. It is used in all three places:

1. The OIDC issuer URL: `https://oidc.circleci.com/org/<org-id>`
2. The OIDC provider audience/client ID.
3. The IAM trust-policy `aud` condition.

## Apply

From this directory:

```bash
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars with the real CircleCI organization ID and S3 region.

terraform init
terraform fmt -recursive
terraform validate
terraform plan -out=tfplan
terraform apply tfplan
```

Use an AWS profile explicitly when appropriate:

```bash
AWS_PROFILE=<profile> terraform plan
AWS_PROFILE=<profile> terraform apply tfplan
```

The account guard in `versions.tf` prevents Terraform from applying to an AWS
account other than `113345163213`.

## CircleCI context values

After applying, configure the `ossec-package-publisher` context with:

```text
AWS_ROLE_ARN = <terraform output -raw role_arn>
AWS_REGION = <region of td-ossec-agents>
S3_BUCKET = td-ossec-agents
S3_PREFIX = installer
```

Do not configure long-lived AWS access keys. The CircleCI `aws-cli` orb in the
main pipeline assumes this role through OIDC.

## Existing OIDC provider

If the CircleCI OIDC provider already exists in account `113345163213`, do not
apply this configuration unchanged. Import the existing provider into the
Terraform state, or adapt the module to reference the existing provider, to
avoid an `EntityAlreadyExists` error.

## Safer project restriction

To restrict this role to one CircleCI project, add a second trust-policy
condition using the documented claim namespace:

```hcl
condition {
test = "StringEquals"
variable = "${local.circleci_claim_namespace}:oidc.circleci.com/project-id"
values = [var.circleci_project_id]
}
```

That requires adding a `circleci_project_id` variable and is preferable once
the CircleCI project UUID is available.
75 changes: 75 additions & 0 deletions terraform/circleci-ossec-publisher/iam.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
data "aws_iam_policy_document" "circleci_assume_role" {
statement {
sid = "CircleCIOrganizationOIDC"
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]

principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.circleci.arn]
}

# Organization-only trust, as requested. Every project in this CircleCI
# organization can assume the role if it obtains a valid token.
condition {
test = "StringEquals"
variable = "${local.circleci_claim_namespace}:aud"
values = [var.circleci_org_id]
}
}
}

resource "aws_iam_role" "circleci" {
name = var.role_name
description = "CircleCI OIDC role for publishing OSSEC Debian packages"
assume_role_policy = data.aws_iam_policy_document.circleci_assume_role.json
max_session_duration = 3600
tags = var.tags
}

data "aws_iam_policy_document" "s3_upload" {
statement {
sid = "GetArtifactBucketLocation"
effect = "Allow"
actions = [
"s3:GetBucketLocation"
]
resources = ["arn:aws:s3:::${var.artifact_bucket}"]
}

statement {
sid = "ListArtifactPrefix"
effect = "Allow"
actions = [
"s3:ListBucket",
"s3:ListBucketMultipartUploads"
]
resources = ["arn:aws:s3:::${var.artifact_bucket}"]

condition {
test = "StringLike"
variable = "s3:prefix"
values = [
local.normalized_artifact_prefix,
"${local.normalized_artifact_prefix}/*"
]
}
}

statement {
sid = "WriteArtifactObjects"
effect = "Allow"
actions = [
"s3:AbortMultipartUpload",
"s3:ListMultipartUploadParts",
"s3:PutObject"
]
resources = [local.artifact_object_arn]
}
}

resource "aws_iam_role_policy" "s3_upload" {
name = "OSSECInstallerArtifactUpload"
role = aws_iam_role.circleci.id
policy = data.aws_iam_policy_document.s3_upload.json
}
6 changes: 6 additions & 0 deletions terraform/circleci-ossec-publisher/locals.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
locals {
normalized_artifact_prefix = trim(var.artifact_prefix, "/")
circleci_issuer = "https://oidc.circleci.com/org/${var.circleci_org_id}"
circleci_claim_namespace = "oidc.circleci.com/org/${var.circleci_org_id}"
artifact_object_arn = "arn:aws:s3:::${var.artifact_bucket}/${local.normalized_artifact_prefix}/*"
}
5 changes: 5 additions & 0 deletions terraform/circleci-ossec-publisher/oidc.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resource "aws_iam_openid_connect_provider" "circleci" {
url = local.circleci_issuer
client_id_list = [var.circleci_org_id]
tags = var.tags
}
19 changes: 19 additions & 0 deletions terraform/circleci-ossec-publisher/outputs.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
output "role_name" {
description = "IAM role name for CircleCI package publishing."
value = aws_iam_role.circleci.name
}

output "role_arn" {
description = "Set this as AWS_ROLE_ARN in the CircleCI context."
value = aws_iam_role.circleci.arn
}

output "oidc_provider_arn" {
description = "CircleCI IAM OIDC provider ARN."
value = aws_iam_openid_connect_provider.circleci.arn
}

output "artifact_prefix" {
description = "S3 prefix writable by the role."
value = "s3://${var.artifact_bucket}/${local.normalized_artifact_prefix}/"
}
7 changes: 7 additions & 0 deletions terraform/circleci-ossec-publisher/terraform.tfvars
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
aws_account_id = "113345163213"
aws_region = "us-west-2"
circleci_org_id = "b07c248e-976c-4d6e-bc2b-44101ea4339a"

artifact_bucket = "td-ossec-agents"
artifact_prefix = "installer"
role_name = "CircleCI-OSSEC-Packages-Publisher"
58 changes: 58 additions & 0 deletions terraform/circleci-ossec-publisher/variables.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
variable "aws_account_id" {
description = "AWS account in which the IAM role and OIDC provider are managed."
type = string
default = "113345163213"

validation {
condition = can(regex("^[0-9]{12}$", var.aws_account_id))
error_message = "aws_account_id must be a 12-digit AWS account ID."
}
}

variable "aws_region" {
description = "AWS region containing the td-ossec-agents bucket."
type = string
}

variable "circleci_org_id" {
description = "CircleCI organization UUID used by the organization-specific OIDC issuer and audience."
type = string

validation {
condition = length(trimspace(var.circleci_org_id)) > 0
error_message = "circleci_org_id must be provided."
}
}

variable "role_name" {
description = "IAM role assumed by CircleCI through OIDC."
type = string
default = "CircleCI-OSSEC-Packages-Publisher"
}

variable "artifact_bucket" {
description = "S3 bucket that receives OSSEC package artifacts."
type = string
default = "td-ossec-agents"
}

variable "artifact_prefix" {
description = "S3 prefix that CircleCI may write to."
type = string
default = "installer"

validation {
condition = length(trim(var.artifact_prefix, "/")) > 0
error_message = "artifact_prefix must not be empty."
}
}

variable "tags" {
description = "Additional tags applied to the IAM role and OIDC provider."
type = map(string)
default = {
ManagedBy = "Terraform"
Service = "OSSEC"
Purpose = "CircleCI package publishing"
}
}
15 changes: 15 additions & 0 deletions terraform/circleci-ossec-publisher/versions.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
terraform {
required_version = ">= 1.6.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0, < 7.0"
}
}
}

provider "aws" {
region = var.aws_region
allowed_account_ids = [var.aws_account_id]
}