Authentication in CI/CD
The Rule
Set environment variables. Do not run vulnetix auth login.
Environment variables sit at the top of the precedence chain, persist nothing, and disappear with the job. A login step buys you nothing on an ephemeral runner and leaves a plaintext credentials.json in a workspace that may be cached, uploaded as an artifact, or shared between steps.
env:
VULNETIX_ORG_ID: ${{ secrets.VULNETIX_ORG_ID }}
VULNETIX_API_KEY: ${{ secrets.VULNETIX_API_KEY }}
Then verify before doing work:
vulnetix auth verify
auth verify reads the credential, calls the API, exits non-zero on failure, and writes nothing.
--store keyring on a CI runner silently falls back to a plaintext file, because there is no Secret Service on the D-Bus session bus. You get file storage with the appearance of keychain storage. Use environment variables instead.Choosing a Credential for CI
| Credential | Suitability |
|---|---|
Bearer token (VULNETIX_API_TOKEN) | Best. Org-less, revocable individually, no second variable to leak. |
ApiKey (VULNETIX_API_KEY + VULNETIX_ORG_ID) | Fine. Two variables; the org ID is not sensitive. |
SigV4 (VVD_ORG + VVD_SECRET) | Avoid. The secret derives request keys; do not expose it to a shared runner. |
Issue a distinct token per pipeline. A token shared across repositories cannot be revoked without breaking all of them, and its use cannot be attributed.
GitHub Actions
jobs:
scan:
runs-on: ubuntu-latest
env:
VULNETIX_ORG_ID: ${{ secrets.VULNETIX_ORG_ID }}
VULNETIX_API_KEY: ${{ secrets.VULNETIX_API_KEY }}
steps:
- uses: actions/checkout@v5
- name: Install Vulnetix CLI
run: curl -fsSL https://cli.vulnetix.com/install.sh | sh
- name: Verify authentication
run: vulnetix auth verify
- name: Scan
run: vulnetix scan --severity high
Values from secrets.* are masked in logs automatically. Values you compute are not — register them:
echo "::add-mask::$DERIVED_TOKEN"
The native action takes org-id and api-key inputs and runs vulnetix auth login --api-key … --org-id … --store project internally. That writes ./.vulnetix/credentials.json into the workspace. It is safe on an ephemeral runner, but do not upload the workspace as an artifact afterwards. See GitHub Actions.
GitLab CI
Mark the variables Masked and Protected in Settings → CI/CD → Variables. A value is maskable when it is a single line, contains no spaces, and is at least 8 characters — both an ApiKey hex digest and a UUID qualify. Protected restricts the variable to pipelines on protected branches and tags.
vulnetix:
stage: security
image: golang:latest
variables:
VULNETIX_ORG_ID: $VULNETIX_ORG_ID
VULNETIX_API_KEY: $VULNETIX_API_KEY
before_script:
- curl -fsSL https://cli.vulnetix.com/install.sh | sh
- vulnetix auth verify
script:
- vulnetix scan --severity high
Bitbucket Pipelines
Define secured repository variables. Bitbucket redacts them from logs.
pipelines:
pull-requests:
'**':
- step:
name: Vulnetix
image: golang:latest
script:
- curl -fsSL https://cli.vulnetix.com/install.sh | sh
- vulnetix auth verify
- vulnetix scan --severity high
Azure DevOps
Store credentials in a variable group backed by Azure Key Vault. Mark them secret so they are not passed to the task environment implicitly — map them explicitly.
steps:
- task: Bash@3
displayName: Vulnetix scan
env:
VULNETIX_ORG_ID: $(VULNETIX_ORG_ID)
VULNETIX_API_KEY: $(VULNETIX_API_KEY)
inputs:
targetType: inline
script: |
curl -fsSL https://cli.vulnetix.com/install.sh | sh
vulnetix auth verify
vulnetix scan --severity high
Secret variables in Azure Pipelines are not injected into the environment automatically. Omitting the env: block gives you an empty VULNETIX_API_KEY and a silent fall through to community access.
Jenkins
pipeline {
agent any
stages {
stage('Vulnetix') {
steps {
withCredentials([
string(credentialsId: 'vulnetix-org-id', variable: 'VULNETIX_ORG_ID'),
string(credentialsId: 'vulnetix-api-key', variable: 'VULNETIX_API_KEY')
]) {
sh '''
curl -fsSL https://cli.vulnetix.com/install.sh | sh
vulnetix auth verify
vulnetix scan --severity high
'''
}
}
}
}
}
Use withCredentials rather than an environment { } block: the binding is scoped to the step, and Jenkins masks the value in console output.
Never use sh "… ${VULNETIX_API_KEY} …" with double quotes — Groovy interpolates before the shell runs, and the secret is written to the build log as part of the command. Single quotes keep expansion in the shell.
Kubernetes
apiVersion: batch/v1
kind: Job
metadata:
name: vulnetix-scan
spec:
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 10001
containers:
- name: vulnetix
image: ghcr.io/example/ci
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
capabilities: { drop: ["ALL"] }
env:
- name: VULNETIX_API_TOKEN
valueFrom:
secretKeyRef:
name: vulnetix
key: token
command: ["vulnetix", "scan", "--severity", "high"]
readOnlyRootFilesystem: true guarantees no credential file can be written. Combined with VULNETIX_API_TOKEN this is the strongest configuration available.
Rules That Apply Everywhere
Never put the secret in argv. Every process on the host can read /proc/<pid>/cmdline, and most CI systems echo the command before running it.
vulnetix auth login --api-key 6e40f1c3… # leaks to logs and ps
export VULNETIX_API_KEY=…; vulnetix scan # does not
Never echo a credential to debug it. Print its length or its source instead:
echo "key length: ${#VULNETIX_API_KEY}"
vulnetix auth status | head -n 6
Do not persist the workspace after a --store project login. If the native GitHub Action wrote ./.vulnetix/credentials.json, that path must not appear in any upload-artifact, cache key, or subsequent docker build context.
- name: Remove workspace credentials
if: always()
run: rm -f .vulnetix/credentials.json
Scope tokens per pipeline and rotate them. See Rotation & Revocation.
Assert on the credential source when a pipeline must not silently degrade to community access:
vulnetix auth status | grep -q 'environment' || {
echo "expected an environment credential; refusing to continue"; exit 1;
}
Without this check, a typo in a secret name produces a scan that runs, reports fewer results at community rate limits, and exits 0.