Skip to content

Deployment

github-actions[bot] edited this page Oct 28, 2026 · 15 revisions

Veyport Deployment Guide

Runtime identity and access paths

The Hub serves HTTP on :8081, agent mTLS gRPC on :9090, and the SSH gateway on :2222 by default. Expose the gateway through TCP when enabling vey ssh; HTTPS-only reverse proxy routes do not carry SSH. Managed nodes need the agent's outbound Hub connection; gateway shells do not use the node's own SSH daemon. See Proxy Configuration and SSH Gateway.

The standard agent systemd installer omits User=, so the agent runs as root. Local Veyport admin shells inherit that identity in both the browser and CLI. LDAP shells instead use the mapped host account, which must be resolvable on each managed node. Changing the agent service identity also changes the OS permissions available to its file operations and inherited admin shells; see Architecture.

TL;DR

  • What: Docker-first deployment with a single docker-compose.yml; bare-metal binary also available
  • Who: The person deploying Veyport Hub and agents
  • Why: One file to download, one command to run - no cloning, no building
  • Where: Hub on a central server (Docker or bare-metal); agents on each managed server
  • When: curl the compose file, docker compose up -d, open the browser
  • How: Docker Compose (primary) or build from source (contributors)

Prerequisites

  • Docker and Docker Compose (v2+)
  • A Linux server (amd64 or arm64)
  • A domain name pointing to your server (for production use with TLS)

Quick Start with Docker

# Download the compose file
curl -O https://raw.githubusercontent.com/lorconksu/veyport/main/docker-compose.yml

# Start Veyport
docker compose up -d

The Hub starts on port 8081 (HTTP) and 9090 (gRPC). Open http://localhost:8081 in your browser to create the initial admin account and set up 2FA. Then run the one-liner shown in the UI on each server you want to manage.

That's it. No cloning, no building, no dependencies.


Docker Compose Configuration

The default docker-compose.yml:

services:
  veyport:
    image: yiucloud/veyport:latest
    container_name: veyport
    ports:
      - "8081:8081"   # HTTP - web UI and REST API
      - "9090:9090"   # gRPC - agent connections
    volumes:
      - veyport-data:/data   # SQLite DB and persistent state
    restart: unless-stopped

volumes:
  veyport-data:

Configuration details

Setting Value Notes
Image yiucloud/veyport:latest Pin to a specific tag (e.g. yiucloud/veyport:vX.Y.Z) for reproducible deployments
HTTP port 8081 Web UI and REST API
gRPC port 9090 Agent connections - must be reachable by agents
Data volume veyport-data mounted at /data Contains the SQLite database (/data/veyport.db) and all persistent state
Binary path /app/veyport The Hub binary inside the container

Pinning a version

Replace latest with a specific version tag to avoid unexpected upgrades:

image: yiucloud/veyport:vX.Y.Z

The published command-passthrough prototype is Hub 2.0.50-prototype.9 / tag prototype-ssh-passthrough.9, with CLI and agent prototype 9 artifacts. Older CLI prototype 6 builds are also compatible with the Hub. These prototype builds are not a stable or main-branch release. Check current publication and qualification on the release page, verify compatible builds and target agent support, and follow the owner-approved staging and rollback procedure. This publication information is not new runtime qualification evidence.


Running Behind Traefik

Docker Compose with Traefik labels

If Traefik is also running in Docker, add labels to the Veyport service. Create a docker-compose.traefik.yml:

services:
  veyport:
    image: yiucloud/veyport:latest
    container_name: veyport
    volumes:
      - veyport-data:/data
    restart: unless-stopped
    labels:
      # HTTP router - web UI and REST API
      - "traefik.enable=true"
      - "traefik.http.routers.veyport.rule=Host(`veyport.example.com`)"
      - "traefik.http.routers.veyport.entrypoints=websecure"
      - "traefik.http.routers.veyport.tls.certresolver=letsencrypt"
      - "traefik.http.routers.veyport.service=veyport"
      - "traefik.http.services.veyport.loadbalancer.server.port=8081"

      # gRPC router - agent connections (path prefix matches the proto package)
      - "traefik.http.routers.veyport-grpc.rule=Host(`veyport.example.com`) && PathPrefix(`/veyport.v1.`)"
      - "traefik.http.routers.veyport-grpc.entrypoints=websecure"
      - "traefik.http.routers.veyport-grpc.tls.certresolver=letsencrypt"
      - "traefik.http.routers.veyport-grpc.service=veyport-grpc"
      - "traefik.http.services.veyport-grpc.loadbalancer.server.port=9090"
      - "traefik.http.services.veyport-grpc.loadbalancer.server.scheme=h2c"
    networks:
      - traefik

volumes:
  veyport-data:

networks:
  traefik:
    external: true

No port mappings are needed when Traefik handles routing - traffic flows through the Docker network.

Bare-metal Traefik (file provider)

If Traefik runs outside Docker, use a file provider config. Create /etc/traefik/dynamic/veyport.yml:

