Back to tools

N3X Vault

A self-hosted password manager with browser-side end-to-end encryption, YubiKey/FIDO2 and no server access to plaintext secrets.

SELF-HOSTED · E2EESECURITY ALPHAv0.1.0-alpha.1

Your secrets. Your server. Keys stay on your side.

N3X Vault is an independent password manager with its own protocol and browser-side end-to-end encryption. The server never receives the master password or an unwrapped account key.

Release status: the security alpha is suitable for protocol review, development and controlled testing. Do not store production secrets yet.

Application interface

A real product interface — open any preview to inspect it at full size.

1 / 3

Encrypted vault

Logins, secure notes and TOTP secrets are encrypted in the browser before reaching your own server.

Managed hosting by N3X

Want to use it without maintaining the server?

N3X can deploy and maintain this application, from initial setup and SSL to updates, monitoring and backups.

Ask about N3X hosting

Explicitly designed security boundaries

Client-side encryption

Argon2id, HKDF and XChaCha20-Poly1305 protect data before it reaches the API.

YubiKey / FIDO2

WebAuthn PRF unlock, hardware-key inventory and a two-device readiness policy.

Recovery kit

Encrypted export and resumable restore into an empty installation without a hidden universal key.

Data-blind API

The Rust API and PostgreSQL store ciphertext, protocol parameters and minimal operational metadata.

Short sessions

Fifteen-minute browser sessions remain memory-only and are revocable server-side.

Hardened Compose

A private database network, dropped capabilities, read-only filesystems and volume-backed runtime secrets.

DOCKER CONFIGURATOR

Compose bound to the correct WebAuthn domain

Set the final hostname before enrolling a YubiKey. Changing the RP ID later invalidates existing hardware credentials.

Use the final reverse-proxy domain, for example vault.company.com.

Security alpha: use for controlled testing only. An independent cryptographic review and penetration test are required before production secrets.
Public address
https://vault.example.com
WebAuthn RP ID
vault.example.com
Reverse proxy target
http://127.0.0.1:8793
Health check
http://127.0.0.1:8793/api/health

Ready docker-compose.yml

name: n3xvault

x-api-security: &api-security
  read_only: true
  security_opt:
    - no-new-privileges:true
  cap_drop:
    - ALL
  tmpfs:
    - /tmp:size=32m,mode=1777

x-web-security: &web-security
  read_only: true
  security_opt:
    - no-new-privileges:true
  cap_drop:
    - ALL
  tmpfs:
    - /tmp:size=32m,mode=1777
    - /app/.next/cache:size=64m,mode=0700,uid=1001,gid=1001

services:
  bootstrap:
    image: alpine:3.22@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce
    restart: "no"
    command:
      - /bin/sh
      - -ec
      - |
        umask 077
        if [ ! -s /run/n3xvault/database_password ]; then
          od -An -N32 -tx1 /dev/urandom | tr -d ' \n' > /run/n3xvault/database_password
        fi
        if [ ! -s /run/n3xvault/bootstrap_token ]; then
          od -An -N32 -tx1 /dev/urandom | tr -d ' \n' > /run/n3xvault/bootstrap_token
        fi
        chmod 0444 /run/n3xvault/database_password /run/n3xvault/bootstrap_token
    volumes:
      - runtime_secrets:/run/n3xvault
    networks:
      - backend
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL

  postgres:
    image: postgres:17-alpine@sha256:18cfe3ef5e6815560c98237d6216d1e5119702fb0f3894c8785dd58b8bbe5d73
    restart: unless-stopped
    depends_on:
      bootstrap:
        condition: service_completed_successfully
    environment:
      POSTGRES_DB: n3xvault
      POSTGRES_USER: n3xvault
      POSTGRES_PASSWORD_FILE: /run/n3xvault/database_password
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - runtime_secrets:/run/n3xvault:ro
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U n3xvault -d n3xvault"]
      interval: 5s
      timeout: 5s
      retries: 12
    networks:
      - backend

  api:
    image: ghcr.io/nexitpl/n3xvault-api:alpha
    restart: unless-stopped
    depends_on:
      bootstrap:
        condition: service_completed_successfully
      postgres:
        condition: service_healthy
    environment:
      N3XVAULT_BIND: 0.0.0.0:8080
      N3XVAULT_DATABASE_HOST: postgres
      N3XVAULT_DATABASE_NAME: n3xvault
      N3XVAULT_DATABASE_USER: n3xvault
      N3XVAULT_DATABASE_PASSWORD_FILE: /run/n3xvault/database_password
      N3XVAULT_BOOTSTRAP_TOKEN_FILE: /run/n3xvault/bootstrap_token
      N3XVAULT_ENVIRONMENT: ${N3XVAULT_ENVIRONMENT:-development}
      N3XVAULT_PUBLIC_ORIGIN: https://vault.example.com
      N3XVAULT_WEBAUTHN_RP_ID: vault.example.com
      N3XVAULT_SESSION_TTL_SECONDS: ${N3XVAULT_SESSION_TTL_SECONDS:-900}
      N3XVAULT_WEBAUTHN_FLOW_TTL_SECONDS: ${N3XVAULT_WEBAUTHN_FLOW_TTL_SECONDS:-300}
      N3XVAULT_DEVICE_PAIRING_TTL_SECONDS: ${N3XVAULT_DEVICE_PAIRING_TTL_SECONDS:-300}
      RUST_LOG: ${N3XVAULT_LOG_LEVEL:-n3xvault_api=info,tower_http=info}
    volumes:
      - runtime_secrets:/run/n3xvault:ro
    healthcheck:
      test: ["CMD", "curl", "--fail", "--silent", "http://127.0.0.1:8080/health/ready"]
      interval: 15s
      timeout: 3s
      retries: 5
      start_period: 15s
    networks:
      - backend
    <<: *api-security

  web:
    image: ghcr.io/nexitpl/n3xvault-web:alpha
    restart: unless-stopped
    depends_on:
      api:
        condition: service_healthy
    environment:
      N3XVAULT_API_INTERNAL_URL: http://api:8080
    ports:
      - "127.0.0.1:8793:3000"
    healthcheck:
      test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/api/health"]
      interval: 20s
      timeout: 5s
      retries: 5
      start_period: 20s
    networks:
      - frontend
      - backend
    <<: *web-security

