TrustPin™
TrustPin™
Shift-left security without slowing down releases

Certificate Pinning in Your CI/CD Pipeline
Automate Pin Updates, Ship Faster

Automate security and accelerate delivery: manage certificate pins from your pipeline and publish updates instantly, without blocking releases or waiting for app-store approval.

How does TrustPin fit into a CI/CD pipeline?

Two commands. trustpin-cli projects refresh-certs stages the pins TrustPin can see for a domain, from the live certificate and the Certificate Transparency logs. trustpin-cli projects sign signs the configuration and publishes it to the CDN. Nothing goes live until the signature succeeds, so the job is safe to run on a schedule.

GitHub Actions

Jenkins

GitLab CI

Azure DevOps

The whole rotation, in a shell

export TRUSTPIN_API_TOKEN="tp_..."   # from app.trustpin.cloud/account/access-tokens

# Stage pins from the live certificate + CT logs (never removes a domain)
trustpin-cli projects refresh-certs "$ORG_ID" "$PROJECT_ID" \
  --domain api.example.com --remove-expired

# Publish: master password (cloud keys) or --private-key key.pem (BYOK)
trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$MASTER_PASSWORD"

# Confirm what installed apps will fetch
trustpin-cli projects jws "$ORG_ID" "$PROJECT_ID" --verify
Runs in seconds; safe to schedule daily

No app-store wait

Push pin updates instantly without shipping a new app release

Zero downtime

Rotate certificates without service interruptions

Full automation

Drop-in CI/CD pipeline integration

Team collaboration

Centralized certificate management for your team

Which CI platforms does the TrustPin CLI work with?

Any runner that can download a Linux or macOS binary. The examples below cover GitHub Actions, GitLab CI, Jenkins and Azure DevOps; the same three steps work in CircleCI, Bitrise or a plain cron job. Each one rehearses the signature with --dry-run first, so a wrong key or password fails the job before anything is staged.

GitHub Actions

.github/workflows/refresh-pins.yml
name: Refresh certificate pins
on:
  schedule:
    - cron: "0 5 * * *"   # daily 05:00 UTC
  workflow_dispatch:

jobs:
  refresh:
    runs-on: ubuntu-latest
    env:
      TRUSTPIN_API_TOKEN: ${{ secrets.TRUSTPIN_API_TOKEN }}
      ORG_ID: ${{ vars.TRUSTPIN_ORG_ID }}
      PROJECT_ID: ${{ vars.TRUSTPIN_PROJECT_ID }}
      MASTER_PASSWORD: ${{ secrets.TRUSTPIN_MASTER_PASSWORD }}
    steps:
      - name: Install TrustPin CLI
        run: |
          curl -sSL https://github.com/trustpin-cloud/homebrew-trustpin/releases/latest/download/trustpin-cli-linux-x64 -o trustpin-cli
          chmod +x trustpin-cli && sudo mv trustpin-cli /usr/local/bin/

      - name: Preflight the signing credentials (nothing is published)
        run: trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$MASTER_PASSWORD" --dry-run > /dev/null

      - name: Record the configuration version
        id: before
        run: |
          VERSION=$(trustpin-cli projects get "$ORG_ID" "$PROJECT_ID" --output json | jq -r '.data.project.config_version')
          echo "version=$VERSION" >> "$GITHUB_OUTPUT"

      - name: Refresh pins from the live certificate and CT logs
        run: |
          trustpin-cli projects refresh-certs "$ORG_ID" "$PROJECT_ID" \
            --domain api.example.com --remove-expired --output json

      - name: Sign and publish only if something changed
        run: |
          AFTER=$(trustpin-cli projects get "$ORG_ID" "$PROJECT_ID" --output json | jq -r '.data.project.config_version')
          if [ "$AFTER" = "${{ steps.before.outputs.version }}" ]; then
            echo "Pins already up to date, nothing to publish."; exit 0
          fi
          trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$MASTER_PASSWORD"
          trustpin-cli projects jws "$ORG_ID" "$PROJECT_ID" --verify

GitLab CI

.gitlab-ci.yml
refresh-pins:
  image: ubuntu:24.04
  rules:
    - if: $CI_PIPELINE_SOURCE == "schedule"   # add a daily schedule in CI/CD > Schedules
    - when: manual
  variables:
    ORG_ID: "$TRUSTPIN_ORG_ID"                # CI/CD variables; mark the token and password as masked
    PROJECT_ID: "$TRUSTPIN_PROJECT_ID"
  before_script:
    - apt-get update -qq && apt-get install -y -qq curl jq ca-certificates
    - curl -sSL https://github.com/trustpin-cloud/homebrew-trustpin/releases/latest/download/trustpin-cli-linux-x64 -o trustpin-cli
    - chmod +x trustpin-cli && mv trustpin-cli /usr/local/bin/
  script:
    - trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$TRUSTPIN_MASTER_PASSWORD" --dry-run > /dev/null
    - BEFORE=$(trustpin-cli projects get "$ORG_ID" "$PROJECT_ID" --output json | jq -r '.data.project.config_version')
    - trustpin-cli projects refresh-certs "$ORG_ID" "$PROJECT_ID" --domain api.example.com --remove-expired --output json
    - AFTER=$(trustpin-cli projects get "$ORG_ID" "$PROJECT_ID" --output json | jq -r '.data.project.config_version')
    - |
      if [ "$BEFORE" = "$AFTER" ]; then echo "Pins already up to date"; exit 0; fi
      trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$TRUSTPIN_MASTER_PASSWORD"
      trustpin-cli projects jws "$ORG_ID" "$PROJECT_ID" --verify

