Implementing access control policies in CI/CD pipelines
Developer Advocate
Imagine yourself in this situation: You are a motivated and skilled DevOps or DevEx engineer. You have a plan to implement automated, complete CI/CD pipelines. You know how to do it, and you know how the extra productivity and automation will benefit your team and the whole company. But the project is never approved, because of security concerns.
Many organizations, especially those in regulated industries, have strict requirements for releasing their software, and rightfully so. Who can trigger a release, and when, under what conditions, and what specific checks are required before release can go ahead must be tightly controlled, auditable, and reversible.
These kinds of complicated processes often require a dedicated delivery manager to collect and present all relevant feature briefs, test data, security assessment, and rollback plans to a committee of decision makers in the company. That group can give the delivery manager the green light to deploy, often also on a specific schedule.
There is another approach. You can set up effective and fully automated CI/CD pipelines that include all the checks a human delivery manager and approval committee can complete. This tutorial will show you ways to create fine grained access control for your pipelines. You can use these methods for strictly regulated internal company projects for the enterprise, as well as for popular open source projects.
What you will learn
In this tutorial, you will learn how to implement access control policies in CI/CD pipelines using hands-on examples. By the end, you’ll have set up protected branches on GitHub with modern repository rulesets, configured CircleCI contexts with restricted access, implemented approval jobs in your CI/CD pipeline, and set up OIDC authentication for secure cloud deployments. You’ll also understand access control best practices for both enterprise and open-source projects.
We’ll use a practical Node.js web API example to demonstrate real-world access control scenarios.
Prerequisites
This tutorial assumes you have experience with CI/CD pipelines, and ideally with CircleCI. To implement the steps covered you will also need admin access to your GitHub and CircleCI organizations (and accounts for both services).
If you do not have administrator access to a GitHub organization, you can create a free one in GitHub and use it with a free CircleCI account.
Setting up the demo project
Let’s create a simple Node.js web API to demonstrate access control policies. Create a new repository called secure-web-api in your GitHub organization.
Project setup
Clone your new repository and add the following files:
Create a package.json file:
{
"name": "secure-web-api",
"version": "1.0.0",
"scripts": {
"start": "node server.js",
"test": "jest"
},
"dependencies": {
"express": "^4.18.0"
},
"devDependencies": {
"jest": "^29.0.0",
"supertest": "^6.3.0"
}
}
Create a server.js file for your Express API:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/health", (req, res) => {
res.json({ status: "healthy", timestamp: new Date().toISOString() });
});
app.get("/api/users", (req, res) => {
res.json({ users: ["alice", "bob", "charlie"] });
});
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
module.exports = app;
Create a basic test file test/api.test.js:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/health", (req, res) => {
res.json({ status: "healthy", timestamp: new Date().toISOString() });
});
app.get("/api/users", (req, res) => {
res.json({ users: ["alice", "bob", "charlie"] });
});
// Only start the server if this file is run directly, not when imported
if (require.main === module) {
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
}
module.exports = app;
Add this initial .circleci/config.yml file:
version: 2.1
jobs:
test:
docker:
- image: cimg/node:18.20
steps:
- checkout
- run: npm install
- run: npm test
security-scan:
docker:
- image: cimg/node:18.20
steps:
- checkout
- run: npm install
- run: npm audit
deploy-dev:
docker:
- image: cimg/node:18.20
steps:
- run: echo "Deploying to development environment..."
deploy-prod:
docker:
- image: cimg/node:18.20
steps:
- run: echo "Deploying to production environment..."
workflows:
build-test-deploy:
jobs:
- test
- security-scan:
context: security
- deploy-dev:
context: deployment-dev
requires:
- test
- security-scan
filters:
branches:
only: main
- approve-for-prod:
type: approval
requires:
- deploy-dev
- deploy-prod:
context: release-prod
requires:
- approve-for-prod
Commit and push these files to your repository:
git add .
git commit -m "Initial setup with access control configuration"
git push origin main
Access control in CI/CD pipelines for enterprise projects
For an enterprise organization it is vital that existing checks are followed and communicated well. The process often includes manual checks of supporting documentation and the potential impact to business before a release can continue.
I will guide you through implementing a pipeline that executes a number of verification steps automatically, and continues with deployment to production only after the dedicated delivery manager manually approves the process.
To implement access control you will need to configure protected branches in GitHub, set up security groups in GitHub (or establish conceptual roles for individual accounts), create contexts in CircleCI, and implement approval jobs in CircleCI.
Protected branches with repository rulesets
Setting up a protected branch is the first step to setting up access control. A protected branch prohibits team members from pushing code directly to that branch, and instead forces all changes to go through the pull request process.
GitHub now uses repository rulesets for more flexible branch protection. Navigate to your repository settings and click “Rules” then “Rulesets” in the left sidebar. Click “New ruleset” and select “New branch ruleset”.
Configure the ruleset with these settings:
- Ruleset Name: Enter “Main Branch Protection”
- Enforcement status: Set to “Active”
- Target branches: Click “Add target” and select “Include by pattern”, then enter
mainas the branch pattern
Under “Branch rules”, enable these protections:
- “Require a pull request before merging” with at least 1 required approval
- “Require status checks to pass” and ensure branches are up to date before merging
- “Block force pushes”
Add the status check ci/circleci: test to ensure tests pass before merging.

