docs: sync documentation with current 11-stage pipeline #30
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: workshop-action | |
| on: | |
| push: | |
| branches: | |
| - main | |
| pull_request: | |
| workflow_dispatch: | |
| jobs: | |
| # ============================================================ | |
| # STAGE 1 - JWT Authentication & Basic Secret Retrieval | |
| # Demonstrates: how Conjur authenticates GitHub Actions via | |
| # JWT (no stored credentials) and retrieves a secret. | |
| # ============================================================ | |
| stage-1-jwt-auth: | |
| name: "Stage 1 - JWT Auth & Secret Retrieval" | |
| runs-on: self-hosted | |
| environment: dev | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve secret from Conjur via JWT | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: data/vault/dev-demo-aslan/asramos_pcloud_pov/username|APP_USERNAME | |
| - name: Show that secret is available (masked in logs) | |
| run: | | |
| echo "Secret retrieved successfully!" | |
| echo "Value (masked): $APP_USERNAME" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 2 - Multiple Secrets & Masking Demo | |
| # Demonstrates: retrieving multiple secrets in one call and | |
| # how GitHub Actions automatically masks values in the logs. | |
| # ============================================================ | |
| stage-2-multiple-secrets: | |
| name: "Stage 2 - Multiple Secrets & Masking" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-1-jwt-auth | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve multiple secrets from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/asramos_pcloud_pov/username|APP_USERNAME;data/vault/dev-demo-aslan/asramos_pcloud_pov/password|APP_PASSWORD" | |
| - name: "Masking demo: each character spaced out to confirm value was set" | |
| run: | | |
| echo "--- Username (spaced) ---" | |
| printenv APP_USERNAME | sed 's/./& /g' | |
| echo "--- Password (spaced) ---" | |
| printenv APP_PASSWORD | sed 's/./& /g' | |
| - name: "Masking demo: trying to echo directly (will be masked)" | |
| run: | | |
| echo "Username: $APP_USERNAME" | |
| echo "Password: $APP_PASSWORD" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 3 - Database Credentials from Conjur | |
| # Demonstrates: retrieving database credentials stored in | |
| # Privilege Cloud via Conjur and connecting to the database. | |
| # ============================================================ | |
| stage-3-database: | |
| name: "Stage 3 - Database Connection with Conjur Credentials" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-2-multiple-secrets | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve database credentials from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/dbuser_dual/username|DB_USERNAME;data/vault/dev-demo-aslan/dbuser_dual/password|DB_PASSWORD;data/vault/dev-demo-aslan/dbuser_dual/address|DB_ADDRESS" | |
| - name: Show database host (address is not a secret, safe to print) | |
| run: echo "Connecting to database at $DB_ADDRESS" | |
| - name: Query database using Conjur credentials | |
| run: | | |
| MYSQL_PWD=$DB_PASSWORD mysql \ | |
| --host="$DB_ADDRESS" \ | |
| --user="$DB_USERNAME" \ | |
| --execute="SELECT current_user(), database(), now();" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 4 - Hardcoded vs Conjur (Before / After) | |
| # Demonstrates: the risk of hardcoded credentials failing | |
| # versus Conjur delivering the correct secret dynamically. | |
| # ============================================================ | |
| stage-4-before-after: | |
| name: "Stage 4 - Hardcoded vs Conjur" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-3-database | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: "BAD PRACTICE: hardcoded credential (connection will fail)" | |
| run: | | |
| echo ">>> Trying to connect with a hardcoded password..." | |
| MYSQL_PWD="SuperSecret123!" mysql \ | |
| --host="${{ secrets.DB_ADDRESS_PLAIN }}" \ | |
| --user="app_user" \ | |
| --connect-timeout=5 \ | |
| --execute="SELECT 'connected!' AS status;" \ | |
| || echo "Connection FAILED — hardcoded credentials are wrong or rotated!" | |
| - name: Retrieve fresh credentials from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/dbuser_dual/username|DB_USERNAME;data/vault/dev-demo-aslan/dbuser_dual/password|DB_PASSWORD;data/vault/dev-demo-aslan/dbuser_dual/address|DB_ADDRESS" | |
| - name: "GOOD PRACTICE: connect with Conjur credentials (always up-to-date)" | |
| run: | | |
| echo ">>> Connecting with credentials from Conjur..." | |
| MYSQL_PWD=$DB_PASSWORD mysql \ | |
| --host="$DB_ADDRESS" \ | |
| --user="$DB_USERNAME" \ | |
| --execute="SELECT 'connected!' AS status;" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 5 - Least Privilege / Access Denied | |
| # Demonstrates: the host only has access to what is explicitly | |
| # permitted in Conjur policy — unauthorized access is denied. | |
| # ============================================================ | |
| stage-5-least-privilege: | |
| name: "Stage 5 - Least Privilege & Access Denied" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-4-before-after | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: "Retrieve authorized secret (should succeed)" | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: data/vault/dev-demo-aslan/asramos_pcloud_pov/username|APP_USERNAME | |
| - name: "Authorized access confirmed" | |
| run: echo "Authorized secret retrieved successfully." | |
| - name: "Attempt to access an unauthorized secret (should fail)" | |
| id: unauthorized | |
| continue-on-error: true | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: data/vault/unauthorized-safe/secret|FORBIDDEN_SECRET | |
| - name: "Confirm access was denied" | |
| run: | | |
| if [ "${{ steps.unauthorized.outcome }}" == "failure" ]; then | |
| echo "Access DENIED by Conjur — least privilege working as expected!" | |
| else | |
| echo "WARNING: access was not denied. Review Conjur policy." | |
| exit 1 | |
| fi | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 6 - Real Query & Data Retrieval | |
| # Demonstrates: end-to-end flow — Conjur delivers credentials, | |
| # application uses them to query real data from the database. | |
| # ============================================================ | |
| stage-6-real-query: | |
| name: "Stage 6 - Real Query & Data Retrieval" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-5-least-privilege | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve database credentials from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/dbuser_dual/username|DB_USERNAME;data/vault/dev-demo-aslan/dbuser_dual/password|DB_PASSWORD;data/vault/dev-demo-aslan/dbuser_dual/address|DB_ADDRESS" | |
| - name: Run real query and display results | |
| run: | | |
| echo "=== Database Server Info ===" | |
| MYSQL_PWD=$DB_PASSWORD mysql \ | |
| --host="$DB_ADDRESS" \ | |
| --user="$DB_USERNAME" \ | |
| --execute="SELECT @@version AS mysql_version, @@hostname AS host;" | |
| echo "" | |
| echo "=== Current Session Info ===" | |
| MYSQL_PWD=$DB_PASSWORD mysql \ | |
| --host="$DB_ADDRESS" \ | |
| --user="$DB_USERNAME" \ | |
| --execute="SELECT current_user() AS logged_in_as, now() AS query_time;" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 7 - Credential Rotation (Zero Pipeline Changes) | |
| # Demonstrates: Conjur always delivers the current password | |
| # from Privilege Cloud. After a rotation, this pipeline runs | |
| # unchanged and still connects — no secret stored in the repo. | |
| # ============================================================ | |
| stage-7-rotation: | |
| name: "Stage 7 - Credential Rotation Demo" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-6-real-query | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve credentials from Conjur (run 1) | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/dbuser_dual/username|DB_USERNAME;data/vault/dev-demo-aslan/dbuser_dual/password|DB_PASSWORD;data/vault/dev-demo-aslan/dbuser_dual/address|DB_ADDRESS" | |
| - name: Connect to database (run 1 — current password) | |
| run: | | |
| echo "=== Run 1: connecting with current Conjur credentials ===" | |
| MYSQL_PWD=$DB_PASSWORD mysql \ | |
| --host="$DB_ADDRESS" \ | |
| --user="$DB_USERNAME" \ | |
| --execute="SELECT 'Run 1 OK' AS result, now() AS timestamp;" | |
| echo "" | |
| echo "--- Rotate the password in Privilege Cloud now and re-run ---" | |
| echo "--- This pipeline needs zero changes to keep working ---" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 8 - Environment Promotion (dev → staging → prod) | |
| # Demonstrates: the same pipeline promotes through environments | |
| # automatically. Each environment fetches its own secrets from | |
| # Conjur. The prod job pauses for human approval. | |
| # | |
| # Pre-requisite: create GitHub Environments "staging" and | |
| # "prod" under Settings → Environments. On "prod", add at | |
| # least one Required Reviewer to enable the approval gate. | |
| # ============================================================ | |
| stage-8-promote-dev: | |
| name: "Stage 8a - Promote: dev" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-7-rotation | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve dev secrets from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/asramos_pcloud_pov/username|APP_USERNAME;data/vault/dev-demo-aslan/asramos_pcloud_pov/password|APP_PASSWORD" | |
| - name: Simulate deployment to dev | |
| run: | | |
| echo "=== Deploying to DEV ===" | |
| echo "Authenticated as: $APP_USERNAME" | |
| echo "DEV deployment complete." | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| stage-8-promote-staging: | |
| name: "Stage 8b - Promote: staging" | |
| runs-on: self-hosted | |
| environment: staging | |
| needs: stage-8-promote-dev | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve staging secrets from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/asramos_pcloud_pov/username|APP_USERNAME;data/vault/dev-demo-aslan/asramos_pcloud_pov/password|APP_PASSWORD" | |
| - name: Simulate deployment to staging | |
| run: | | |
| echo "=== Deploying to STAGING ===" | |
| echo "Authenticated as: $APP_USERNAME" | |
| echo "STAGING deployment complete." | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| stage-8-promote-prod: | |
| name: "Stage 8c - Promote: prod (requires approval)" | |
| runs-on: self-hosted | |
| environment: prod | |
| needs: stage-8-promote-staging | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve prod secrets from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/dbuser_dual/username|APP_USERNAME;data/vault/dev-demo-aslan/dbuser_dual/password|APP_PASSWORD" | |
| - name: Simulate deployment to prod | |
| run: | | |
| echo "=== Deploying to PRODUCTION ===" | |
| echo "Authenticated as: $APP_USERNAME" | |
| echo "PRODUCTION deployment complete." | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 9 - SSH Deploy with Credentials from Conjur | |
| # Demonstrates: server credentials (username, password, | |
| # address) stored in Privilege Cloud, injected at runtime. | |
| # The jumpserver account in PCloud holds these values. | |
| # | |
| # Conjur path: data/vault/dev-demo-aslan/jumpserver/ | |
| # ============================================================ | |
| stage-9-ssh-deploy: | |
| name: "Stage 9 - SSH Deploy with Conjur Credentials" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-8-promote-prod | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve SSH credentials from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/dev-demo-aslan/jumpserver/username|SSH_USER_CONJUR;data/vault/dev-demo-aslan/jumpserver/password|SSH_PASSWORD;data/vault/dev-demo-aslan/jumpserver/address|SSH_HOST_CONJUR" | |
| - name: Connect via SSH using credentials from Conjur | |
| run: | | |
| echo "Connecting to $SSH_HOST_CONJUR as $SSH_USER_CONJUR (password from Conjur)" | |
| sshpass -p "$SSH_PASSWORD" ssh \ | |
| -o StrictHostKeyChecking=no \ | |
| -o ConnectTimeout=10 \ | |
| "$SSH_USER_CONJUR@$SSH_HOST_CONJUR" \ | |
| "echo 'Connected via credentials from Conjur — host: \$(hostname), date: \$(date)'" | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 10 - Docker Registry Login with Conjur Credentials | |
| # Demonstrates: registry credentials managed in Privilege | |
| # Cloud, including the registry address — no GitHub secrets | |
| # needed. Uses dockerhub_aslan account from PCloud. | |
| # | |
| # Conjur path: data/vault/devsecops/dockerhub_aslan/ | |
| # ============================================================ | |
| stage-10-docker-registry: | |
| name: "Stage 10 - Docker Registry Login with Conjur" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-9-ssh-deploy | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Retrieve Docker Hub credentials from Conjur | |
| uses: ./ | |
| with: | |
| url: ${{ secrets.CONJUR_URL }} | |
| account: conjur | |
| authn_id: ${{ secrets.CONJUR_SERVICE_ID }} | |
| secrets: "data/vault/devsecops/dockerhub_aslan/username|REGISTRY_USERNAME;data/vault/devsecops/dockerhub_aslan/password|REGISTRY_PASSWORD" | |
| - name: Login to Docker Hub using Conjur credentials | |
| run: | | |
| echo "Logging in to docker.io as $REGISTRY_USERNAME (credentials from Conjur)" | |
| echo "$REGISTRY_PASSWORD" | docker login docker.io \ | |
| --username "$REGISTRY_USERNAME" \ | |
| --password-stdin | |
| echo "Docker login successful — credentials came from Conjur, not from GitHub Secrets!" | |
| - name: Pull public image to confirm login | |
| run: | | |
| docker pull "$REGISTRY_USERNAME/conjur-workshop:latest" 2>/dev/null \ | |
| || docker pull hello-world \ | |
| && echo "Registry access confirmed." | |
| - name: Logout from registry | |
| if: always() | |
| run: docker logout docker.io | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] | |
| # ============================================================ | |
| # STAGE 11 - Audit Trail | |
| # Demonstrates: every secret access is logged automatically | |
| # in Conjur. This job queries the Conjur audit API to show | |
| # who accessed what and when — full traceability for compliance. | |
| # | |
| # Pre-requisite: the Conjur host must have read permission on | |
| # audit events (contact your Conjur admin to grant auditor role). | |
| # ============================================================ | |
| stage-11-audit-trail: | |
| name: "Stage 11 - Audit Trail" | |
| runs-on: self-hosted | |
| environment: dev | |
| needs: stage-10-docker-registry | |
| permissions: | |
| id-token: 'write' | |
| contents: 'read' | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Authenticate with Conjur to obtain session token | |
| run: | | |
| JWT_TOKEN=$(curl -s \ | |
| -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ | |
| "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r .value) | |
| SESSION_TOKEN=$(curl -s \ | |
| --request POST \ | |
| "${{ secrets.CONJUR_URL }}/authn-jwt/${{ secrets.CONJUR_SERVICE_ID }}/conjur/authenticate" \ | |
| --header "Content-Type: application/x-www-form-urlencoded" \ | |
| --header "Accept-Encoding: base64" \ | |
| --data-urlencode "jwt=$JWT_TOKEN") | |
| echo "CONJUR_SESSION_TOKEN=$SESSION_TOKEN" >> $GITHUB_ENV | |
| - name: Query Conjur audit log (last 20 fetch events) | |
| run: | | |
| echo "=== Conjur Audit Log — Secret Fetch Events ===" | |
| curl -s \ | |
| -H "Authorization: Token token=\"$CONJUR_SESSION_TOKEN\"" \ | |
| "${{ secrets.CONJUR_URL }}/audit?limit=20&kind=fetch" \ | |
| | jq -r '.[] | "[\(.timestamp)] \(.subject.role) fetched \(.resource.id)"' \ | |
| || echo "Note: audit access requires the host to have auditor permissions in Conjur." | |
| - name: Workshop summary | |
| run: | | |
| echo "" | |
| echo "============================================" | |
| echo " Workshop Complete — 11 Stages Executed" | |
| echo "============================================" | |
| echo "" | |
| echo "What was demonstrated:" | |
| echo " 1. JWT auth — no stored credentials" | |
| echo " 2. Multiple secrets + log masking" | |
| echo " 3. Database connection (MySQL)" | |
| echo " 4. Hardcoded credentials vs Conjur" | |
| echo " 5. Least privilege enforcement" | |
| echo " 6. Real database query end-to-end" | |
| echo " 7. Credential rotation" | |
| echo " 8. Environment promotion (dev→staging→prod)" | |
| echo " 9. SSH deploy with key from Conjur" | |
| echo " 10. Docker registry login from Conjur" | |
| echo " 11. Audit trail — every access logged" | |
| echo "" | |
| echo "Zero credentials stored in this repository." | |
| - name: Clean Workspace | |
| uses: AutoModality/[email protected] |