networks:
  frontend:
  backend:
    internal: true

volumes:
  postgres_data:
  runtime_secrets:

Startup and bootstrap token

mkdir -p n3xvault
cd n3xvault
# Save the configuration as docker-compose.yml
docker compose up -d
docker compose ps
docker compose run --rm api print-bootstrap-token
curl -fsS http://127.0.0.1:8793/api/health

Changelog

    • ReleaseFirst security-alpha release for controlled testing and protocol review — not yet intended for production secrets.
    • NewBrowser-side end-to-end encryption with Argon2id, HKDF and XChaCha20-Poly1305; the server stores encrypted envelopes only.
    • NewSign-in and unlock with a master password or YubiKey/FIDO2 WebAuthn PRF, including a two-hardware-key readiness policy.
    • NewEncrypted recovery kit and safe resumable restore into an empty instance without a universal recovery key.

USER GUIDE

N3X Vault user guide

Installation, getting started, settings and troubleshooting.

N3X Vault

N3X Vault is a self-hosted password manager with client-side encryption.

Security alpha: use this version only for controlled testing. It is not approved for production secrets.

Requirements and installation

You need a server with Docker Compose, a browser and the final HTTPS domain. PostgreSQL is included. Hardware unlock requires a compatible browser and a FIDO2 key supporting WebAuthn PRF; password unlock remains available.

  1. Set the final domain, bind address and port in the Compose configurator.
  2. Download the generated file and save it as docker-compose.yml in a dedicated server directory.
  3. Run these commands in that directory:
docker compose up -d
docker compose ps
docker compose run --rm api print-bootstrap-token
  1. Point the HTTPS reverse proxy at the selected address and port (8793 by default). Open the configured domain, enter the token and create the first account.

The public application URL and WebAuthn RP ID must match the final domain before enrolling keys. Changing the RP ID later prevents existing hardware credentials from working.

Treat the bootstrap token as a password. Do not store it in public logs or the stack definition. Owner registration closes after the first account is created.

Read the bootstrap token in Portainer

Portainer may not expose the Compose working directory. The token already exists in the persistent volume; read it through the running API container.

docker ps \
  --filter label=com.docker.compose.service=api \
  --format 'table {{.Names}}\t{{.Label "com.docker.compose.project"}}'

Select the API container belonging to the correct stack and replace PORTAINER_API_CONTAINER with its name:

docker exec PORTAINER_API_CONTAINER \
  /usr/local/bin/n3xvault-api print-bootstrap-token

Alternatively, open Portainer → Containers → API container → Console → /bin/sh and run:

/usr/local/bin/n3xvault-api print-bootstrap-token

If the API is unavailable, find the correct stack's bootstrap container. Replace PORTAINER_BOOTSTRAP_CONTAINER with its name; the temporary container reads only the existing volume:

docker ps -a \
  --filter label=com.docker.compose.service=bootstrap \
  --format 'table {{.Names}}\t{{.Label "com.docker.compose.project"}}'
docker run --rm \
  --volumes-from PORTAINER_BOOTSTRAP_CONTAINER:ro \
  alpine:3.22 \
  cat /run/n3xvault/bootstrap_token

Use the Alpine image reference pinned in the downloaded Compose file if alpine:3.22 is not available locally. Do not remove runtime_secrets or generate a new token to troubleshoot this issue.

Hardware keys and recovery

After unlocking with your password, open Security, enroll a compatible FIDO2 key and complete the second touch required to configure unlock. Enroll two keys before relying on this method.

With the vault unlocked, select Recovery kit. Store the encrypted .n3xvault file on protected offline media.

Restoration requires an empty installation, the recovery-kit file, the destination's bootstrap token and either the original master password or an enrolled key with PRF. Hardware-key restoration requires the same RP ID. When changing domains, restore using the password and enroll keys again. Test restoration on an isolated empty installation.

Maintenance and troubleshooting

Take consistent backups of postgres_data and runtime_secrets, and keep a separate recovery kit. Follow the release notes when updating, preserving volumes and the existing domain:

docker compose pull
docker compose up -d
docker compose ps

For startup failures, check container status and docker compose logs --tail=100 api web. If hardware unlock fails, check HTTPS, the domain, RP ID and browser PRF support. Switching between localhost and 127.0.0.1 also changes the credential context.

Encryption and decryption happen in the client. A deployment or session token alone cannot decrypt vault contents.