Skip to content

Authentication

NORA supports multiple authentication methods: htpasswd-based credentials, OIDC workload identity (for CI/CD systems), and API tokens. Authentication is disabled by default and must be explicitly enabled.


Set the NORA_AUTH_ENABLED environment variable or configure it in config.toml:

Terminal window
# Environment variable
export NORA_AUTH_ENABLED=true
config.toml
[auth]
enabled = true
htpasswd_file = "users.htpasswd"
token_storage = "data/tokens"

NORA uses Apache-compatible htpasswd files for user management. Create a password file using htpasswd (from apache2-utils) or any compatible tool:

Terminal window
# Install htpasswd (Debian/Ubuntu)
apt-get install apache2-utils
# Create file with first user
htpasswd -Bc users.htpasswd admin
# Add additional users
htpasswd -B users.htpasswd developer
htpasswd -B users.htpasswd ci-bot

The -B flag uses bcrypt hashing, which is the recommended algorithm.

Docker:

Terminal window
docker run -d \
--name nora \
-p 4000:4000 \
-v /data/nora:/data \
-v /etc/nora/users.htpasswd:/app/users.htpasswd:ro \
-e NORA_AUTH_ENABLED=true \
-e NORA_AUTH_HTPASSWD_FILE=/app/users.htpasswd \
ghcr.io/getnora-io/nora:latest

Kubernetes:

apiVersion: v1
kind: Secret
metadata:
name: nora-htpasswd
type: Opaque
stringData:
users.htpasswd: |
admin:$2y$05$...
ci-bot:$2y$05$...
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: nora
spec:
template:
spec:
containers:
- name: nora
env:
- name: NORA_AUTH_ENABLED
value: "true"
- name: NORA_AUTH_HTPASSWD_FILE
value: /etc/nora/users.htpasswd
volumeMounts:
- name: htpasswd
mountPath: /etc/nora
readOnly: true
volumes:
- name: htpasswd
secret:
secretName: nora-htpasswd

When NORA_AUTH_ANONYMOUS_READ=true, unauthenticated users can pull/download artifacts, but authentication is still required for push/upload operations.

Terminal window
export NORA_AUTH_ENABLED=true
export NORA_AUTH_ANONYMOUS_READ=true
config.toml
[auth]
enabled = true
anonymous_read = true

This is useful for organizations that want open read access (e.g., shared libraries) while restricting who can publish artifacts.

OperationAnonymous Read = falseAnonymous Read = true
Pull / DownloadAuth requiredNo auth needed
Push / UploadAuth requiredAuth required
Delete / AdminAuth requiredAuth required

NORA supports OIDC (OpenID Connect) workload identity for CI/CD systems like GitHub Actions and GitLab CI. This allows pipelines to authenticate without storing long-lived secrets — the CI platform issues a short-lived JWT that NORA validates directly.

  1. Your CI platform (GitHub Actions, GitLab CI) issues a short-lived OIDC token with claims identifying the workflow, repository, and branch.
  2. The pipeline sends this token as a Bearer token to NORA.
  3. NORA validates the JWT signature against the provider’s JWKS endpoint, checks the issuer, audience, and lifetime, then maps the sub claim to a role via configured rules.

No static secrets are stored in your CI — only the OIDC audience needs to be configured.

config.toml
[auth]
enabled = true
[auth.oidc]
enabled = true
leeway_secs = 60 # Clock skew tolerance (default: 60)
jwks_cache_secs = 300 # JWKS key cache TTL (default: 300)
[[auth.oidc.providers]]
name = "github-actions"
issuer = "https://token.actions.githubusercontent.com"
audience = "nora"
algorithms = ["RS256", "ES256"]
max_token_lifetime_secs = 900
enabled = true
# jwks_uri = "https://..." # Optional: explicit JWKS endpoint override
# Role rules: first match wins. Glob patterns on the `sub` claim.
[[auth.oidc.providers.role_rules]]
pattern = "repo:myorg/*:ref:refs/heads/main"
role = "write"
[[auth.oidc.providers.role_rules]]
pattern = "repo:myorg/*"
role = "read"

