Skip to content

Admin Account Setup

Already completed. This was done during initial CampusCore setup. You only need this if you're migrating to a new admin AWS account, re-creating the OIDC trust from scratch, or updating the admin stack.

How the CampusCore admin AWS account is set up - the central account that holds Terraform state and authenticates GitHub Actions deployments.


What the Admin Account Does

The admin account is a lightweight coordination layer. It does NOT host any client infrastructure. It has three jobs:

  1. Stores Terraform state - One S3 bucket per client (campuscore-tfstate-{client}) holds all infrastructure state files.
  2. OIDC trust anchor - GitHub Actions authenticates to this account first via OIDC, then chains into each client's deploy role.
  3. Serves the deploy-role template - A private bucket holds deploy-role.yaml for the console link in Deploy the CampusCore IAM Role, readable only by the client accounts we list.
GitHub Actions  ──OIDC──>  Admin Account  ──AssumeRole──>  Client Account
                           (state bucket)                  (ECS, RDS, etc.)

One CloudFormation stack, CampusCore-Admin-Role, built from infrastructure/deploy-roles/admin-oidc-role.yaml, holds everything in the admin account:

Resource What it is for
GitHubOIDCProvider The GitHub OIDC identity provider. Every workflow that reaches AWS signs in through it.
CampusCore-Admin-Role The deploy workflows' role: Terraform state, Route53, and the chain into each client's deploy role.
CampusCore-Findings-Reporter-Role The daily security findings report's role. It trusts only security-findings-report.yml on main, run in the vsu-troy-pilot environment, and it may only assume the findings reader role in the pilot account.
CampusCore-Template-Publisher-Role The role of publish-deploy-role-template.yml. It trusts only that workflow on main, and it may only upload deploy-role.yaml to the template bucket.
campuscore-deploy-role-template-<admin account id> The template bucket: versioned, encrypted, all public access blocked, HTTPS only. Only the accounts in TemplateReaderAccountIds may read deploy-role.yaml.

Permissions Summary

The admin role (CampusCore-Admin-Role) needs only these permissions:

Category Actions Scope
S3 - Create buckets CreateBucket, PutBucketVersioning, PutBucketEncryption, PutPublicAccessBlock, GetBucketLocation arn:aws:s3:::campuscore-tfstate-*
S3 - Read/write state GetObject, PutObject, DeleteObject arn:aws:s3:::campuscore-tfstate-*/tfstate/*
S3 - List/check buckets HeadBucket, ListBucket arn:aws:s3:::campuscore-tfstate-*
STS - Role chaining AssumeRole, TagSession arn:aws:iam::*:role/CampusCore-Deploy-Role
STS - Identity check GetCallerIdentity *
Route53 - DNS management ChangeResourceRecordSets, GetHostedZone, ListResourceRecordSets arn:aws:route53:::hostedzone/*
Route53 - Change tracking GetChange arn:aws:route53:::change/*
Route53 - List zones ListHostedZones *

Route53 permissions are used to manage the campuscoreai.com hosted zone - creating subdomain CNAMEs and ACM certificate validation records for each deployment. No EC2, RDS, ECS, or other compute permissions - all infrastructure lives in client accounts.


Deploy the Admin Role CloudFormation Stack

This section is for creating the stack in a new admin account. To change the live stack, use Updating the Stack instead.

Log in to the admin AWS account and deploy the template:

aws cloudformation deploy \
  --template-file infrastructure/deploy-roles/admin-oidc-role.yaml \
  --stack-name CampusCore-Admin-Role \
  --parameter-overrides \
    GitHubOrg=CampusCoreAI \
    GitHubRepo=campuscore \
    CreateOIDCProvider=true \
    TemplateReaderAccountIds=<first client account id> \
  --capabilities CAPABILITY_NAMED_IAM \
  --no-fail-on-empty-changeset \
  --profile campuscore-admin

Parameters:

Parameter Description Example
GitHubOrg GitHub org or username, case-sensitive CampusCoreAI
GitHubRepo Repository name, case-sensitive campuscore
CreateOIDCProvider true makes this stack create and own the GitHub OIDC provider. Use false only when another stack in the account already owns it. true
StateBucketPrefix S3 bucket prefix for state buckets (default: campuscore-tfstate-) campuscore-tfstate-
TemplateReaderAccountIds Comma-separated 12-digit IDs of the client accounts that may read deploy-role.yaml from the template bucket. It has no default. Add each new client before sending the console link (Step 03). 591563075290

Upload the template once

The template bucket starts empty, and publish-deploy-role-template.yml fills it only on a push to main that changes deploy-role.yaml or the workflow itself, or when you run it by hand. Until it has run once, upload the file yourself from an up-to-date main checkout, or the console link finds no template:

aws s3 cp infrastructure/deploy-roles/deploy-role.yaml \
  s3://campuscore-deploy-role-template-<admin account id>/deploy-role.yaml \
  --content-type text/yaml \
  --profile campuscore-admin

Running the workflow by hand from main (gh workflow run publish-deploy-role-template.yml --ref main) does the same once the TEMPLATE_PUBLISHER_ROLE_ARN secret below is set.


Stack Outputs and Where They Go

Each output feeds one GitHub value, which a workflow reads. Read any of them with the stack's outputs:

aws cloudformation describe-stacks \
  --stack-name CampusCore-Admin-Role \
  --query 'Stacks[0].Outputs[].[OutputKey,OutputValue]' \
  --output table \
  --profile campuscore-admin
Output GitHub value Read by
AdminRoleArn Repo secret ADMIN_AWS_ROLE_ARN deploy-aws.yml and the other deploy workflows. It is also the admin ARN hardcoded in deploy-role.yaml's trust policy.
FindingsReporterRoleArn Repo secret FINDINGS_REPORTER_ROLE_ARN, and the ReporterRoleArn parameter of the findings reader stack below security-findings-report.yml
TemplatePublisherRoleArn Repo secret TEMPLATE_PUBLISHER_ROLE_ARN publish-deploy-role-template.yml
DeployRoleTemplateUrl None. It is the template URL in the console link on the deploy-role how-to. docs/product/how-to/deploy-the-iam-role.md
OIDCProviderArn None. It exists only when this stack owns the provider. -

Example AdminRoleArn: arn:aws:iam::111111111111:role/CampusCore-Admin-Role. It is used in the Repo & Pipeline Setup and when onboarding each client in Step 03 - Deployment.


Updating the Stack

To change a parameter or deploy a new version of the template, pass only the parameters you are changing. aws cloudformation deploy keeps the current value of every parameter it is not given, so an update never needs the full list.

aws cloudformation deploy \
  --template-file infrastructure/deploy-roles/admin-oidc-role.yaml \
  --stack-name CampusCore-Admin-Role \
  --parameter-overrides TemplateReaderAccountIds=<current list>,<new account id> \
  --capabilities CAPABILITY_NAMED_IAM \
  --no-fail-on-empty-changeset \
  --profile campuscore-admin

Leave out --parameter-overrides entirely to deploy a new template version with every parameter unchanged. Deploy from an up-to-date main checkout, because the template you pass replaces the live one.

Important: Never pass CreateOIDCProvider=false on an update. The live stack has CreateOIDCProvider=true and owns the GitHub OIDC provider, so false deletes the provider, and every workflow that reaches AWS then fails to sign in. A list parameter such as TemplateReaderAccountIds is replaced as a whole, so read its current value first, as Step 03 shows.


The Findings Reader Stack (Pilot Account Only)

The daily security findings report reads the vsu-troy-pilot account through a read-only role in that account, CampusCore-Findings-Reader-Role. It lives in its own stack, built from infrastructure/deploy-roles/findings-reader-role.yaml, and it is never deployed to a client account. The reporter role's policy names this role in the pilot account only, so a reader stack in any other account would never be reached.

Create it once, with a profile for the pilot account, in the region the pilot deploys to:

aws cloudformation deploy \
  --template-file infrastructure/deploy-roles/findings-reader-role.yaml \
  --stack-name CampusCore-Findings-Reader-Role \
  --parameter-overrides ReporterRoleArn=<FindingsReporterRoleArn output> \
  --capabilities CAPABILITY_NAMED_IAM \
  --no-fail-on-empty-changeset \
  --region us-east-1 \
  --profile vsu-pilot

aws cloudformation update-termination-protection \
  --enable-termination-protection \
  --stack-name CampusCore-Findings-Reader-Role \
  --region us-east-1 \
  --profile vsu-pilot

Termination protection matches the deploy-role stack and keeps Security Hub CloudFormation.3 passing. The stack has no service role, so it fails CloudFormation.4, which scripts/security_findings_known.json records as known.

Then set its FindingsReaderRoleArn output as the FINDINGS_READER_ROLE_ARN variable on the vsu-troy-pilot GitHub environment:

gh variable set FINDINGS_READER_ROLE_ARN --env vsu-troy-pilot \
  --body "arn:aws:iam::591563075290:role/CampusCore-Findings-Reader-Role"

Security Notes

  • The admin role follows least privilege - it can manage S3 state buckets, assume client deploy roles, and manage Route53 records in the campuscoreai.com hosted zone. No compute, database, or networking permissions.
  • S3 bucket access is scoped to the campuscore-tfstate-* prefix only.
  • State file access is further scoped to the tfstate/* key prefix inside those buckets.
  • Route53 access is used only for subdomain provisioning ({env}.campuscoreai.com) and ACM certificate validation records.
  • The AssumeRole permission targets CampusCore-Deploy-Role by name. Each client's deploy role independently controls who can assume it via its trust policy.
  • The admin role's OIDC trust is scoped to your specific GitHub org/repo - no other repos can assume this role.
  • The findings reporter and template publisher roles each trust one workflow file on main, so a run from any other branch or workflow fails at the AWS sign-in step.
  • The template bucket allows reads only to the listed client accounts. Of the GitHub workflow roles, only the publisher role may write deploy-role.yaml, because clients create their deploy role from it with CAPABILITY_NAMED_IAM.
  • The admin role has no long-lived credentials. GitHub Actions authenticates via OIDC (short-lived tokens). The only time you use static credentials is when running create-new-client-tf-state-bucket.sh locally.

See also: Repo & Pipeline Setup