Hands-on workshop demonstrating secure secrets delivery to GitHub Actions pipelines using CyberArk Conjur and Privilege Cloud.
No passwords stored in code, in the runner, or in GitHub Secrets — credentials are fetched at runtime via JWT authentication.
GitHub Actions Runner
│
│ 1. Request JWT token (OIDC)
▼
GitHub OIDC Provider ──► JWT Token
│
│ 2. Authenticate with JWT
▼
CyberArk Conjur ──────────────────► Validates JWT claims
│ (repository, workflow)
│ 3. Return session token
▼
Conjur API ────────────────────────► Retrieve secrets from
│ Privilege Cloud vault
│ 4. Inject as masked env vars
▼
Your Workflow Steps
The pipeline in .github/workflows/main.yml runs 11 sequential stages:
Conjur authenticates the workflow using GitHub's OIDC JWT. No API keys or passwords are stored anywhere — GitHub's identity is the credential.
Retrieves multiple secrets in a single call. Demonstrates that GitHub Actions automatically masks secret values in all log output.
Fetches database credentials (username, password, address) from Privilege Cloud via Conjur and opens a live MySQL connection.
First attempts to connect with a hardcoded password (fails). Then retrieves the current password from Conjur and connects successfully. Makes the risk of hardcoded credentials tangible.
Accesses an authorized secret (succeeds), then attempts to access an unauthorized path (denied). Shows that the Conjur host can only reach what its policy explicitly permits.
Full end-to-end demo: Conjur delivers DB credentials → MySQL query runs → real data returned. No credential ever touches the pipeline code.
Connects to the database using the current Conjur credentials and prompts the presenter to rotate the password in Privilege Cloud. Re-running the pipeline with zero changes shows the new password works automatically.
Three sub-stages chained with needs:. Each runs in a different GitHub Environment with its own secrets. The prod job pauses and waits for a human reviewer to approve before running — demonstrating governance over production deployments. Requires staging and prod GitHub Environments to be created under Settings → Environments, with at least one Required Reviewer on prod.
Retrieves SSH credentials (username, password, address) from the jumpserver account in Privilege Cloud and connects to the remote server using sshpass. No credentials are stored in the repo or in GitHub Secrets.
Fetches Docker Hub username and password from the dockerhub_aslan account in Privilege Cloud via Conjur, performs docker login docker.io with --password-stdin (no credential in the command line), and logs out. Demonstrates credential injection into container workflows.
Authenticates with Conjur directly via the JWT and calls the Conjur audit API to retrieve the last 20 secret fetch events. Displays who accessed what and when — showing the full traceability that Conjur provides for compliance and incident response.
- CyberArk Conjur Cloud tenant
- CyberArk Privilege Cloud with at least two safes/accounts configured
- GitHub self-hosted runner with Docker and MySQL client installed
- GitHub repository with Actions enabled
Load the JWT authenticator policy (github-authn-jwt.yml):
conjur policy load -f github-authn-jwt.yml -b rootSet the authenticator variables:
conjur variable set -i conjur/authn-jwt/github/issuer \
-v "https://token.actions.githubusercontent.com"
conjur variable set -i conjur/authn-jwt/github/jwks-uri \
-v "https://token.actions.githubusercontent.com/.well-known/jwks"
conjur variable set -i conjur/authn-jwt/github/token-app-property \
-v "workflow"
conjur variable set -i conjur/authn-jwt/github/identity-path \
-v "/github-apps"
conjur variable set -i conjur/authn-jwt/github/enforced-claims \
-v "workflow,repository"Load the app identity policy (github-app-id.yml):
conjur policy load -f github-app-id.yml -b rootThe host must have both annotations:
annotations:
authn-jwt/github/repository: aslancarlos/workshop-action
authn-jwt/github/workflow: workshop-actionGo to Settings → Secrets and variables → Actions and add:
| Secret | Value |
|---|---|
CONJUR_URL |
https://<tenant>.secretsmgr.cyberark.cloud/api |
CONJUR_SERVICE_ID |
JWT authenticator ID (e.g. github) |
DB_ADDRESS_PLAIN |
Database host address — used only in Stage 4 hardcoded failure demo |
# Ubuntu / Debian
sudo apt-get install -y mysql-client sshpass
# RHEL / Amazon Linux
sudo yum install -y mysql sshpassTo use this action in your own workflow:
- name: Retrieve secrets from Conjur
uses: aslancarlos/workshop-action@main
with:
url: ${{ secrets.CONJUR_URL }}
account: conjur
authn_id: ${{ secrets.CONJUR_SERVICE_ID }}
secrets: "path/to/secret|ENV_VAR_NAME;path/to/other/secret|OTHER_VAR"| Input | Required | Description |
|---|---|---|
url |
Yes | Conjur endpoint URL |
account |
Yes | Conjur account name (usually conjur for Cloud) |
authn_id |
No | JWT authenticator service ID |
host_id |
No | Host ID for API key authentication |
api_key |
No | API key for host authentication |
secrets |
Yes | Semicolon-delimited list of secrets to retrieve |
certificate |
No | Self-signed SSL certificate content |
audience |
No | Custom aud claim value for JWT |
authn_token_file |
No | Path to a pre-fetched Conjur auth token |
path/to/variable|ENV_VAR_NAME;path/to/other/variable
- Delimiter:
;between secrets - Mapping:
|separates the Conjur variable path from the environment variable name - If no name is given, the last segment of the path is used (uppercased)
Important: never use YAML multi-line folding (>-) for the secrets input. It inserts spaces that break path parsing.
| Error | Cause | Fix |
|---|---|---|
CONJ00057E Role does not have the required constraints |
Missing workflow annotation on host |
Add authn-jwt/github/workflow: workshop-action to the host |
Malformed authorization token |
Auth returned empty token | Verify CONJUR_SERVICE_ID and host annotations match |
Variable is empty or not found |
Space in secret path | Remove YAML >- folding from secrets: input |
Permission denied: set_env_* |
Container running as non-root user | Dockerfile must not set USER 1001 |
Node.js 20 actions are deprecated |
Outdated actions/checkout version |
Use actions/checkout@v4 |
.
├── action.yml # Action definition (uses local Dockerfile)
├── Dockerfile # Container image (Alpine, runs as root)
├── entrypoint.sh # JWT auth + secret retrieval logic
├── .github/
│ └── workflows/
│ └── main.yml # 11-stage workshop pipeline
├── github-authn-jwt.yml # Sample: JWT authenticator Conjur policy
├── github-app-id.yml # Sample: app host identity Conjur policy
└── bin/
├── policy/root.yml # Combined policy for local dev
├── start.sh # Start local test environment
├── stop.sh # Stop local test environment
└── coverage.sh # Run test coverage
- Secrets are never logged — they are masked before being set as environment variables
- JWT authentication uses GitHub's OIDC provider — no long-lived credentials needed
- The Conjur host policy enforces least privilege — each workflow only accesses its permitted secrets
- See SECURITY.md for vulnerability reporting