Environment variable override:

Terminal window
export NORA_AUTH_OIDC_ENABLED=true
  1. Configure NORA with the GitHub OIDC issuer (as shown above).
  2. Add the id-token: write permission to your workflow.
  3. Use the token directly — no secrets needed.
name: Publish to NORA
on:
push:
branches: [main]
permissions:
id-token: write
contents: read
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get OIDC Token
id: oidc
uses: actions/github-script@v7
with:
script: |
const token = await core.getIDToken('nora');
core.setOutput('token', token);
- name: Push to NORA
run: |
# Docker
echo "${{ steps.oidc.outputs.token }}" | \
docker login registry.example.com -u oidc --password-stdin
docker push registry.example.com/myapp:${{ github.sha }}
# Or npm
echo "//registry.example.com/:_authToken=${{ steps.oidc.outputs.token }}" > .npmrc
npm publish
config.toml
[[auth.oidc.providers]]
name = "gitlab-ci"
issuer = "https://gitlab.com" # or your self-hosted GitLab URL
audience = "nora"
algorithms = ["RS256"]
max_token_lifetime_secs = 7200 # GitLab tokens have 1hr lifetime
enabled = true
[[auth.oidc.providers.role_rules]]
pattern = "project_path:mygroup/*:ref_type:branch:ref:main"
role = "write"
[[auth.oidc.providers.role_rules]]
pattern = "project_path:mygroup/*"
role = "read"
.gitlab-ci.yml
publish:
image: docker:latest
id_tokens:
NORA_TOKEN:
aud: nora
script:
- echo "$NORA_TOKEN" | docker login $NORA_REGISTRY -u oidc --password-stdin
- docker push $NORA_REGISTRY/myapp:$CI_COMMIT_SHA

Role rules use glob patterns matched against the JWT sub claim. The first matching rule wins.

