Skip to content

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. Parse — nanokit.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).

yaml
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:

yaml
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.

yaml
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
yaml
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:

yaml
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.

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.

yaml
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
# .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):

bash
# 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:

bash
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:

bash
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:

bash
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:

yaml
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:

yaml
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:

yaml
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:

yaml
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):

yaml
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:

bash
# 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:

yaml
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.

bash
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:

bash
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:

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:

yaml
# ❌ 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:

hcl
# 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:

gitignore
# 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.