Leaked credentials in CI/CD workflows pose a significant security risk, potentially leading to unauthorized access, data breaches, and system compromises. This can disrupt development pipelines, undermine the integrity of software deployment processes, and ultimately damage an organization’s reputation and bottom line. Protecting sensitive information like API keys, database passwords, and other secrets within the CI/CD pipeline is crucial for maintaining a secure and reliable software delivery process.
Long-lived credentials, such as traditional usernames/passwords and tokens, present significant security risks. Today, we’ll discuss how transitioning to OpenID Connect (OIDC) can mitigate these risks and provide enhanced security and manageability.
These risks of Long-lived credentials:
- Expiration Management: Enforcing expiration for static credentials is challenging and can lead to workflow disruptions during renewal.
- Credential Leakage: Long-lived credentials are vulnerable to leaks, potentially remaining undetected for extended periods.
- Credential Reuse: Reusing credentials across environments significantly increases security risks.
What is OIDC?
OIDC extends OAuth 2.0 by adding an identity layer. While OAuth 2.0 is primarily designed for authorization (granting permissions to access resources), OIDC focuses on authentication — verifying the identity of a user and providing basic profile information.
OIDC introduces a JSON Web Token (JWT) called an ID Token, which contains claims about the authenticated user. This ID Token can be used by applications to verify the user’s identity.
Key Components of OIDC
- Relying Party (RP): The application or service requesting authentication.
- OpenID Provider (OP): The identity provider (e.g., Google, AWS Cognito) that authenticates users and issues tokens.
- ID Token: A signed JWT containing user identity information.
- Discovery Document: A JSON document provided by the OP, containing URLs and information about supported OIDC features.
How OIDC Works: Step-by-Step
1. Discover the OpenID Configuration
The relying party fetches the OpenID Provider’s discovery document, typically hosted at a standard URL:
https://<provider-domain>/.well-known/openid-configuration
2. Redirect to Authorization Endpoint
The relying party redirects the user to the OP’s authorization endpoint with the following parameters:
- client_id: Identifier for the relying party.
- redirect_uri: URL where the user is redirected after authentication.
- scope: Permissions requested (e.g.,
openid
,profile
,email
). - response_type: Type of response expected (e.g.,
code
for authorization code flow). - state: A random value to prevent CSRF attacks.
https://<provider-domain>/authorize?response_type=code&client_id=<client-id>&redirect_uri=<redirect-uri>&scope=openid%20profile&state=<random-string>
3. Authenticate User
The OP displays a login page, prompting the user to authenticate (e.g., username/password, biometrics). After successful authentication, the user is redirected back to the relying party.
4. Receive Authorization Code
The relying party receives an authorization code as a query parameter in the redirect URI. This code is short-lived and can be exchanged for tokens.
Example redirect URI:https://<redirect-uri>?code=<auth-code>&state=<random-string>
5. Exchange Code for Tokens
The relying party sends the authorization code to the OP’s token endpoint using a secure backchannel. This request must include:
- grant_type:
authorization_code
- code: The received authorization code.
- redirect_uri: Same as the one used in the authorization request.
- client_id and client_secret: To authenticate the relying party.
6. Receive Tokens
The OP responds with an access token, ID token, and optionally a refresh token. The ID token contains user identity information (e.g., name, email) and is signed using a JSON Web Key (JWK).
Example token response:
{
"access_token": "eyJhbGciOiJIUzI1...",
"id_token": "eyJhbGciOiJIUzI1...",
"expires_in": 3600,
"token_type": "Bearer"
}
7. Validate ID Token
The relying party validates the ID token by:
- Verifying its signature using the OP’s JWKs.
- Ensuring the
aud
(audience) claim matches the client ID. - Checking the
exp
(expiration) claim to ensure the token is still valid.
8. Authenticate User in the Application
After validating the ID token, the relying party extracts user claims (e.g., sub
, email
) and uses them to authenticate the user in the application.
9. Optional: Refresh Tokens
If a refresh token is issued, the relying party can request new access tokens without requiring user re-authentication.
Benefits of OIDC
- Interoperability: Standardized mechanism for authentication across providers.
- Enhanced Security: Built-in protections like CSRF prevention, token expiration, and secure key handling.
- User Experience: Simplifies user login with Single Sign-On (SSO).
- Scalability: Works well with modern, distributed architectures.
Terraform code
provider "aws" {
region = "us-east-1"
}
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = ["6938fd4d98bab03faadb97b34396831e3780aea1"]
}
data "aws_iam_policy_document" "github_oidc_assume_role_policy" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github_actions.arn]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:abhi-io/test-oidc-cicd:*"]
}
}
}
resource "aws_iam_role" "github_oidc_role" {
name = "github-oidc-role"
assume_role_policy = data.aws_iam_policy_document.github_oidc_assume_role_policy.json
}
data "aws_iam_policy_document" "s3_access_policy" {
statement {
actions = ["s3:*"]
resources = ["arn:aws:s3:::my-test-s3-q98u211u9qw2/*"]
}
}
resource "aws_iam_policy" "github_oidc_s3_policy" {
name = "github-oidc-s3-policy"
policy = data.aws_iam_policy_document.s3_access_policy.json
}
resource "aws_iam_role_policy_attachment" "github_oidc_policy_attachment" {
role = aws_iam_role.github_oidc_role.name
policy_arn = aws_iam_policy.github_oidc_s3_policy.arn
}