PatternMatches
repo:myorg/*:ref:refs/heads/mainAny repo in myorg, main branch only
repo:myorg/*Any repo in myorg, any branch
project_path:mygroup/*:ref_type:branch:ref:mainGitLab: any project, main branch
*Everything (catch-all)

Available roles: read, write, admin.

NORA resolves the JWKS (signing keys) endpoint for each provider using this order:

  1. Explicit jwks_uri in provider config (highest priority)
  2. OIDC Discovery via {issuer}/.well-known/openid-configuration
  3. Fallback to {issuer}/.well-known/jwks.json

Most providers work automatically via step 2. Use jwks_uri when:

  • Your provider’s JWKS endpoint doesn’t follow the standard path
  • You need to point to an internal proxy or mirror
  • OIDC discovery is blocked by a firewall
[[auth.oidc.providers]]
name = "custom-idp"
issuer = "https://auth.internal.corp"
jwks_uri = "https://auth.internal.corp/keys/jwks.json" # explicit override
audience = "nora"
algorithms = ["RS256"]
  • Algorithm whitelist: Only RS256 and ES256 by default. Symmetric algorithms (HS256/HS384/HS512) are always rejected.
  • Strict issuer binding: NORA never follows jku/x5u headers from the token. Keys are always fetched from the configured issuer’s JWKS endpoint.
  • Token lifetime ceiling: Tokens with exp - iat exceeding max_token_lifetime_secs are rejected, even if not yet expired.
  • Stale JWKS fallback: If JWKS refresh fails (network issue), NORA serves stale cached keys to maintain availability.
  • Per-provider kill switch: Disable a provider instantly with enabled = false without removing its configuration.

You can configure multiple OIDC providers simultaneously:

[[auth.oidc.providers]]
name = "github-actions"
issuer = "https://token.actions.githubusercontent.com"
audience = "nora"
# ...
[[auth.oidc.providers]]
name = "gitlab-ci"
issuer = "https://gitlab.example.com"
audience = "nora"
# ...

NORA routes each token to the correct provider based on the iss claim.


API tokens provide programmatic access without exposing htpasswd credentials. Tokens are prefixed with nra_ for easy identification and use Argon2 hashing.

RolePermissions
readPull and download artifacts only
writePull, push, and download artifacts
adminFull access including token management
Terminal window
curl -X POST http://localhost:4000/api/tokens \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "your-password",
"role": "write",
"ttl_days": 90,
"description": "CI/CD pipeline token"
}'

Response:

{
"token": "nra_a1b2c3d4e5f6...",
"expires_in_days": 90
}

Save the token value immediately — it is only shown once at creation time.

Terminal window
curl -X POST http://localhost:4000/api/tokens/list \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "your-password"
}'

Response:

{
"tokens": [
{
"hash_prefix": "a1b2c3",
"created_at": 1714200000,
"expires_at": 1721976000,
"last_used": 1714300000,
"description": "CI/CD pipeline token",
"role": "write"
}
]
}

Use the hash_prefix from the list response:

Terminal window
curl -X POST http://localhost:4000/api/tokens/revoke \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "your-password",
"hash_prefix": "a1b2c3"
}'

NORA supports standard Docker authentication. When auth is enabled, use docker login before push/pull operations:

Terminal window
# Login with htpasswd credentials
docker login localhost:4000
# Username: admin
# Password: ****
# Login with API token (use token as password, any username)
docker login localhost:4000 -u token -p nra_a1b2c3d4e5f6...

For automated workflows, use --password-stdin:

Terminal window
echo "nra_a1b2c3d4e5f6..." | docker login localhost:4000 -u token --password-stdin

For CI/CD pipelines, prefer OIDC Workload Identity over static API tokens when your CI platform supports it (GitHub Actions, GitLab CI). OIDC eliminates secret management entirely.

If OIDC is not available, use API tokens as shown below.

name: Build and Push
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to NORA
run: |
echo "${{ secrets.NORA_TOKEN }}" | \
docker login registry.example.com -u token --password-stdin
- name: Build and Push
run: |
docker build -t registry.example.com/myapp:${{ github.sha }} .
docker push registry.example.com/myapp:${{ github.sha }}

For non-Docker registries (npm, PyPI, Cargo, etc.), use the token in the appropriate client configuration:

# npm
- name: Publish npm package
env:
NORA_TOKEN: ${{ secrets.NORA_TOKEN }}
run: |
echo "//registry.example.com/:_authToken=${NORA_TOKEN}" > .npmrc
npm publish --registry=https://registry.example.com
# PyPI (twine)
- name: Publish Python package
env:
NORA_TOKEN: ${{ secrets.NORA_TOKEN }}
run: |
twine upload --repository-url https://registry.example.com/pypi/ \
-u token -p "${NORA_TOKEN}" dist/*
stages:
- build
- publish
variables:
NORA_REGISTRY: registry.example.com
build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
- echo "$NORA_TOKEN" | docker login $NORA_REGISTRY -u token --password-stdin
script:
- docker build -t $NORA_REGISTRY/myapp:$CI_COMMIT_SHA .
- docker push $NORA_REGISTRY/myapp:$CI_COMMIT_SHA
publish-maven:
stage: publish
image: maven:3.9
script:
- >
mvn deploy
-DaltDeploymentRepository=nora::https://${NORA_REGISTRY}/maven2
-Dserver.username=token
-Dserver.password=${NORA_TOKEN}

Store NORA_TOKEN as a masked CI/CD variable in GitLab project settings.


  1. Use scoped tokens. Create read tokens for pull-only workloads and write tokens only for pipelines that publish.
  2. Set TTL. Always specify ttl_days when creating tokens. Rotate tokens regularly.
  3. Do not commit tokens. Use CI/CD secrets (GitHub Secrets, GitLab CI Variables) to inject tokens at runtime.
  4. Revoke on compromise. If a token is leaked, revoke it immediately using the API.
  5. Use anonymous read when possible. If your artifacts are not sensitive, enable NORA_AUTH_ANONYMOUS_READ=true to reduce token management overhead.