Key points
- Secrets are declared per step, not per pipeline, so a credential is not injected into steps that have no use for it.
- A declared secret is injected as an environment variable under its bare name.
- Cross-scope references work exactly as they do for variables:
scope/path:NAME. - Declared secret values are masked in run logs.
- A stored secret value can be read back through
GET /v1/secrets/{name}, but only by a caller holdingsecret.read_valueon that secret. Treat that action as the sensitive one. - The same runtime name may not resolve from two different scopes in one step.
Examples
secrets:
- REGISTRY_TOKEN
- platform/shared:SIGNING_KEYname: release-service
description: Build, verify, and publish the payments service.
container_image: alpine:3.20
working_directory: /workspace
timeout: 45m
variables:
- RELEASE_CHANNEL
- platform/shared:ARTIFACT_BUCKET
steps:
- name: build
script: |
make build
echo "1.0.$(date +%s)" > /nopsai/outputs/BUILD_TAG
outputs:
- name: BUILD_TAG
- name: verify
image: golang:1.24
depends_on: [build]
runtime_pool: ci
volumes:
- build-cache:/root/.cache
script: |
go vet ./...
go test ./...
- name: checks
depends_on: [verify]
tasks:
- name: licenses
script: |
./scripts/license-check.sh
- name: sbom
script: |
./scripts/sbom.sh > /workspace/sbom.json
sha256sum /workspace/sbom.json | cut -d' ' -f1 > /nopsai/outputs/SBOM_DIGEST
outputs:
- name: SBOM_DIGEST
- name: report
depends_on: [checks.licenses, checks.sbom]
variables:
SBOM_DIGEST: $steps.checks.sbom.outputs.SBOM_DIGEST
script: |
echo "checks complete"
echo "sbom $SBOM_DIGEST"
- name: package
depends_on: [checks]
secrets:
- REGISTRY_TOKEN
- platform/shared:SIGNING_KEY
variables:
BUILD_TAG: $steps.build.outputs.BUILD_TAG
script: |
./scripts/package.sh "$BUILD_TAG"
echo "channel $RELEASE_CHANNEL, bucket $ARTIFACT_BUCKET"
./scripts/mint-token.sh > /nopsai/outputs/PUBLISH_TOKEN
./scripts/sign.sh --key "$SIGNING_KEY" --token "$REGISTRY_TOKEN"
outputs:
- name: PUBLISH_TOKEN
sensitive: true secrets:
- registry/tokenField reference
steps[].secretsstepstring[]OptionalNone
Scoped secret references injected into the step as environment variables.
secrets:
- REGISTRY_TOKEN
- platform/shared:SIGNING_KEY- Names must match ^[A-Za-z0-9_.-]+$.
- The same runtime name may not resolve from two different scopes in one step.
Declared secret values are masked in run logs.
services/nopsai/pkg/validation/pipeline.go
How it works
Declaring a secret is an access statement. It is visible in review, it is auditable, and it is the thing a security reviewer reads to answer "what can this step reach?".
For a secret that should live in Git rather than the database, encrypt it first through POST /v1/secrets/encrypt; the envelope is safe to commit and resolves at run time.
Masking covers declared secrets and outputs marked sensitive. A value the pipeline constructs itself — a token pasted into a log line by a script — is not something the platform can know about.
Implementation evidence
pkg/models/runtime_refs.goScoped secret reference parsing.
doc/credential-management.mdEncrypted registry, GitOps envelopes, rotation, and AAA.