http:
  routers:
    # HTTP and REST API traffic
    veyport:
      rule: "Host(`veyport.example.com`)"
      entryPoints:
        - websecure
      tls:
        certResolver: letsencrypt
      service: veyport

    # gRPC traffic from agents (path prefix matches the proto package)
    veyport-grpc:
      rule: "Host(`veyport.example.com`) && PathPrefix(`/veyport.v1.`)"
      entryPoints:
        - websecure
      tls:
        certResolver: letsencrypt
      service: veyport-grpc

  services:
    veyport:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:8081"

    # h2c (cleartext HTTP/2) is required for gRPC backends without TLS
    veyport-grpc:
      loadBalancer:
        servers:
          - url: "h2c://127.0.0.1:9090"

Make sure your main traefik.yml has the file provider enabled:

providers:
  file:
    directory: /etc/traefik/dynamic
    watch: true

Traefik handles TLS termination (Let's Encrypt). The Hub sees plain HTTP on port 8081 and cleartext gRPC (h2c) on port 9090. The PathPrefix('/veyport.v1.') matcher routes agent gRPC connections correctly because gRPC uses the proto package path as the HTTP/2 :path header.


Browser application access

Application access is optional. The Hub accepts --application-domain and --application-portal-origin at startup. Set both when enabling application access; use the exact HTTPS origin of the Veyport portal, for example:

/app/veyport --addr :8081 --grpc-addr :9090 --db /data/veyport.db \
  --agent-bin-dir /app --application-domain apps.example.com \
  --application-portal-origin https://veyport.example.com

For Docker Compose, pass the same flags as the service command (the image entrypoint is /app/veyport). Keep the application's domain namespace dedicated to Veyport application hosts. Configure DNS and the HTTPS edge so each configured application hostname in *.apps.example.com reaches the Hub's HTTP listener, with a certificate valid for the exact hostname (typically a wildcard certificate). The edge must preserve the request host and support WebSocket upgrades. Keep the portal hostname and application namespace aligned with the two Hub flags.

Example when the portal is published as veyport.example.com:443 and the Hub also has a private control address:

--application-domain apps.example.com
--application-portal-origin https://veyport.example.com
--application-control-deny-authorities veyport.internal.example:8443,veyport.example.com:443

--application-control-deny-authorities is a comma-separated list of additional host:port control-plane authorities that app backends must not target. Configure it for published or private Hub control mappings that differ from the Hub listener addresses or portal origin. For Docker, account for both sides of each published port mapping: for example, a host mapping 443:8081 is reached by clients as veyport.example.com:443, while veyport.internal.example:8443 is a separate private address if that is how operators reach the Hub. Supply the client-visible authority, not the container's internal port, and keep this flag in the service command alongside the two application flags. The application hostname namespace itself must route to the Hub HTTP listener; backend application ports should remain private to the Hub or selected node.

Each application is separately configured with a backend URL, an allowed destination CIDR set, and either a Hub route or an enrolled-node route. The Hub validates DNS results and pins an allowed destination address for direct routes; node routes also enforce destination policy at the node. Use a custom CA only when the backend uses a private certificate authority. Do not disable certificate verification. Restrict backend CIDRs to the intended application network and keep Hub control-plane addresses out of those ranges.

For this prototype, each Hub startup durably ends existing application sessions and clears unused launch tickets before application admission. Users must reopen their apps; the original completed-MFA maximum deadline remains in force, and portal/CLI session policy is unchanged. Failed cleanup writes keep application admission closed until a retry succeeds. Session-end audit records remain queued for idempotent retry after audit storage recovers. The in-memory pending-revocation queue is capped at 1024 IDs; overflow closes application admission and requires durable bulk cleanup before access resumes. Existing housekeeping retries at most 100 pending IDs per pass and stops at the first failed write.

The Hub bounds application transport: up to 128 active proxy connections globally, 32 node-routed connections per node, eight concurrent requests per application session, 64 KiB transport frames, and a 30-second socket write-stall deadline. Backend response headers are capped at 1 MiB. Request and response bodies stream through the proxy rather than being collected as whole-body buffers; a 64 MiB transfer is a test scenario, not a configured maximum body size. These are runtime guardrails, not a substitute for measuring memory and cleanup under the deployment's expected load.

Native applications should use the configured public application URL for links and callbacks. Veyport does not rewrite arbitrary HTML or JavaScript URLs and does not follow backend redirects by opening a new backend connection. An external identity provider or other cross-origin service must be independently reachable and trusted by the browser; verify the application's redirect/callback behavior before enabling access.

Before enabling the feature on an existing Hub, take and verify a consistent database backup, retain the current image or binary and exact startup configuration, and record the candidate artifact digest. Agree on an owner-approved rollout and rollback procedure. If rollback is needed, restore a mutually compatible software/configuration/database set from the recorded backup; do not assume that replacing only the binary reverses database changes. Disabling the application flags stops the feature from being configured at startup but does not restore records written after the backup. No staging or public deployment qualification is implied by this guide.

The feature does not require exposing backend application servers directly to users. Choose DNS visibility, firewall rules, and edge exposure according to the deployment's access policy; this guide does not imply that a public application namespace is provisioned. See Application Access for grants, session behavior, and API details.


Agent Deployment

Agents are lightweight Go binaries deployed on each managed server. They dial out to the Hub's gRPC port - no inbound firewall rules are needed on the agent host.

Initial enrollment (one command)

The Hub serves an install script and the agent binaries. On the managed server, run:

curl -sSL https://veyport.example.com/install.sh | sudo bash -s -- \
  --token <REGISTRATION_TOKEN> \
  --hub veyport.example.com:443

For a new node, the script will:

  1. Detect the OS and CPU architecture
  2. Auto-detect an existing installation - if veyport-agent is already installed and has a valid agent.conf, the script calls veyport-agent --self-unregister to remove the old server entry from the Hub before proceeding
  3. Download the correct agent binary from /install/{os}/{arch}
  4. Write the configuration to /etc/veyport/agent.conf
  5. Install and enable a systemd service for the agent
  6. Verify registration - after starting the agent service, the script checks that the agent successfully registered with the Hub before reporting success

This script is an enrollment or replacement workflow, not an upgrade command. On an existing installation it calls --self-unregister, which removes the current Hub server entry before registering again. That can change the node's identity and lose the continuity of its server history and assignments. Use the built-in update flow below to preserve an enrolled node's identity, local configuration, and credentials.

Piped vs. manual execution

Mode Behaviour
Piped from curl (non-interactive) Automatically replaces any existing installation without prompting; this unregisters and re-enrolls it
Run as a script manually (interactive terminal) Prompts [R]eplace / [K]eep; Replace unregisters and re-enrolls, while Keep exits with a non-zero status

Manual install

If you prefer not to pipe to bash:

# Download the binary (example: linux/amd64)
curl -Lo /usr/local/bin/veyport-agent \
  https://veyport.example.com/install/linux/amd64
chmod +x /usr/local/bin/veyport-agent

# Run directly
/usr/local/bin/veyport-agent \
  --hub veyport.example.com:443 \
  --token <REGISTRATION_TOKEN>

Agent configuration file

After first registration, the agent writes its assigned server_id and hub_url to:

/etc/veyport/agent.conf

Example contents (JSON):

{
  "server_id": "550e8400-e29b-41d4-a716-446655440000",
  "hub_url": "veyport.example.com:443"
}

On restart, the agent loads this file and reconnects without needing the registration token again.

Agent systemd service

The install script creates /etc/systemd/system/veyport-agent.service:

[Unit]
Description=Veyport Agent
After=network.target

[Service]
Type=simple
EnvironmentFile=/etc/veyport/agent.env
ExecStart=/bin/sh -eu -c 'if [ -f /etc/veyport/agent.conf ]; then exec /usr/local/bin/veyport-agent; else exec /usr/local/bin/veyport-agent --hub "$VEYPORT_HUB" --token "$VEYPORT_TOKEN" --ca-pin "$VEYPORT_HUB_CA_PIN"; fi'
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Agent flags

Flag Description
--hub <addr> Hub gRPC address (e.g. veyport.example.com:443 or 192.168.1.10:9090)
--token <token> One-time registration token obtained from the Hub when creating a server record
--insecure Disables TLS for development only, including self-unregistration. Never use on an untrusted network.
--self-unregister Calls DELETE /api/servers/{id}/self-unregister on the Hub to remove the current server entry, then exits. Used by the install script before re-installing to clean up the old registration. Requires a valid agent.conf with a known server_id.

Agent transport security

The agent uses TLS by default for both hostnames and IP addresses. Self-unregistration uses the same host over HTTPS on port 443 and does not follow redirects, so its unregister credential stays on the original request. An IP address must have a trusted TLS endpoint to support self-unregistration.

--insecure explicitly enables plaintext transport for development only. With --self-unregister --insecure, the REST request uses HTTP on port 8081. Production deployments should use a TLS proxy and omit this flag.

Reconnect behavior

The agent reconnects with exponential backoff - starting at 1 second and capping at 60 seconds. On each reconnect it re-sends a Register message so the Hub updates sysinfo and resets connection state.

Network requirements

Direction Protocol Port Notes
Agent - Hub gRPC (HTTP/2) 9090 (or 443 via Traefik) Must be reachable from agent host
Hub - Agent None - Agents always dial out; Hub never initiates

LDAP Login and Terminal Access

Veyport can authenticate users against LDAP and map LDAP groups to Veyport roles. Admins can configure LDAP from Settings > Directory in the web UI. The Hub stores the same values in _config, so operators can still inspect or recover the configuration directly from the database when needed.

The Directory tab supports:

  • Enabling or disabling LDAP login
  • LDAP URL, service bind DN, and write-only bind password
  • User and group search base DNs
  • Search filters and attribute mappings
  • StartTLS, TLS server name override, CA certificate PEM, and lab-only insecure transport
  • Configurable LDAP groups for admin, auditor, viewer, and terminal access
  • A service bind connection test

LDAP configuration keys

Key Purpose
ldap.enabled Enables LDAP login when set to true
ldap.url LDAP endpoint, such as ldaps://freeipa.example.com:636
ldap.bind_dn Optional service account DN used for searches
ldap.bind_password Service account password; encrypted enc: values are preferred
ldap.user_base_dn Base DN for user searches
ldap.group_base_dn Base DN for group searches
ldap.user_search_filter User search filter, default (uid={username})
ldap.group_search_filter Group search filter, default `(
ldap.username_attribute Username attribute, default uid
ldap.email_attribute Email attribute, default mail
ldap.external_id_attribute Stable external ID attribute, default entryUUID
ldap.group_name_attribute Group display/name attribute, default cn
ldap.start_tls Uses StartTLS with ldap:// when set to true
ldap.tls_server_name Optional TLS server name override
ldap.ca_cert_pem Optional PEM CA bundle for LDAP server verification
ldap.allow_insecure_transport Allows plaintext LDAP only for lab use
ldap.admin_groups JSON array or comma-separated LDAP groups mapped to the Admin role
ldap.auditor_groups JSON array or comma-separated LDAP groups mapped to the Auditor role
ldap.viewer_groups JSON array or comma-separated LDAP groups mapped to the Viewer role
ldap.terminal_groups JSON array or comma-separated LDAP groups eligible for terminal access

LDAP transport is fail-closed by default. Use ldaps:// or set ldap.start_tls=true; plaintext ldap:// is rejected unless ldap.allow_insecure_transport=true.

Group mapping

LDAP group Veyport effect
veyport-admins Admin role
veyport-auditors Auditor role
veyport-viewers Viewer role
veyport-terminal-users Marks the LDAP user as eligible for terminal access

These are defaults only. Change them in Settings > Directory to match your LDAP or FreeIPA group names.

Terminal access has an additional server-level gate. LDAP terminal users must also have a root (/) path assignment on the specific server. Local non-admin users cannot open terminal sessions.


mTLS Configuration (v1.1)

As of v1.1, the Hub supports mutual TLS (mTLS) for gRPC connections from agents. This provides cryptographic identity verification beyond the initial registration token.

How it works

  1. Automatic CA generation -- On first boot, the Hub generates an ECDSA P-256 Certificate Authority. No manual certificate management is required.
  2. Agent certificates issued during registration -- When an agent registers, the Hub signs a client certificate (12-hour validity) and sends it to the agent over the gRPC stream.
  3. In-stream renewal -- Agents automatically renew their certificates at the 6-hour mark (50% lifetime), with no downtime or reconnection.
  4. Agent cert storage -- Client certificates and keys are stored at /etc/veyport/tls/ on each agent host.

Enabling mandatory mTLS

By default, mTLS is optional for backward compatibility. Agents that support mTLS will use it; older agents continue to work without it.

To enforce mTLS for all agent connections, add the --require-mtls flag:

# Docker: add to the command in docker-compose.yml
command: ["--require-mtls"]

# Bare-metal: add to the ExecStart line in the systemd unit
ExecStart=/opt/veyport/bin/veyport \
  --addr 127.0.0.1:8081 \
  --grpc-addr 0.0.0.0:9090 \
  --db /var/lib/veyport/veyport.db \
  --agent-bin-dir /opt/veyport/agent-bins \
  --require-mtls

Migrating pre-v1.1 agents without mTLS

This is a one-time transport migration for agents deployed before v1.1 that do not have mTLS certificates. It is separate from normal agent updates, which use vey servers update <node> and preserve the enrolled identity. To migrate an agent that cannot establish mTLS:

  1. Re-register the agent (unregister from the Hub UI, then re-run the install script with a new token)
  2. The new agent will automatically receive an mTLS certificate during registration
  3. Once all agents are re-registered, enable --require-mtls on the Hub

Certificate locations (agent)

File Path Description
Client cert /etc/veyport/tls/client.crt Agent's certificate signed by Hub CA
Client key /etc/veyport/tls/client.key Agent's private key (never leaves the machine)
CA cert /etc/veyport/tls/ca.crt Hub CA certificate for verifying the Hub

Cookie-Based Authentication (v1.1)

As of v1.1, the web UI uses httpOnly cookies for JWT token storage instead of localStorage. This change is fully automatic -- no configuration is needed.

  • Tokens are set as HttpOnly, Secure, SameSite=Strict cookies
  • CSRF protection uses a double-submit cookie pattern (handled by the frontend automatically)
  • Non-browser clients (scripts, CLI tools) can still use Authorization: Bearer <token> headers
  • The refresh token cookie is scoped to /api/auth/refresh to limit exposure

No action is required from operators. The cookie auth is active by default when upgrading to v1.1.


Machine Auth for Scripts

If you are automating Veyport from cron, CI, or a scanner, do not script a human username/password/TOTP flow. Use a dedicated service account plus a CLI-created API token instead.

Create a dedicated token

# Docker deployment
docker exec veyport /app/veyport admin create-api-token \
  --username scanner \
  --name nightly-scan \
  --expires-in 720h \
  --db /data/veyport.db

# Bare-metal deployment
./bin/veyport admin create-api-token \
  --username scanner \
  --name nightly-scan \
  --expires-in 720h \
  --db /var/lib/veyport/veyport.db

Store the printed token in a root-only env file on the machine that runs the automation:

sudo install -m 600 /dev/null /etc/veyport/automation.env
sudo editor /etc/veyport/automation.env

Example contents:

VEYPORT_SCAN_BASE_URL=https://veyport.example.com
VEYPORT_API_TOKEN=adt_replace_me

Reference the token from your scripts as an Authorization: Bearer $VEYPORT_API_TOKEN header, and rotate it before the --expires-in window elapses by issuing a new token and updating the env file.


LXC Container Deployment

Veyport can be deployed in a Proxmox LXC container as a lightweight alternative to a full VM. This is useful for home lab and small-team deployments.

Setup steps

  1. Create an unprivileged LXC container (Debian/Ubuntu) with at least 512 MB RAM and 4 GB disk.
  2. Install Docker inside the container (requires the keyctl and nesting features enabled on the LXC).
  3. Deploy with Docker Compose using the same docker-compose.yml as any other deployment.
  4. Configure Traefik on the host or a separate LXC to reverse-proxy HTTPS traffic to the container's IP.

Example: Proxmox LXC with Traefik

If Traefik runs on a separate LXC and Veyport runs in another container at 192.0.2.10:

# /etc/traefik/dynamic/veyport.yml on the Traefik host
http:
  routers:
    veyport:
      rule: "Host(`veyport.example.com`)"
      entryPoints: [websecure]
      tls:
        certResolver: letsencrypt
      service: veyport
    veyport-grpc:
      rule: "Host(`veyport.example.com`) && PathPrefix(`/veyport.v1.`)"
      entryPoints: [websecure]
      tls:
        certResolver: letsencrypt
      service: veyport-grpc
  services:
    veyport:
      loadBalancer:
        servers:
          - url: "http://192.0.2.10:8081"
    veyport-grpc:
      loadBalancer:
        servers:
          - url: "h2c://192.0.2.10:9090"

In the Veyport container, bind the HTTP and gRPC ports to 0.0.0.0 so Traefik can reach them across the LXC network.


DNS Setup

Point your domain's A record (and AAAA for IPv6) to the public IP of the server running Traefik.

veyport.example.com.  IN  A  203.0.113.10

Traefik will automatically obtain a Let's Encrypt certificate on first request once the DNS record propagates.


Database Management

Veyport uses SQLite with WAL mode. The database is a single file.

  • Docker: The database lives at /data/veyport.db inside the container, persisted via the veyport-data named volume.
  • Bare-metal: The database path is set via the --db flag (default: veyport.db).

Auto-migrations: On every startup, the Hub checks for and applies any unapplied migration files. No manual schema management is needed after upgrades.

Migration 031 adds the per-account SSH credential generation used to invalidate existing SSH certificates after password recovery. Existing accounts start at generation zero, so certificates issued before this migration remain compatible until a password recovery advances the generation. No manual database migration step is required.

Backups: SQLite's WAL mode makes it safe to copy the database file while the Hub is running. To take a consistent snapshot:

# Docker - use docker exec to run sqlite3 inside the container
docker exec veyport sqlite3 /data/veyport.db ".backup /data/backup-$(date +%Y%m%d).db"

# Bare-metal
sqlite3 /var/lib/veyport/veyport.db ".backup /var/lib/veyport/backup-$(date +%Y%m%d).db"

Store backups off-host. The database contains all user accounts, server registrations, and audit logs.


CLI Break-Glass: Emergency TOTP Reset

If an admin is locked out (lost their authenticator app), use the admin reset-totp command directly on the server hosting the Hub. This requires shell access to the machine - it cannot be triggered via the web UI.

# If running via Docker
docker exec veyport /app/veyport admin reset-totp --username <username> --db /data/veyport.db

# If running as a bare-metal binary
./bin/veyport admin reset-totp --username <username> --db /var/lib/veyport/veyport.db

For local accounts, including protected security principals, this atomically clears TOTP, installs an expiring temporary password, preserves completed initial setup, and records a system audit event. It retains the outgoing permanent password using the configured password history count; repeated resets replace the temporary credential without consuming history. The temporary password uses the configured TTL and is printed only after the transaction commits.

The reset increments token and SSH credential generations and revokes sessions, API tokens, application login authority, application sessions, and launch tickets. Any storage failure rolls back the entire reset. LDAP accounts are refused; directory passwords must be recovered in the directory.

The user must enroll TOTP and choose a fresh permanent password on their next login, before the temporary password expires. Protected local accounts receive authority limited to completing this recovery; ordinary protected-account changes still require governance approval.

If the database is locked (bare-metal only):

systemctl stop veyport
./bin/veyport admin reset-totp --username admin --db /var/lib/veyport/veyport.db
systemctl start veyport

In practice the Hub does not hold exclusive locks on the database (WAL mode), so running the command while the service is live is usually safe.


CLI Break-Glass: Rotating the JWT Signing Secret

Use admin rotate-jwt-secret when you need to invalidate all existing sessions and API tokens — either as part of a routine security cadence or in response to a suspected credential compromise.

When to rotate:

  • Routine: Annually, or per your organisation's key-rotation policy.
  • Compromise response: Immediately if the database file or its signing secret may have been exposed.
# If running via Docker
docker exec veyport /app/veyport admin rotate-jwt-secret --db /data/veyport.db

# If running as a bare-metal binary
./bin/veyport admin rotate-jwt-secret --db /var/lib/veyport/veyport.db

Without --yes the command prints an impact summary and asks for confirmation. Pass --yes for scripted use.

What this does:

  1. Generates a new JWT signing secret and persists it to the database.
  2. Revokes all API tokens immediately (count reported on stdout).
  3. Invalidates all active user sessions — every user must sign in again.
  4. Encrypted settings (LDAP bind password, SMTP password, CA key) are unaffected; the storage key is separate from the signing secret.

After rotating, restart the Hub to apply the new secret:

# Docker
docker compose -f /opt/veyport/docker-compose.yml restart

# Bare-metal
systemctl restart veyport

Verification checklist:

  1. An access token issued before rotation is rejected (HTTP 401) on any API call.
  2. Fresh sign-in (password + TOTP) succeeds.
  3. Settings → Directory "Test Connection" passes.
  4. Test notification email sends successfully.
  5. Agents reconnect or remain connected.
  6. GET /api/settings/hub shows the updated jwt_secret_rotated_at timestamp.
  7. Audit log contains an auth.jwt_secret_rotated event with the revoked-token count.

Backup/restore note: A database backup is self-consistent — it pairs the secret with its ciphertexts. Restoring a pre-rotation backup undoes the rotation. If the rotation was a compromise response, re-rotate immediately after restoring.


Updating / Redeploying

Update the CLI and enrolled agents

Update the operator's CLI from its configured Hub with vey update. Use vey update --check first to inspect the Hub version and verified artifact metadata without downloading or changing files. The local updater supports Linux and macOS amd64/arm64 and installs only to /usr/local/bin/vey. If it reports a permission problem, rerun the exact command printed by the error with explicit sudo; the command does not elevate itself. See CLI#update-the-cli for version checks and recovery details. A CLI build that predates the built-in updater needs one final install from the configured Hub's Install CLI command before it can use vey update.

For a node update, the node must already be enrolled with a valid mTLS identity, and its Veyport installation must never have enrolled Enhanced or other governance controls. Updates are available only for Standard installations. They require a live interactive administrator session; API tokens are refused. The command uses the effective Hub selected by --hub, VEYPORT_HUB, or the configured default. That Hub selects the exact artifact for the enrolled node's Linux amd64/arm64 platform. No arbitrary URL or version can be supplied, and the command will not downgrade an agent. Linux systemd nodes using the root-owned /usr/local/bin/veyport-agent service are supported.

vey servers update web01 --check
vey servers update web01

--check reports the current agent capability, Hub artifact, version, checksum, and whether a one-time bootstrap is required. Current agents report file-list, file-read, file-delete, upload, and log-tail activity to the Hub. These records stay active until the agent reports actual completion, even if the HTTP caller cancels or times out. Maintenance is refused while any known or unconfirmed activity, terminal, command, or other agent work remains; once reserved, new work is blocked. A client timeout, cancellation, disconnect, or stop request is not proof that remote work finished. Let the work complete and verify the node is idle before retrying. The command waits up to 150 seconds for the agent to reconnect, report the exact target version, revision, and checksum, and durably confirm the update. If it cannot confirm that evidence, inspect the durable operation before retrying:

vey servers update web01 --status
vey servers update web01 --status --operation <operation-id>

For an older enrolled agent, or an agent that cannot report complete file/log quiescence (including prototype.4 agents), the CLI's check response includes bootstrap_required, bootstrap_idle_confirmation_required, and the current connection_id. Close file and log clients, agent sessions, and other work; allow it to finish and verify the node is idle over direct SSH. The ordinary preserving bootstrap uses only the explicit idle assertion:

vey servers update web01 --bootstrap-idle-confirmed

If the original binary is unstamped (for example, the legacy 0.1.0 agent from the prototype.5 staging incident), also establish its version from trustworthy deployment records and measure the installed bytes over direct SSH. Do not infer a version from an arbitrary unstamped executable. For that one-time path, pass both assertions:

vey servers update web01 --bootstrap-idle-confirmed \
  --bootstrap-original-version 0.1.0 \
  --bootstrap-original-sha256 <sha256-from-direct-ssh>

Obtain the digest on the target with sudo sha256sum /usr/local/bin/veyport-agent. The version and hash flags are mandatory as a pair. They bind the operator's assertion to the exact bytes being replaced. The helper checks the local package/platform, confirms that both linker-stamped identity fields are absent, and verifies the measured SHA-256. A stamped binary cannot use this original-identity path. The target must be newer than the asserted original version. The Hub binds the original-identity assertion to the authenticated actor, session, node, fresh connection, and one-use ticket. The idle flag remains an operator assertion, not an automatic activity detector; the Hub still rejects known active work and a changed connection. If you cannot confirm idle, leave the node untouched and check again later.

The CLI prints a one-use direct-SSH bootstrap command tied to the node, Hub artifact, operation, and (for an unstamped binary) the same version and SHA-256 assertions. Run that exact printed command on the node within 120 seconds. It verifies the Hub artifact and original bytes, checks the service is idle, then updates the existing installation while preserving its mTLS identity, agent.conf, service configuration, and node ID. It does not re-enroll or use a registration token. The HTTPS Hub hostname in the printed command must match the hostname configured for the agent's mTLS connection; HTTPS and gRPC ports may differ. Do not substitute or reconstruct the command by hand.

For the unstamped-original case, the printed command has this form and includes the same assertions supplied to vey servers update:

curl -fsS --proto '=https' 'https://<hub>/install/agent-update.sh' | \
  sudo bash -s -- --hub 'https://<hub>' --ticket '<64-char-ticket>' \
    --original-version '0.1.0' --original-sha256 '<sha256>'

Creating the operation and printing a command does not mean installation succeeded. After running the command, inspect the durable operation and wait for succeeded:

vey servers update web01 --status
vey servers update web01 --status --operation <operation-id>
vey ssh web01 -- 'hostname; id; uname -s'

The Hub records success only after the agent reconnects and durably confirms the exact target artifact. Verify the node's identity and OS over vey ssh as a separate operational check.

An update that fails can be failed, rolled_back, or unknown. A legacy agent's rollback may remain unknown because the old protocol cannot report an exact binary hash. In that case, inspect the operation and use direct SSH to verify the original binary checksum and service before acknowledging recovery. The CLI prints the expected original SHA-256 and earliest recovery time. Only after verification, run:

vey servers update web01 --acknowledge-recovery <operation-id> \
  --verified-sha256 <verified-original-sha256>

This is an administrator attestation that direct SSH checks passed; it is not remote confirmation of the binary. Do not acknowledge recovery if the checksum or service state is uncertain. A recovery acknowledgement releases the update maintenance reservation and is audited.

Docker (recommended)

docker compose pull
docker compose up -d

New migrations (if any) run automatically on startup.

Do not use this generic latest/tag upgrade procedure for an unreleased feature branch. For the command-passthrough prototype, preserve a consistent SQLite and configuration backup, retain the current image for rollback, verify the exact prototype image digest/revision, and confirm that every target agent supports the new execution protocol before testing command mode. Legacy agents must retain interactive compatibility; do not replace unrelated production agents solely to exercise the prototype. The project owner controls staging deployment and release.

Enhanced audit is optional. Standard Hub operation requires no receipt, archive, key service, or ELK service. Enabling Enhanced requires the separately qualified receipt/archive services and fixed trust configuration; do not point a candidate destination at an arbitrary user-supplied endpoint. The repository's isolated lab runbooks under infra/audit-lab/ describe its private test topology and are not general production deployment instructions.

Bare-metal

  1. Build the new binary from source (make build)
  2. Copy the binary to the server:
    scp bin/veyport user@yourserver:/opt/veyport/bin/veyport.new
  3. Swap the binary and restart:
    mv /opt/veyport/bin/veyport.new /opt/veyport/bin/veyport
    systemctl restart veyport
  4. New migrations (if any) run automatically on startup.

Building from Source

For contributors or those who prefer running a bare-metal binary.

Prerequisites

  • Go 1.26+
  • Node.js 25+
  • Make

Build

git clone https://github.com/lorconksu/veyport.git
cd veyport
make build

The make build target runs three steps in sequence:

  1. build-web - Runs npm run build inside web/, producing web/dist/
  2. embed-web - Copies web/dist/ to hub/web/dist/ so it's picked up by go:embed
  3. build-hub - Compiles hub/cmd/veyport/ into bin/veyport

The output is a single self-contained binary at bin/veyport. No Node.js, no separate web server, no external dependencies at runtime.

Running the binary

./bin/veyport [flags]
Flag Default Description
--addr :8080 HTTP listen address and port
--grpc-addr :9090 gRPC listen address for agent connections
--db veyport.db Path to the SQLite database file
--agent-bin-dir `` Directory containing agent binaries served via /install/{os}/{arch}
--require-mtls false Require agents to present valid mTLS certificates (v1.1+)
--grpc-external-addr `` External gRPC address shown in install commands (e.g. veyport.example.com:9443). Overridable from the admin UI.
--dev false Enable development mode (permissive CORS, no embedded frontend served)

Example:

./bin/veyport \
  --addr 127.0.0.1:8081 \
  --grpc-addr 0.0.0.0:9090 \
  --db /var/lib/veyport/veyport.db \
  --agent-bin-dir /opt/veyport/agent-bins

Bind HTTP to 127.0.0.1 (loopback only) when running behind a reverse proxy. The gRPC port (9090) must be reachable by agents - bind to 0.0.0.0 or the server's public interface.

On first run, migrations execute automatically and the database file is created. Navigate to your domain in a browser to complete initial setup.

Systemd service

Create /etc/systemd/system/veyport.service:

[Unit]
Description=Veyport Hub
After=network.target

[Service]
Type=simple
User=veyport
Group=veyport
WorkingDirectory=/opt/veyport
ExecStart=/opt/veyport/bin/veyport \
  --addr 127.0.0.1:8081 \
  --grpc-addr 0.0.0.0:9090 \
  --db /var/lib/veyport/veyport.db \
  --agent-bin-dir /opt/veyport/agent-bins
Restart=on-failure
RestartSec=5s

# Harden the service
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/lib/veyport /opt/veyport/agent-bins
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Enable and start:

# Create the user and directories
useradd --system --no-create-home --shell /usr/sbin/nologin veyport
mkdir -p /opt/veyport/bin /var/lib/veyport
cp bin/veyport /opt/veyport/bin/veyport
chown -R veyport:veyport /opt/veyport /var/lib/veyport

# Enable and start
systemctl daemon-reload
systemctl enable veyport
systemctl start veyport
systemctl status veyport

Established SSH gateway and browser terminal shells revalidate their captured credential generation and current terminal authorization from SQLite every second, including when the shell is silent or only receiving agent output. Each check requires an eligible account, the current administrator role or LDAP terminal grant and exact root (/) assignment for that server, an existing server, and the execution username captured at shell creation. Browser shells also require that their captured owning session still exists, belongs to that account, has well-formed timestamps, and remains within its absolute and idle limits. Both transports recheck current policy coverage and local policy continuity; recovery state, covered Enhanced execution, and unavailable decisions close the shell. This detects role downgrades, permission removal, directory authorization changes, and veyport admin reset-totp changes made by a separate process. A failed read closes the shell locally; each check has a 250 ms budget, including waiting for a query slot or connection. Credential reads use a separate pool capped at eight connections, with SQLite busy waits disabled during connection initialization and reads. WAL or rollback lock contention therefore fails closed immediately instead of inheriting the writer pool's five-second busy wait. Revocation is detected by the next polling check: one interval plus the read budget is 1.25 seconds, subject to process scheduling. The output gate is revoked before cancelling an in-flight write; completing transport cancellation can add latency. The Hub prioritizes an exit while the client transport remains writable, otherwise closes it, and sends TerminalClose to the captured agent connection (up to a five-second send deadline). Output may continue until the next revalidation; this is not a synchronous cross-process notification. Forced closure discards queued data and gates already-dequeued data against the exact registration. It aborts an in-flight transport write before closure returns; bytes already accepted by a network transport cannot be recalled. Browser data writes have a one-second deadline. An SSH data write in progress may require closing its whole retained SSH connection, including other channels. Further agent output and client input are refused. Account disable, MFA removal, and password reset atomically advance both browser and SSH credential generations. Re-enabling an account never restores its pre-disable SSH certificates. SSH admission and revalidation also require enrolled MFA. Governed password-reset and mfa-reset apply/reconcile immediately close registrations with revoked credential generations. Replaying an already applied recovery preserves shells authenticated after recovery completed; durable generation checks still clean up stale registrations after a crash.

The terminal registry permits at most 256 interactive shells across the Hub, with the existing eight-per-node limit. At most 256 credential watchers may be active, including watchers finishing agent cleanup, with at most eight concurrent revalidation queries. Waiting for a query slot shares the 250 ms deadline; each watcher stops its ticker and releases its slot on registration removal/transport teardown. An agent-reported exit retains revalidation until buffered output is drained and the transport removes the registration. Agent output creates no extra database queries. These limits and the revalidation interval are fixed, with no new configuration keys. Capacity refusals use the existing terminal registration conflict response.

Temporary-password rotation and TOTP enrollment now commit together with recovery-grant consumption, credential revocation, password history, setup state and success audits. A storage failure preserves the temporary password and recovery grant; retry with a fresh authenticator code before recovery expires. A governance change that revokes an MFA recovery grant requires a fresh approved MFA reset; a subsequent password reset cannot restore the revoked grant.

Password changes capture the signed token generation, expiry, owning session, and account/MFA snapshot before reading the request body. The immediate write transaction rechecks that authority, session ownership and ended/absolute/idle state, and account eligibility before changing the password. Administrator reset and reactivation, password recovery, and combined password/TOTP enrollment also evaluate expiry after acquiring SQLite's write lock. Their injected clock advances by monotonic elapsed waiting time. Authority revoked or expired during body reads, password hashing, history checks, or writer contention causes refusal and leaves credentials, history, recovery grants, and success audits unchanged.

Administrator TOTP disable commits factor removal, both credential generations, browser/API/application revocation and its success audit in one immediate transaction. Persistence failures return an opaque server error and roll back the entire operation, including the audit. Setup persistence failures likewise return HTTP 500; retry enrollment with a fresh TOTP code. The removal transaction revalidates the authenticated actor's enabled account, current administrator role, MFA revision, credential generation and live session against the snapshot used to verify the administrator's TOTP. Revocation while the write waits for SQLite causes refusal without changing the target or audit.

Shell recovery grants reference the exact successful system user.totp_reset audit. Retention compares UTC instants and deletes only records strictly older than its cutoff. An audit linked to an unexpired shell recovery grant is retained even if audit retention is shorter than the temporary-password lifetime. Once the grant expires or is consumed, normal retention applies. Direct deletion of the audit still deletes its grant, and invalid provenance blocks protected-account MFA/password completion. Migration 033 also binds databases that ran the early migration 032, discarding orphaned or invalid grants.

SSH certificates issued by this version include the immutable account ID and credential generation as signed extensions. Migration 035 reserves existing and future account IDs permanently, including after deletion. Recreating a username creates a different identity. Certificates lacking the identity extension are refused, including on retained connections and exec channels; after upgrading, users must sign in with MFA and run vey ssh-cert again. The CA and gateway host keys remain stable. Migration 036 also invalidates outstanding step-up proofs that predate credential-generation binding; obtain a fresh action-bound proof.

Clone this wiki locally