Jenkins

Jenkinsfile
pipeline {
  agent any
  triggers { cron('H 5 * * *') }
  environment {
    TRUSTPIN_API_TOKEN = credentials('trustpin-api-token')       // Secret text
    MASTER_PASSWORD    = credentials('trustpin-master-password')  // Secret text
    ORG_ID             = 'fba3418e-b5ae-b273-4bab-6da6ae07ba99'
    PROJECT_ID         = '9caaea0a-80ea-013e-4e7b-cee6bfb52b36'
  }
  stages {
    stage('Install CLI') {
      steps { sh '''
        curl -sSL https://github.com/trustpin-cloud/homebrew-trustpin/releases/latest/download/trustpin-cli-linux-x64 -o trustpin-cli
        chmod +x trustpin-cli
      ''' }
    }
    stage('Preflight') {
      steps { sh './trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$MASTER_PASSWORD" --dry-run > /dev/null' }
    }
    stage('Refresh and publish') {
      steps { sh '''
        BEFORE=$(./trustpin-cli projects get "$ORG_ID" "$PROJECT_ID" --output json | jq -r '.data.project.config_version')
        ./trustpin-cli projects refresh-certs "$ORG_ID" "$PROJECT_ID" --domain api.example.com --remove-expired --output json
        AFTER=$(./trustpin-cli projects get "$ORG_ID" "$PROJECT_ID" --output json | jq -r '.data.project.config_version')
        if [ "$BEFORE" = "$AFTER" ]; then echo "Pins already up to date"; exit 0; fi
        ./trustpin-cli projects sign "$ORG_ID" "$PROJECT_ID" --password "$MASTER_PASSWORD"
        ./trustpin-cli projects jws "$ORG_ID" "$PROJECT_ID" --verify
      ''' }
    }
  }
}

Azure DevOps

azure-pipelines.yml
schedules:
  - cron: "0 5 * * *"
    displayName: Daily pin refresh
    branches: { include: [main] }
    always: true

pool: { vmImage: ubuntu-latest }

variables:
  - group: trustpin   # TRUSTPIN_API_TOKEN and TRUSTPIN_MASTER_PASSWORD marked secret
  - name: ORG_ID
    value: fba3418e-b5ae-b273-4bab-6da6ae07ba99
  - name: PROJECT_ID
    value: 9caaea0a-80ea-013e-4e7b-cee6bfb52b36

steps:
  - bash: |
      curl -sSL https://github.com/trustpin-cloud/homebrew-trustpin/releases/latest/download/trustpin-cli-linux-x64 -o trustpin-cli
      chmod +x trustpin-cli && sudo mv trustpin-cli /usr/local/bin/
    displayName: Install TrustPin CLI

  - bash: |
      trustpin-cli projects sign "$(ORG_ID)" "$(PROJECT_ID)" --password "$MASTER_PASSWORD" --dry-run > /dev/null
      BEFORE=$(trustpin-cli projects get "$(ORG_ID)" "$(PROJECT_ID)" --output json | jq -r '.data.project.config_version')
      trustpin-cli projects refresh-certs "$(ORG_ID)" "$(PROJECT_ID)" --domain api.example.com --remove-expired --output json
      AFTER=$(trustpin-cli projects get "$(ORG_ID)" "$(PROJECT_ID)" --output json | jq -r '.data.project.config_version')
      if [ "$BEFORE" = "$AFTER" ]; then echo "Pins already up to date"; exit 0; fi
      trustpin-cli projects sign "$(ORG_ID)" "$(PROJECT_ID)" --password "$MASTER_PASSWORD"
      trustpin-cli projects jws "$(ORG_ID)" "$(PROJECT_ID)" --verify
    displayName: Refresh, sign and verify pins
    env:
      TRUSTPIN_API_TOKEN: $(TRUSTPIN_API_TOKEN)
      MASTER_PASSWORD: $(TRUSTPIN_MASTER_PASSWORD)

Bring Your Own Keys: write the PEM private key from a secret to a temporary file and replace --password "$MASTER_PASSWORD" with --private-key "$PRIVATE_KEY_FILE". The dry run validates the key against the project's registered public key.

Internal hosts TrustPin cannot observe: extract the SPKI pin yourself and stage it with projects upsert. The DevOps guide covers both paths, plus an AWS ACM renewal workflow with EventBridge and Lambda.

How do you automate pin rotation safely?

Stage, then publish. refresh-certs never discards an existing pin and writes nothing if the lookup fails, so a DNS blip can never strip a domain. Because it returns every unexpired issuance, a renewed certificate is pinned alongside the one still being served: the dual-pin overlap that makes rotation safe, maintained without bookkeeping.

1

Integrate the SDK

Add the TrustPin SDK to your app once; every later pin change is remote

2

Refresh the pins

refresh-certs stages the pins TrustPin can see for each domain, on a schedule

3

Sign and publish

sign publishes the configuration with your master password or BYOK private key

4

Apps update

Installed apps fetch the signed configuration within minutes

Want the CLI reference first? Read the CLI documentation.

Ready to automate your certificate management?

Start free, add the CLI to one pipeline, and let the schedule keep your pins current.