Skip to Content
SecuritySecret Management

Secrets & Security

Nanokit treats secrets as first-class citizens. Rather than embedding credentials directly in configuration files, every sensitive value is expressed as a URI reference that the platform resolves at runtime — keeping plain text out of your repository and out of local state files.


Secret Resolution Pipeline

When you deploy or start a service, Nanokit resolves secret references through a deterministic pipeline:

nanokit.yml secrets: block URI resolution ┌─────────────────────────────────┐ │ env:// → host environment │ │ vault:// → HashiCorp Vault │ │ db:// → connection string│ │ service:// → service host │ └─────────────────────────────────┘ Injected into container env (never written to state files)
  1. Parsenanokit.yml is read and every value that starts with a URI scheme (env://, vault://, db://, service://) is flagged for resolution.
  2. Resolve — The CLI contacts the appropriate backend (host env, Vault, or the database registry) and fetches the raw secret value.
  3. Inject — Resolved values are passed directly to the container runtime as environment variables.
  4. No persistence — Plain-text secrets are never written to .nanokit/services-state.json or any other local state file.

[!IMPORTANT] Nanokit never stores resolved secret values on disk. If you need a local snapshot for development, use nkapp secrets pull — the output goes to .env.local, which should always be gitignored.


Secret References

Secrets are declared in the secrets: block of nanokit.yml, either at the global level (shared across all environments) or scoped to a specific environment block.

env:// — Host Environment Variable

Reads a value from the environment where the Nanokit CLI is running (your shell, CI runner, or VPS).

secrets: DATABASE_URL: env://DATABASE_URL STRIPE_SECRET_KEY: env://STRIPE_SECRET_KEY API_KEY: env://MY_APP_API_KEY

The environment variable name after env:// is the source variable on the host; the map key (DATABASE_URL, STRIPE_SECRET_KEY, etc.) becomes the variable name injected into the container.

Environment-scoped override:

environments: production: secrets: DATABASE_URL: env://PROD_DATABASE_URL staging: secrets: DATABASE_URL: env://STAGING_DATABASE_URL

[!TIP] Use ${VAR_NAME:-default} syntax anywhere in nanokit.yml for YAML-level expansion with fallbacks. This is evaluated before URI resolution and is useful for non-secret configuration values.

services: api: image: myapp:${IMAGE_TAG:-latest} replicas: ${REPLICA_COUNT:-2}

vault:// — HashiCorp Vault

Fetches a secret from HashiCorp Vault using the KV secrets engine. The URI format is:

vault://<mount>/<path>#<key>
  • mount/path — the KV path in Vault (e.g., secret/myapp/production)
  • #key — the field name within the KV secret object
secrets: DATABASE_PASSWORD: vault://secret/myapp/production#db_password SMTP_PASSWORD: vault://secret/myapp/production#smtp_password JWT_SECRET: vault://secret/myapp/shared#jwt_signing_key

Mixed global + per-environment secrets:

secrets: JWT_SECRET: vault://secret/myapp/shared#jwt_signing_key environments: production: secrets: DATABASE_PASSWORD: vault://secret/myapp/production#db_password REDIS_URL: vault://secret/myapp/production#redis_url staging: secrets: DATABASE_PASSWORD: vault://secret/myapp/staging#db_password REDIS_URL: vault://secret/myapp/staging#redis_url

[!NOTE] Vault requires the VAULT_ADDR and VAULT_TOKEN environment variables to be set in the shell where you run nkapp commands. See Vault Integration for details.


db:// — Database Connection String

The db:// scheme maps a named database connection (registered in your Nanokit project) to an environment variable containing the full connection string. This lets services consume a single DATABASE_URL-style variable without embedding credentials in YAML.

secrets: DATABASE_URL: db://postgres-primary CACHE_URL: db://redis-cache

The connection string — including host, port, credentials, and database name — is resolved from the Nanokit platform’s database registry and injected at runtime.


service:// — Sibling Service Host Name

The service:// scheme maps the hostname of a sibling service in your Nanokit project to an environment variable. This allows containers to find and connect to other workloads dynamically on the private virtual network.

services: api-auth: env: SMTP_HOST: service://mail

Nanokit automatically translates service://<service-name> into the exact container hostname matching the active environment (e.g. nk-nanokit-local-mail), ensuring zero hardcoding of container names or virtual network details.


The .env File Strategy

Nanokit maps each environment to its own .env file:

EnvironmentFilePurpose
local.envResolved values for local development
any other (e.g. stage).env.<environment>Resolved values for that environment (e.g., .env.stage)

What to gitignore

All .env* files may contain resolved plain-text credentials after a nkapp secrets pull — keep them out of version control:

# .gitignore (nkapp init adds these automatically) .env .env.* .nanokit/

[!CAUTION] Never commit a .env file containing real credentials. nkapp init adds the patterns above to .gitignore for you — keep them there.

nkapp secrets pull

Resolves all secret URIs for the target environment and writes the plain-text values to the matching .env file (.env for local, .env.<env> otherwise):

# Pull secrets for local development → .env nkapp secrets pull # Pull secrets for a specific environment → .env.production nkapp secrets pull -e production

Pushing values back (nkapp env sync)

To upload local values back to a remote secret store, use nkapp env sync:

nkapp env sync -e stage --direction push

[!NOTE] Push only applies to keys that reference HashiCorp Vault (vault://…). env://-backed keys and brand-new local variables have no remote target and are reported as skipped.

nkapp secrets list

Lists all declared secret keys for an environment (global and environment-specific), so you can verify what is configured without exposing credentials:

nkapp secrets list -e production

Auto-Injected Variables

Nanokit automatically injects the following variables into every container, regardless of what is declared in secrets::

VariableDescription
NANOKIT_PROJECTThe project slug from nanokit.yml
NANOKIT_SERVICEThe name of the running service
NANOKIT_ENVThe active deployment environment (e.g., production, staging)

These variables are always available and can be used for runtime environment detection, structured logging, and tracing.


Vault Integration

Prerequisites

Set the following environment variables before running any nkapp command that resolves vault:// references:

export VAULT_ADDR="https://vault.example.com:8200" export VAULT_TOKEN="hvs.XXXXXXXXXXXXXXXXXXXXXXXX"

For CI environments, inject these as secrets in your pipeline configuration rather than hardcoding them.

KV Path Structure

Nanokit expects Vault’s KV v2 secrets engine. A recommended path structure:

secret/ ├── myapp/ │ ├── shared/ # Secrets shared across environments │ │ ├── jwt_signing_key │ │ └── encryption_key │ ├── production/ # Production-only secrets │ │ ├── db_password │ │ └── smtp_password │ └── staging/ # Staging-only secrets │ ├── db_password │ └── smtp_password

Corresponding nanokit.yml:

secrets: JWT_SECRET: vault://secret/myapp/shared#jwt_signing_key environments: production: secrets: DATABASE_PASSWORD: vault://secret/myapp/production#db_password SMTP_PASSWORD: vault://secret/myapp/production#smtp_password staging: secrets: DATABASE_PASSWORD: vault://secret/myapp/staging#db_password SMTP_PASSWORD: vault://secret/myapp/staging#smtp_password

[!NOTE] The #key fragment in a vault:// URI refers to the field name within the Vault KV secret, not a URL anchor. For example, vault://secret/myapp/production#db_password reads the db_password field from the KV document stored at secret/myapp/production.


SSH Key Security

Nanokit uses SSH to connect to VPS hosts for deployment operations. Two key management modes are supported.

File-Based SSH Keys

Point the CLI to a key file in your nanokit.yml:

hosts: vps-primary: address: 203.0.113.42 user: deploy ssh_key: ~/.ssh/id_ed25519

Keys are read directly from disk and never copied to state files.

Vault-Backed Ephemeral Keys

For zero-trust environments, SSH private keys can be stored in Vault and fetched at connection time:

hosts: vps-primary: address: 203.0.113.42 user: deploy ssh_key: vault://secret/infra/ssh#private_key

When an ephemeral key is used:

  1. The private key is fetched from Vault over TLS.
  2. It is written to a temporary file with 0600 permissions.
  3. The SSH connection is established using that file.
  4. The temporary file is deleted immediately after the connection closes.

[!IMPORTANT] Ephemeral vault-backed SSH keys are never left on disk between operations. If the CLI crashes during a connection, the temp file path is recorded in the process’s cleanup handler so it is removed on the next invocation.

SSH Agent Forwarding

SSH agent forwarding is supported for workflows that require chained SSH hops:

hosts: vps-primary: address: 203.0.113.42 user: deploy forward_agent: true

[!WARNING] Enable agent forwarding only on trusted hosts. A compromised remote host with agent forwarding enabled could use your local keys.


TLS Certificates

Automatic ACME (Public Domains)

Nanokit uses Caddy as its built-in reverse proxy. For any service exposed on a public domain, Caddy automatically provisions and renews TLS certificates via ACME (Let’s Encrypt or a configured CA):

services: api: port: 3000 domain: api.example.com # TLS is provisioned automatically

No manual certificate management is required. Caddy handles HTTP-01 or TLS-ALPN-01 challenges, stores certificates, and renews them before expiry.

Local Development CA

For local development environments, Caddy generates a self-signed Certificate Authority (CA) the first time it starts. This CA and its signed leaf certificates are stored in:

.nanokit/gateway/ ├── root.crt # Self-signed CA certificate ├── root.key # CA private key └── ...

To avoid browser TLS warnings, trust the local CA once:

# macOS sudo security add-trusted-cert -d -r trustRoot \ -k /Library/Keychains/System.keychain \ .nanokit/gateway/root.crt # Linux (Debian/Ubuntu) sudo cp .nanokit/gateway/root.crt /usr/local/share/ca-certificates/nanokit-local.crt sudo update-ca-certificates

[!TIP] The .nanokit/gateway/ directory contains generated local certificates and should be added to .gitignore. The CA key should never leave the machine it was generated on.

Custom Certificate Mounts

Some services (such as a Stalwart mail server) require explicit TLS certificate files. Mount your certificates into the container via volumes:

services: mail: image: stalwartlabs/mail-server:latest volumes: - ./certs/fullchain.pem:/opt/stalwart-mail/etc/certs/tls.crt:ro - ./certs/privkey.pem:/opt/stalwart-mail/etc/certs/tls.key:ro environment: SERVER_TLS_CERT: /opt/stalwart-mail/etc/certs/tls.crt SERVER_TLS_KEY: /opt/stalwart-mail/etc/certs/tls.key

[!CAUTION] Certificate private keys mounted via volumes must never be stored in version control. Keep them in a secrets manager (e.g., Vault) and provision them to the host out-of-band, or use vault:// references with a secrets sync sidecar.


API Token Storage

nkapp auth login authenticates with the Nanokit platform (Vestauth) and stores resulting tokens locally.

nkapp auth login

Provider-specific API tokens (Cloudflare, AWS, etc.) are written to:

~/.nanokit/auth.json # global scope .nanokit/auth.json # project scope (takes precedence)

This file contains bearer tokens for cloud provider APIs used during deployment (DNS record management, object storage, etc.). It is created automatically and should be protected:

chmod 600 .nanokit/auth.json

[!WARNING] .nanokit/auth.json must be added to .gitignore. Committing API tokens to a repository — even a private one — is a serious security risk.

Add to your .gitignore:

.nanokit/auth.json .nanokit/gateway/ .env.local .env.*.local .env.production

Security Best Practices

Never Commit Secrets

Use URI references in nanokit.yml instead of inline values:

# ❌ Never do this secrets: DATABASE_PASSWORD: "s3cr3t-p4ssword" # ✅ Always use a URI reference secrets: DATABASE_PASSWORD: vault://secret/myapp/production#db_password # or DATABASE_PASSWORD: env://DATABASE_PASSWORD

Runtime Injection, Not Build-Time Baking

Avoid baking secrets into container images with ARG / ENV directives in Dockerfiles. Let Nanokit inject them at runtime so that the same image artifact can be deployed to any environment without rebuilding.

Principle of Least Privilege for Vault

Issue Vault tokens with policies scoped to only the paths your application needs:

# vault-policy-myapp.hcl path "secret/data/myapp/production/*" { capabilities = ["read"] }

Avoid using root tokens or broadly scoped tokens in production.

Rotate Secrets Regularly

Because secrets are resolved at deployment time (not baked in), rotation is straightforward:

  1. Update the value in Vault or the host environment.
  2. Redeploy the service with nkapp deploy.
  3. The new secret is picked up automatically — no image rebuild needed.

Gitignore Reference

A complete .gitignore block for Nanokit projects:

# Nanokit — never commit these .nanokit/auth.json .nanokit/gateway/ .env.local .env.*.local .env.production .env.staging *.pem *.key

[!NOTE] It is safe to commit .env (baseline non-secret defaults) and .env.example (a documented template with placeholder values). Never commit files containing real credentials, tokens, or private keys.