Why AWS SSO? Why OIDC?
7 min readWhile building a demo coffee app you realise that the infrastructure-as-code needs appropriate authentication & authorisation to make changes securely. I’ve set this up in previous jobs but have been on a travel break since then, and the muscle memory needs a reminder.
So, I’m refreshing my understanding and reflecting on it here.
Important terminology and tools
An important distiction is the difference between Authentication and Authorisation. Authentication asks “are you who you say you are”, while Authorisation says “I know you, but are you allowed to do what you’re trying to do”.
Infrastructure-as-code (IaC) is self-explanatory, it allows you to codify your infrastructure changes. Letting you track infra changes over time, and efficiently keep the infra actually deployed in line with your intentions. When there is drift between intentions and actual, IaC helps it realign.
Terraform is a great tool for IAC, and is the focus for this reflection.
There are many other tools that help with IaC including CloudFormation and Ansible. CloudFormation is AWS’s solution for the IaC giving declarative provisioning, but its locked to AWS. Ansible is more focused on configuration management and orchestration once infrastructure exists, though there is some overlap with IaC.
CI/CD is Continuous Integration / Continuous Delivery. It is a mindset with many tools that helps our code build, test, and deploy safely for our users. GitHub Actions our CI/CD tool of choice. There are others like TeamCity, Circle CI, Jenkins, Octopus Deploy, GitLab CI etc.
When mentioning MacBook it could be any developer machine enabling you to make infra changes by running Terraform.
Given all of this, there are two main use cases:
- Human to machine - Connecting MacBook to AWS
- Machine to machine - Connecting GitHub Actions to AWS
Both use cases need to authenticate and authorise, but they do it using different methods. We’ll explore that difference in the upcoming sections.
Human to machine
When running Terraform from your laptop, the industry standard is to authenticate with AWS SSO (rebranded as AWS IAM Identity Center in 2022, though the CLI commands remain the same).
From the terminal you run an aws cli command that opens your browser, asks you to log in using your identity provider (or aws creds), and grabs a short-lived credential for use during your AWS terminal session. It normally expires after 8 hours, but this is configurable. Terraform picks up that short-lived credential and uses it to configure the infrastructure however you’ve defined.
# day to day example (assumes sso roles are setup)
aws sso login --profile terraform
export AWS_PROFILE=terraform
terraform apply
These commands work because there’s a human in the loop. Developers can login to AWS, click a button, and complete an MFA prompt. It’s verifiable that you are the person at the keyboard.
This is not possible the moment you’re out of the loop.
Machine to machine
A GitHub Actions CI/CD pipeline can’t easily open a browser or tap an allow button on your phone. It needs to authenticate to AWS without a person in the loop, securely, and without any of the interactive trust mechanisms that make SSO work locally.
The old answer was to generate an AWS access key pair, paste it into GitHub as a secret, and let the pipeline use that to authenticate with AWS. This still works, but it’s a liability and no longer recommended. Long-lived access keys, if leaked, can grant authentication to whoever gets them. Unfortunately, because infra work requires a broader set of permissions, losing these keys is a bigger security risk.
Long-lived keys should also be rotated frequently (i.e. rotating means to update the keys). That requires additional reminder scheduling, and if the key is used in many places, the work to update might require a bit more effort.
The better answer is Open ID Connect (OIDC). To understand OIDC, you have to understand JWTs first.
JWTs
JWT stands for JSON Web Token. It’s a compact, self-contained token for passing claims between parties.
A claim is a key-value assertion about a subject, e.g. "sub": "user_123" or "email": "john@example.com". The issuer party (AWS or GitHub) is saying: I assert these properties are true about the subject of this token (and that it is valid for the stated audience and time window)
What might be at first surprising, is that a signed JWT isn’t encrypted and anyone can read it. Paste a JWT string into jwt.io and you’ll see everything inside (with real tokens, keep them out of public tools!).
The point of JWT tokens isn’t secrecy, it’s integrity. If you trust the issuer’s public key, and the signature checks out, then you can trust the claims.
Think of it like a driver’s licence, it’s contents are not a secret (anyone who physically holds it has the info), but it’s validity is trusted because it was created by a trusted authority. Other parties (police, security, the supermarket cashier) recognise it and trust what it claims.
A JWT is a string with three parts separated by dots. Header, payload, and signature.
<header>.<payload>.<signature>
The header says how it was signed. The payload is a set of claims or facts about whoever the token is describing. The signature is the issuer’s cryptographic stamp.
E.g.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjdXJpb3VzX3JlYWRlciIsIm5hbWUiOiJZb3UsIHRoZSBjdXJpb3VzIG9uZSIsIm1lc3NhZ2UiOiJUaGV5IHNheSBjdXJpb3NpdHkga2lsbGVkIHRoZSBjYXQsIGJ1dCBpdCB3b24ndCBraWxsIHlvdS4gU3RheSBjdXJpb3VzLiIsImlhdCI6MTc0NjY2MjQwMCwiZXhwIjo5OTk5OTk5OTk5fQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
You can read more detail here: https://www.jwt.io/introduction#what-is-json-web-token
OIDC
OIDC (OpenID Connect) is how JWTs get used for authentication. An identity provider mints a signed JWT about who someone is or what it’s doing, and a relying party verifies the token and decides what to do.
Back to the analogy of a JWT and a drivers license. The issuing authority is whoever grants drivers licenses, in New Zealand this would be NZ Transport Agency (NZTA or Waka Kotahi). The relying party, again, could be police, security, or the supermarket cashier.
For your CI/CD, GitHub is the identity provider and AWS is the relying party. GitHub publishes its public keys at a well-known URL. AWS knows where to find them and when the workflow pipeline runs, GitHub mints a JWT describing that specific job (the repo, git branch, the workflow file) and signs it. The pipeline hands the token to AWS, AWS verifies the signature, reads the claims, and decides whether to issue short-lived credentials.
There’s no shared secret and no pre-arranged handshake. You declare in your AWS account that you trust GitHub’s issuer URL and from that moment, AWS knows how to verify any token GitHub signs.
This grants you authentication, and next it’s important to look at the policies that grant authorisation to make changes.
The trust policy
With the authentication and JWT verification mostly handled by AWS & GitHub, you can focus more on deciding which GitHub claims you’ll accept.
For example, every JWT GitHub mints contains a sub claim, i.e. the subject
repo:my-org/my-repo:ref:refs/heads/main
That string identifies exactly which repo, branch, or trigger produced the token. Your trust policy in AWS is a set of conditions on those claims. So if you pin the sub claim to a specific repo and your main branch (like the above example), then only that combination can assume the role.
Set up the AWS trust policy too loosely and you replace one risk with another. The biggest mistake is wildcarding the subject repo:*, which means that any repository on GitHub could potentially assume your role. The fix is to only give access to specific repositories that need it, following least privilege access. Most tutorials highlight this point too.
Using GitHub environments
Many tutorials suggest the use of GitHub Environments for production deploys. So you can pin the trust policy to repo:my-org/my-repo:environment:production. You then set up your repo to require manual approval on production deployments, thus adding another layer of protection. Now even a workflow on main can’t deploy until a human clicks a button.
For an enterprise team, this makes sense. Multiple people merging code, the reviewer and the deployer might not be the same person. So the approval gate gives you a real second pair of eyes.
For a solo project without actual customers it’s optional, but worth knowing the pattern exists for when the team grows.
Relearning
Coming back to OIDC after a year of travel, I noticed new details since the first time round. Especially in terms of trust policies, and why the sub claim specificity matters. Going back to first principles about JWTs and OIDC before touching any config gave that stronger mental model.
If you want to see how someone might configure all of this, check out my repositories here:
- AWS Foundation SSO & OIDC setup: https://github.com/wjkw1/aws-foundations
- Demo Application: https://github.com/wjkw1/devops-profile-coffee-card-app-demo