Skip to content

WIP: add host SSH setup for Docker-managed host nginx#1686

Draft
Hintay wants to merge 42 commits into
devfrom
feat/host-via-ssh-setup
Draft

WIP: add host SSH setup for Docker-managed host nginx#1686
Hintay wants to merge 42 commits into
devfrom
feat/host-via-ssh-setup

Conversation

@Hintay
Copy link
Copy Markdown
Collaborator

@Hintay Hintay commented May 21, 2026

Summary

This PR adds a host SSH control mode for deployments where Nginx UI runs in Docker but needs to manage an nginx instance installed directly on the same host.

Changes

Host SSH control mode

  • Add host_via_ssh support for controlling host nginx from the Nginx UI container.
  • Introduce SSH client, known_hosts handling, host key trust flow, and sudo command execution helpers.
  • Add host-aware nginx runner support for reload, restart, config testing, status checks, and path resolution.
  • Keep existing local and external-container control modes through a shared runner interface.

Host setup workflow

  • Add host setup API endpoints for:
    • previewing setup snippets
    • generating ed25519 keypairs
    • reading/deleting generated public keys
    • trusting verified host keys
    • running setup verification
  • Add nginx-ui host-setup CLI commands for generating snippets, keypairs, and verification checks.
  • Add templates for sudoers, authorized_keys, ACL commands, compose overrides, compose snippets, and docker run examples.
  • Add verification checks for SSH connectivity, same-host validation, sudo availability, sudoers coverage, systemd state, nginx config test, config directory access, log access, and PID file presence.

Frontend setup wizard

  • Add a Host via SSH setup wizard under Nginx preferences.
  • Add steps for:
    • selecting auth method and generating keys
    • applying container-side snippets
    • applying host-side snippets
    • entering connection settings
    • running verification and reviewing remediation hints
  • Add a reusable CodeBlock component with copy support for setup snippets.
  • Extend settings API/types for host SSH configuration fields.

Docker support

  • Add NGINX_UI_DISABLE_BUNDLED_NGINX support so Docker deployments can disable the bundled nginx service when controlling host nginx.

Documentation

  • Add deployment guides for:
    • managing host nginx from Docker
    • managing multi-host nginx with Cluster nodes
  • Add the deployment guide section.

Hintay added 30 commits May 21, 2026 07:25
Introduces Host* fields on settings.Nginx and a ControlMode() helper
that resolves the active nginx control channel with priority
host_via_ssh > external_container > local. RunningInAnotherContainer()
is refactored to delegate to ControlMode() so SSH mode is correctly
excluded from the docker-exec path.

Refs: docs/superpowers/specs/2026-05-21-docker-host-nginx-management-design.md §5
…ment

Replaces string literals in nginx_test.go with the exported constants
defined in nginx.go so that renaming a constant breaks the test.
Removes the internal docs/superpowers/ reference from the source
comment.
Adds internal/nginx/runner.go defining a Runner interface that
abstracts command execution and path-existence checks across the
three control modes. localRunner mirrors the current behavior.
Placeholders for newSSHRunner/newDockerRunner keep the package
compiling until follow-up tasks replace them with real impls.
Wires the Runner interface to the existing docker.Exec / docker.StatPath
helpers for external_container mode. The SSH runner placeholder remains
until Task 5.
Introduces internal/host/ssh package with cosy error codes 510001-510009
and a KnownHosts type that wraps a known_hosts file with thread-safe
Trust + IsTrusted operations. Used by subsequent Client and verify tasks.
Adds a long-lived SSH client with keepalive + lazy reconnect,
command-building with sudo whitelisting and shell escaping, plus
the sshRunner wiring into the Runner interface.
- connect() now holds the mutex across dial, preventing concurrent
  callers from overwriting c.conn and leaking SSH connections.
- buildCommand tokenizes and shell-quotes SudoPrefix so a misconfigured
  setting cannot inject arbitrary commands.
- Adds a ResetSSHClient() hook for settings-change invalidation.
- Logs a clear warning when password auth is configured (currently
  unsupported pending crypto package refactor).
execCommand no longer branches on RunningInAnotherContainer directly.
The Runner abstraction picks the right backend (local/docker/ssh) based
on settings.NginxSettings.ControlMode().
GetPIDPath probing and IsRunning() now branch on ControlMode():
- local: unchanged (os.Stat + gopsutil.PidExists)
- external_container: docker.StatPath as before
- host_via_ssh: systemctl is-active over SSH with PID-file fallback

PID files are visible via the bind-mounted /var/run, so local probing
still works in SSH mode; the systemctl path is preferred for authority.
Adds compose / override / docker run / authorized_keys / sudoers /
acl_commands templates. They share the SetupParams input model and
are rendered by snippets.go in the next task.
Embeds the six templates via embed.FS; RenderAll returns a single
struct convenient for both REST JSON and CLI --json output. Golden
file tests lock down byte-for-byte output so CLI and Web UI render
identical content.
Writes a 0600 OpenSSH-format private key and returns the matching
public key in authorized_keys single-line form. Overwrites any
existing key file at the target path (matches Web UI 'regenerate' UX).
Steps 0-9 with structured StepOutcome including remediation hints.
Linux-only via build tag; non-Linux returns a stub that flags the
platform mismatch.
Implements 'nginx-ui host-setup print/keygen/test'. The 'test' action
body is intentionally a stub until Task 14 extracts the shared
live-client builder helper used by both this CLI and the REST verify
handler.
Adds /api/host/setup/{preview,keypair,publickey,verify,known-host} and
wires the CLI 'host-setup test' subcommand through the shared
setup.NewClientFromSettings + setup.Verify helpers introduced in this
commit. Mounted under the authenticated router group.
- Adds ErrPublicKeyParse (510010) so TrustHostKey returns a semantically
  correct error instead of reusing ErrHostKeyMismatch's expected/got template.
- The TrustHostKey HTTP handler now recomputes the SHA256 fingerprint of
  the submitted public key and rejects requests where the client-confirmed
  fingerprint does not match. Closes a security gap where a tampered
  request body could install an unverified key in known_hosts.
When set to 'true', the s6-overlay nginx service blocks on sleep
infinity instead of starting nginx, and the init-config script
skips the /etc/nginx seed copy. Required by host_via_ssh mode so
the container's internal nginx doesn't conflict with the host one.
…rning

Renders the address/user/unit form and surfaces a non-blocking warning
when the host address is outside the same-host whitelist.
Adds a Wizard.vue that orchestrates the four steps and a control-mode
segmented control on NginxSettings.vue that exposes the wizard entry.
…settings, default strict host key

Three critical gaps surfaced by the final review:

1. nginx Reload()/restart() in SSH mode now call 'sudo systemctl
   reload|restart <unit>' instead of 'nginx -s reload', which would
   send a SIGHUP to a PID that does not exist in the container's PID
   namespace.

2. The wizard's connection form now persists all 10 Host* fields to
   the settings store, so the existing Save button in Preference.vue
   actually saves them. The TypeScript NginxSettings interface gains
   the matching fields.

3. HostStrictHostKey now defaults to true unless explicitly disabled
   via NGINX_UI_NGINX_HOST_STRICT_HOST_KEY=false, closing a TOFU
   security gap on first connection.

4. ResetSSHClient() is now called after SaveSettings so the cached
   SSH client picks up new credentials without a process restart.
Comment thread internal/host/ssh/client.go Fixed
@Hintay Hintay requested a review from Copilot May 22, 2026 19:21
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants