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
26 changes: 14 additions & 12 deletions .github/README-OIDC.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,15 @@ Example trust policy (replace placeholders):
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": [
"repo:<GITHUB_ORG_OR_USER>/<REPO_NAME>:ref:refs/heads/main",
"repo:<GITHUB_ORG_OR_USER>/<REPO_NAME>:pull_request"
]
"token.actions.githubusercontent.com:sub": "repo:<GITHUB_ORG_OR_USER>/<REPO_NAME>:ref:refs/heads/main"
}
}
}
]
}
```

Attach least-privilege IAM permissions for Terraform operations in `eu-west-1`.
Attach the IAM permissions used by the workflows.

You can start from:

Expand All @@ -62,6 +59,11 @@ chmod +x scripts/bootstrap-oidc.sh
In GitHub repository settings, add:

- `AWS_GITHUB_OIDC_ROLE_ARN` = IAM role ARN created above
- Optional: `PLANE_S3_ENDPOINT_URL` if you use S3-compatible storage instead of AWS S3

Optional repository variables include `AWS_REGION`, `TF_STATE_BUCKET`, `TF_STATE_BUCKET_PREFIX`, `TF_STATE_KEY`, `TF_LOCK_TABLE_NAME`, `HELM_RELEASE_NAME`, `K8S_NAMESPACE`, `PLANE_APP_HOST`, `POSTGRES_MODE`, `REDIS_MODE`, `RABBITMQ_MODE`, and `OBJECT_STORE_MODE`.

External service modes may require additional secrets. See `docs/service-modes.md`.

## 4. Protect production apply

Expand All @@ -71,17 +73,17 @@ The workflow already routes apply job to this environment.

## 5. Workflow behavior

- Pull requests: `init`, `validate`, `plan`
- Manual run (`workflow_dispatch`): choose `plan` or `apply`

## 6. RDS password handling
- Pull requests and pushes to `main`: secret-free `CI` workflow only.
- Manual `Terraform Bootstrap` run (`workflow_dispatch`): choose `plan` or `apply`.
- Manual `Terraform Prod` run (`workflow_dispatch`): choose `plan` or `apply`.
- Production apply is manual; `push` does not auto-apply infrastructure.

RDS master password is generated by Terraform using `random_password` and stored in AWS Secrets Manager.
## 6. Secret handling

No database password needs to be stored in GitHub secrets.
RDS, RabbitMQ, Redis, MinIO, and Plane app secrets are generated by Terraform and stored in AWS Secrets Manager for the modes that need workflow-generated credentials. External modes read only the explicitly configured GitHub secrets.

## Security notes

- Do not store AWS access keys in GitHub secrets.
- OIDC role trust policy must be repo-scoped.
- Keep Terraform state backend protected (S3 + DynamoDB lock is recommended as next step).
- Keep Terraform state backend protected (S3 + DynamoDB lock).
50 changes: 27 additions & 23 deletions .github/iam/terraform-prod-policy.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"secretsmanager:Describe*",
"secretsmanager:GetResourcePolicy",
"autoscaling:Describe*",
"sts:GetCallerIdentity",
"s3:Get*",
"s3:List*",
"dynamodb:DescribeTable",
Expand Down Expand Up @@ -53,6 +54,15 @@
"ec2:ReleaseAddress",
"ec2:CreateNatGateway",
"ec2:DeleteNatGateway",
"ec2:CreateNetworkInterface",
"ec2:DeleteNetworkInterface",
"ec2:CreateNetworkInterfacePermission",
"ec2:DeleteNetworkInterfacePermission",
"ec2:DetachNetworkInterface",
"ec2:ModifyNetworkInterfaceAttribute",
"ec2:CreateVpcEndpoint",
"ec2:DeleteVpcEndpoints",
"ec2:ModifyVpcEndpoint",
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
Expand Down Expand Up @@ -81,6 +91,7 @@
"eks:UntagResource",
"iam:CreateRole",
"iam:DeleteRole",
"iam:UpdateAssumeRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:CreatePolicy",
Expand Down Expand Up @@ -127,7 +138,8 @@
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem"
"dynamodb:UpdateItem",
"route53:AssociateVPCWithHostedZone"
],
"Resource": "*"
},
Expand All @@ -140,8 +152,6 @@
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestTag/Project": "plane",
"aws:RequestTag/Environment": "prod",
"aws:RequestTag/ManagedBy": "terraform"
}
}
Expand All @@ -154,15 +164,13 @@
"mq:UpdateBroker",
"mq:RebootBroker",
"mq:CreateTags",
"mq:RemoveTags"
"mq:DeleteTags"
],
"Resource": [
"arn:aws:mq:eu-west-1:925606812216:broker:plane-prod-rabbitmq:*"
"arn:aws:mq:*:*:broker:*-rabbitmq:*"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/Project": "plane",
"aws:ResourceTag/Environment": "prod",
"aws:ResourceTag/ManagedBy": "terraform"
}
}
Expand All @@ -176,8 +184,6 @@
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestTag/Project": "plane",
"aws:RequestTag/Environment": "prod",
"aws:RequestTag/ManagedBy": "terraform"
}
}
Expand Down Expand Up @@ -205,13 +211,11 @@
"elasticache:RemoveTagsFromResource"
],
"Resource": [
"arn:aws:elasticache:eu-west-1:925606812216:subnetgroup:plane-prod-redis-subnet-group",
"arn:aws:elasticache:eu-west-1:925606812216:replicationgroup:plane-prod-redis"
"arn:aws:elasticache:*:*:subnetgroup:*-redis-subnet-group",
"arn:aws:elasticache:*:*:replicationgroup:*-redis"
],
"Condition": {
"StringEquals": {
"aws:ResourceTag/Project": "plane",
"aws:ResourceTag/Environment": "prod",
"aws:ResourceTag/ManagedBy": "terraform"
}
}
Expand All @@ -221,9 +225,9 @@
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::925606812216:role/plane-prod-eks-cluster-role",
"arn:aws:iam::925606812216:role/plane-prod-eks-node-role",
"arn:aws:iam::925606812216:role/plane-prod-eks-ebs-csi-role"
"arn:aws:iam::*:role/*-eks-cluster-role",
"arn:aws:iam::*:role/*-eks-node-role",
"arn:aws:iam::*:role/*-eks-ebs-csi-role"
],
"Condition": {
"StringEquals": {
Expand All @@ -232,27 +236,27 @@
}
},
{
"Sid": "AllowCreateRdsServiceLinkedRole",
"Sid": "AllowCreateEksServiceLinkedRoles",
"Effect": "Allow",
"Action": "iam:CreateServiceLinkedRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:AWSServiceName": "rds.amazonaws.com"
"iam:AWSServiceName": [
"eks.amazonaws.com",
"eks-nodegroup.amazonaws.com"
]
}
}
},
{
"Sid": "AllowCreateEksServiceLinkedRoles",
"Sid": "AllowCreateRdsServiceLinkedRole",
"Effect": "Allow",
"Action": "iam:CreateServiceLinkedRole",
"Resource": "*",
"Condition": {
"StringEquals": {
"iam:AWSServiceName": [
"eks.amazonaws.com",
"eks-nodegroup.amazonaws.com"
]
"iam:AWSServiceName": "rds.amazonaws.com"
}
}
},
Expand Down
93 changes: 93 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: CI

on:
pull_request:
paths:
- 'terraform/**'
- 'helm/**'
- '.github/**'
- 'scripts/**'
- 'README.md'
- 'docs/**'
- '.gitignore'
push:
branches:
- main
paths:
- 'terraform/**'
- 'helm/**'
- '.github/**'
- 'scripts/**'
- 'README.md'
- 'docs/**'
- '.gitignore'

permissions:
contents: read

concurrency:
group: ci-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
static:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.8.5
terraform_wrapper: false

- name: Setup Helm
uses: azure/setup-helm@v4

- name: Validate IAM Policy JSON
run: python -m json.tool .github/iam/terraform-prod-policy.json > /dev/null

- name: Terraform Format Check
run: terraform fmt -check -recursive

- name: Terraform Init Bootstrap
run: terraform -chdir=terraform/environments/bootstrap init -backend=false -input=false

- name: Terraform Validate Bootstrap
run: terraform -chdir=terraform/environments/bootstrap validate

- name: Terraform Init Prod
run: terraform -chdir=terraform/environments/prod init -backend=false -input=false

- name: Terraform Validate Prod
run: terraform -chdir=terraform/environments/prod validate

- name: Helm Dependency Build
run: |
helm repo add makeplane https://helm.plane.so --force-update
helm dependency build helm/plane

- name: Helm Lint
run: |
helm lint helm/plane \
--values helm/plane/values/dev.yaml \
--set-string plane-ce.env.pgdb_remote_url="postgresql://ci:ci@example.invalid:5432/plane" \
--set-string plane-ce.env.remote_redis_url="rediss://:ci@example.invalid:6379/0" \
--set-string plane-ce.rabbitmq.external_rabbitmq_url="amqps://ci:ci@example.invalid:5671/" \
--set-string plane-ce.env.docstore_bucket="ci-docstore" \
--set-string plane-ce.env.secret_key="ci-template-secret-key" \
--set-string plane-ce.env.live_server_secret_key="ci-template-live-secret-key"

- name: Helm Template
run: |
helm template plane helm/plane \
--namespace plane \
--values helm/plane/values/dev.yaml \
--set-string plane-ce.env.pgdb_remote_url="postgresql://ci:ci@example.invalid:5432/plane" \
--set-string plane-ce.env.remote_redis_url="rediss://:ci@example.invalid:6379/0" \
--set-string plane-ce.rabbitmq.external_rabbitmq_url="amqps://ci:ci@example.invalid:5671/" \
--set-string plane-ce.env.docstore_bucket="ci-docstore" \
--set-string plane-ce.env.secret_key="ci-template-secret-key" \
--set-string plane-ce.env.live_server_secret_key="ci-template-live-secret-key" \
> /dev/null
Loading
Loading