Your main branch is now protected, ensuring any changes must go through pull requests with the required approvals and status checks.
Setting up security groups
Security groups in GitHub allow you to control who can access specific resources and perform certain actions. For enterprise organizations, you’ll typically have different roles: developers who can create branches and pull requests, development leads who can approve pull requests and merge changes, and delivery managers who can approve production deployments.
For GitHub organizations If you’re working in a GitHub organization, navigate to organization settings and create two teams:
- A
development-leadsteam for those who can approve pull requests. - A
delivery-managersteam for those who can approve production deployments.
Then configure team access in repository settings under Manage access by giving development-leads Write access and delivery-managers Maintain access.
For individual accounts If you’re using a personal GitHub account for this tutorial, you have a few options to work around the self-approval limitation:
- Disable PR approval requirement temporarily
- In your repository ruleset, uncheck “Require a pull request before merging”
- Also uncheck “Require status checks to pass” initially to avoid being blocked by any test failures
- Keep “Block force pushes” enabled to maintain some protection
- You can re-enable these protections once your CI pipeline is working correctly
- Allow repository administrators to bypass
- In your repository ruleset, check “Allow repository administrators to bypass”
- Keep the other protections enabled but bypass them as needed during setup
- As the repository owner, you can merge PRs even if tests fail initially
- Create a test organization
- Create a free GitHub organization and invite a secondary account
- This gives you a more realistic multi-person workflow for testing
For this tutorial, both Option 1 or 2 work well since the key learning objective is understanding how access control works in team environments.
Using CircleCI contexts
Contexts in CircleCI serve a double role. One role limits the scope of secrets shared with a job while the other controls access.
Only a job that has a context specified to it in a workflow is able to use the context’s secrets as environment variables. Contexts are shared across the entire organization in CircleCI, so they can be reused in multiple projects. You can set them up using the organization settings menu on the left hand side of the CircleCI dashboard.
For access control, you can specify security groups to any context. Create three contexts in your CircleCI organization: security for security scanning tools, deployment-dev for development environment deployments, and release-prod for production deployments with restricted access.
For the release-prod context, go to the “Security” tab and add your delivery-managers GitHub team (or restrict to yourself for the tutorial).
Setting up OIDC authentication
Modern CI/CD security practices avoid storing long-lived credentials. Instead, we’ll use OpenID Connect (OIDC) tokens that CircleCI generates automatically for each job. These tokens are short-lived and much more secure.
Creating an AWS IAM identity provider
First, set up AWS to trust CircleCI’s OIDC tokens. In your AWS console, go to IAM -> Identity providers -> Add provider and select “OpenID Connect”. For Provider URL, enter https://oidc.circleci.com/org/YOUR-ORG-ID. You can find your org ID in CircleCI Organization Settings -> Overview. Enter your CircleCI organization ID for Audience and click Add provider.
Creating an IAM role for CircleCI
Next, create a role that CircleCI jobs can assume. Go to IAM -> Roles -> Create role. Click Web identity and select the identity provider you just created. For Audience, select your organization ID, then add permissions. Start with basic S3 or CloudWatch permissions for testing. Name the role CircleCI-OIDC-Role and create it. Copy the role ARN; you’ll need it for the CircleCI configuration.
Configuring CircleCI for OIDC
Now you need to update the CircleCI configuration to use OIDC. Since you’ve set up branch protection rules, you’ll need to make this change through a pull request.
Create a new branch for this update:
git checkout -b add-oidc-support
Update your .circleci/config.yml to use the AWS CLI orb with OIDC:
version: 2.1
orbs:
aws-cli: circleci/aws-cli@5.1.1
jobs:
test:
docker:
- image: cimg/node:18.20
steps:
- checkout
- run: npm install
- run: npm test
security-scan:
docker:
- image: cimg/node:18.20
steps:
- checkout
- run: npm install
- run: npm audit
deploy-dev:
docker:
- image: cimg/node:18.20
steps:
- run: echo "Deploying to development environment..."
deploy-prod:
docker:
- image: cimg/node:18.20
steps:
- attach_workspace:
at: .
- aws-cli/setup:
role_arn: $AWS_ROLE_ARN
region: $AWS_REGION
role_session_name: "CircleCI-Deploy-Session"
- run:
name: Deploy to production with OIDC
command: |
echo "Deploying to production using OIDC authentication..."
# Example: aws s3 sync ./dist s3://my-app-bucket
workflows:
build-test-deploy:
jobs:
- test
- security-scan:
context: security
- deploy-dev:
context: deployment-dev
requires:
- test
- security-scan
filters:
branches:
only: main
- approve-for-prod:
type: approval
requires:
- deploy-dev
- deploy-prod:
context: release-prod
requires:
- approve-for-prod
Now commit and push this change through a pull request to test the access control system:
git add .
git commit -m "Add OIDC authentication support"
git push origin add-oidc-support
Before creating the pull request, you need to set up the project in CircleCI. Go to the CircleCI dashboard, find your secure-web-api repository, and click Set Up Project.

