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)- Parse —
nanokit.ymlis read and every value that starts with a URI scheme (env://,vault://,db://,service://) is flagged for resolution. - Resolve — The CLI contacts the appropriate backend (host env, Vault, or the database registry) and fetches the raw secret value.
- Inject — Resolved values are passed directly to the container runtime as environment variables.
- No persistence — Plain-text secrets are never written to
.nanokit/services-state.jsonor 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_KEYThe 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 innanokit.ymlfor 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_keyMixed 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_ADDRandVAULT_TOKENenvironment variables to be set in the shell where you runnkappcommands. 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-cacheThe 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://mailNanokit 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:
| Environment | File | Purpose |
|---|---|---|
local | .env | Resolved 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
.envfile containing real credentials.nkapp initadds the patterns above to.gitignorefor 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 productionPushing 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 productionAuto-Injected Variables
Nanokit automatically injects the following variables into every container, regardless of what is declared in secrets::
| Variable | Description |
|---|---|
NANOKIT_PROJECT | The project slug from nanokit.yml |
NANOKIT_SERVICE | The name of the running service |
NANOKIT_ENV | The 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_passwordCorresponding 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
#keyfragment in avault://URI refers to the field name within the Vault KV secret, not a URL anchor. For example,vault://secret/myapp/production#db_passwordreads thedb_passwordfield from the KV document stored atsecret/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_ed25519Keys 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_keyWhen an ephemeral key is used:
- The private key is fetched from Vault over TLS.
- It is written to a temporary file with
0600permissions. - The SSH connection is established using that file.
- 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 automaticallyNo 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 loginProvider-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.jsonmust 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.productionSecurity 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_PASSWORDRuntime 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:
- Update the value in Vault or the host environment.
- Redeploy the service with
nkapp deploy. - 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.