You’ll be prompted to enter the branch name where your CircleCI configuration is located. Enter add-oidc-support (the branch you just created) so CircleCI can detect your .circleci/config.yml file.
Now go to your GitHub repository and create a pull request from the add-oidc-support branch to main. You’ll see that CircleCI tests run automatically on the pull request. Once the tests pass, merge the pull request to complete the configuration update.
Add these environment variables to your release-prod context:
- AWS_REGION set to
us-east-1(or your preferred region) and - AWS_ROLE_ARN set to
arn:aws:iam::YOUR-ACCOUNT:role/CircleCI-OIDC-Role.
Understanding the approval workflow
Here’s a run-through of how the complete access control system works:
- The process begins when developers create feature branches, make changes, and push them to create pull requests.
- During the code review phase, the CI pipeline runs tests automatically without needing any restricted contexts yet.
- Once a development lead reviews and approves the changes, they’re merged to the main branch, which triggers the main branch workflow.
- The automated steps then run through tests and security scans, followed by development deployment.
- The pipeline reaches the critical manual approval point where only delivery managers can approve production deployment.
- Finally, once approved, the production deployment job runs with the restricted context and OIDC credentials.
Setting up approval jobs
The approval job approve-for-prod is the key manual oversight point in your automated pipeline. While technically any user with access to the CircleCI dashboard can click the approval button, the real security comes from what happens next.
The deploy-prod job that follows uses the release-prod context, which only certain security groups can access. If someone without proper permissions approves the job, the deployment step will fail with a 403 unauthorized error when it tries to access the restricted context.
This creates a two-layer security system where protected branches ensure only approved code reaches main and restricted contexts ensure only authorized personnel can deploy.
Additional security considerations
Branch filters provide another layer of control. Development deployments occur only on the main branch because of these filters, and since main is protected, the only way to get code there is by passing automated checks and code review.
You can also add dedicated QA or Security review steps that require manual approval, building them the same way as part of the chain. This gives you flexibility to add as many approval gates as your organization requires.
Considerations for open source projects
The enterprise approach works well for companies with strict compliance requirements, but most open source projects need a different strategy. These projects must open contributions to a much wider group of people, allowing many more contributors to trigger pipelines.
Many popular open source projects have a tiered contribution model. Inner core contributors are company employees who can contribute and administer everything, including releasing new versions. Outer core contributors are part of the organization, can push directly to the main repository and use all CircleCI features. Community contributors work on forks and submit pull requests.
For community contributors, the flow follows the standard fork and pull request model. CircleCI can be configured to build pull requests automatically, verifying changes before they’re considered for merge. You should still maintain protected branches and required checks.
Handling secrets in open source
Core contributors often need access to secrets for deployment and distribution. However, passing secrets via forks creates inherent security risks since anyone could extract secrets by modifying the pipeline in their pull request.
The recommended approach is to never pass secrets to fork builds. CircleCI now automatically prevents contexts and environment variables from being exposed to fork pull requests by default. This means that while tests and basic CI checks can run on fork PRs, any jobs requiring secrets (like deployment) will be skipped.
For open source projects, the typical workflow involves running basic tests on fork PRs to validate the contribution, then having maintainers merge approved changes into the main repository where the full pipeline (including deployments) can run safely with access to secrets.
This security-first approach means that open source projects can safely accept contributions from the community while maintaining strict control over sensitive operations. You can configure different workflows for fork PRs (testing only) versus main branch builds (full deployment pipeline).
Security best practice:
- Ensure all contexts in your organization have proper security group restrictions. Even if secrets aren’t passed to forks by default, maintaining principle of least access for all contexts provides defense in depth.
With your inner and outer core of contributors, you can set up access controls in very much the same way as the enterprise example. Maybe only the inner core can release new versions, but anyone in the outer core can merge any pull requests. The details are up to you!
Testing your access control system
Your access control system is now ready to be tested. Start by creating a feature branch and making a change to your code. Push and create a pull request, which will trigger the CI pipeline to run tests automatically. Review and merge the pull request (if you have team members, have them review it). Watch the main branch workflow as it runs through tests, security scan, and dev deployment. The pipeline will pause at the approval job where you can approve for production.

Once approved, the final deployment runs with the restricted context.

If someone without proper permissions tries to approve, they’ll see the approval succeed but the next job will fail with clear unauthorized access errors.
Conclusion
You’ve successfully implemented a comprehensive access control system for CI/CD pipelines using GitHub and CircleCI. This system provides enterprise-grade security while maintaining developer productivity.
The key components you’ve built include multi-layer security with protected branches that prevent unauthorized code changes, role-based access where GitHub teams control review and approval permissions, secure authentication using OIDC tokens that replace long-lived credentials, manual oversight through approval jobs that require human intervention for critical deployments, and a clear audit trail where all approvals and deployments are logged and visible.
Your access control system now works whether you’re managing internal enterprise applications or popular open-source projects. The same patterns can be extended to other cloud providers and deployment scenarios while maintaining the same security principles.
The key to effective CI/CD security lies in finding the right balance between automation and control. Experiment with the configurations in your demo repository to discover the best setup for your organization’s specific needs and requirements.