Use this runbook to administer the StratoFusion Ubuntu VM without adding a
public desktop or management port. The supported operator paths are:
VS Code Remote SSH for files, Git, terminals, PNPM, Docker Compose, logs,
and container shells.
Tailscale for the private management network.
Cockpit for an optional browser system console, proxied only inside the
tailnet.
The application stack remains owned by deploy/docker-compose.prod.yml and
deploy/deploy.sh. Tailscale and Cockpit are host packages; they are not
application containers and are not routed through public Caddy.
Do not enable Tailscale Funnel. Funnel is an internet-publishing feature;
Tailscale Serve is the private tailnet feature used here.
Prerequisites
If only GitHub Actions holds the production SSH credential, start with
CI admin bootstrap to inspect the VM, install
Tailscale, and register a separate workstation public key without exporting
the CI private key. Tailnet enrollment remains an interactive operator step.
Confirm the target is the intended Ubuntu 24.04 VM by hostname and repository
path, not by an IP address alone.
Confirm key-only SSH works before changing packages or sockets.
Confirm PasswordAuthentication no, PermitRootLogin no, and active UFW.
Confirm the existing application containers are healthy.
Keep a provider-console session available as the lockout escape path.
Install Tailscale on the Windows operator workstation and sign in to the
intended tailnet. Use the current official Windows instructions.
The host helper refuses installation if the expected OS, SSH hardening, or UFW
state is absent.
VS Code Remote SSH
Install Microsoft's Remote - SSH and Container Tools extensions in the
Windows VS Code client. Keep the private key in the Windows user SSH directory;
do not copy it into the repository.
Example %USERPROFILE%\.ssh\config entry:
Host stratofusion-vm
HostName <public-hostname-or-tailnet-name>
User deploy
IdentityFile ~/.ssh/<private-key-file>
IdentitiesOnly yes
Then:
Run Remote-SSH: Connect to Host... and choose stratofusion-vm.
Open /opt/stratofusion.
Use Terminal: New Terminal for VM commands.
Use Container Tools or the terminal for container status, logs, and shells.
Use the VS Code Ports view for temporary loopback forwarding. Never set
forwarded ports to public visibility.
Safe operational examples from the remote terminal:
The VM checkout is a deployment checkout. Routine development belongs in the
normal Git branch and CI workflow; editing production files in place creates
drift and the deploy workflow can overwrite it with an exact commit reset.
Optional Database Administration Tools
pgAdmin and RedisInsight are optional containers in the Compose management
profile. They are disabled by default in production, bind only to VM loopback,
and are not routed by Caddy. Before enabling the profile, replace
PGADMIN_DEFAULT_PASSWORD and REDISINSIGHT_ENCRYPTION_KEY in the protected
VM .env with independent random values stored in the password manager. Keep
the RedisInsight encryption key stable while reusing its persistent volume.
The local-VM example enables the profile and configures dashboard destinations
for an SSH tunnel. The normal deployment then starts the tools with the rest of
the selected profile. To start only these services during an approved setup:
VS Code Remote SSH users may instead forward remote ports 5050 and 5540
from the Ports view and keep their visibility private. With either tunnel
active, use the Infrastructure dashboard actions or open:
http://127.0.0.1:5050 for pgAdmin;
http://127.0.0.1:5540 for RedisInsight.
Log in to pgAdmin with PGADMIN_DEFAULT_EMAIL and the stored password, then
register PostgreSQL host postgres, port 5432, maintenance database
stratofusion-prod, and the credentials from the protected VM .env.
RedisInsight is preconfigured with Compose host redis on port 6379; it does
not expose Redis itself to the host.
Do not add either tool to Caddy, public DNS, UFW, OAuth allowlists, or a public
Tailscale Funnel. A separate private *.ts.net HTTPS endpoint may be used only
after updating the tailnet grants and setting the matching server-only
management URL in .env.
Install Host Packages
From a verified deploy SSH session:
cd /opt/stratofusion/deploy
sudo bash ./vm-admin-access.sh status
sudo bash ./vm-admin-access.sh install
The install action:
adds the official Tailscale stable repository for Ubuntu 24.04;
installs and starts tailscaled without joining a tailnet;
installs Cockpit from Ubuntu Noble backports;
binds cockpit.socket to 127.0.0.1:9090 before enabling it;
adds no UFW rule and changes no Caddy or Compose configuration.
Run the interactive login from the verified public SSH session:
sudo tailscale up
Open the one-time authorization URL yourself and approve the expected device.
Do not paste the URL into tickets or logs. Do not pass an auth key, enable
Tailscale SSH, advertise routes, configure an exit node, or enable Funnel.
On Windows, verify the VM appears from the intended tailnet account, then test
ordinary OpenSSH over its MagicDNS name. Existing SSH keys remain the SSH
authentication mechanism; Tailscale only provides the private network path.
Least-Privilege Tailnet Policy
Use Tailscale Grants and allow only the operator group to reach the VM tag on
OpenSSH and the private Cockpit port. Merge a rule like this into the existing
policy; do not replace unrelated policy sections:
Assign the tag through the Tailscale admin console and use its policy validator
before saving. Tailscale policies are additive: remove broad allow-all rules if
they would make this narrow grant ineffective. See the current
Grants syntax.
Create The Cockpit Operator
Cockpit authenticates Linux users. Keep the deploy account passwordless and
create a separate human account interactively:
sudo adduser vm-operator
sudo usermod -aG sudo vm-operator
id vm-operator
Requirements:
choose the password interactively and store it in the password manager;
do not add vm-operator to the docker group;
do not add a NOPASSWD sudo rule;
verify id vm-operator shows sudo but not docker;
leave root login disallowed and SSH password authentication disabled.
The operator can use password-protected sudo docker compose ... from the
Cockpit terminal when elevated access is actually needed.
Configure Private Cockpit HTTPS
After Tailscale reports the node as connected:
cd /opt/stratofusion/deploy
sudo bash ./vm-admin-access.sh configure-cockpit
sudo tailscale serve status
configure-cockpit derives the current node's MagicDNS name without printing
it, configures Cockpit's exact HTTPS/WebSocket origin on tailnet port 8443,
sets LoginTo=false, and keeps the backend loopback-only.
Review tailscale serve status before proceeding. If any Serve route already
exists, stop and reconcile it rather than overwriting it. For a node with no
existing Serve configuration, start the private proxy interactively:
Tailscale may require a one-time tailnet HTTPS approval. Complete that approval
yourself. The command's reported *.ts.net:8443 URL is private operational
information and must not be committed or pasted into logs. https+insecure
applies only to the loopback hop to Cockpit's local certificate; the browser
connection uses the Tailscale-provisioned certificate and the tailnet remains
end-to-end encrypted.
Log in at the reported private URL as vm-operator. Do not add this URL to
Caddy, public DNS, monitoring services, or OAuth allowlists.
Verification
On the VM
cd /opt/stratofusion/deploy
sudo bash ./vm-admin-access.sh status
sudo bash ./vm-admin-access.sh verify
sudo tailscale serve status
sudo sshd -T | grep -E '^(passwordauthentication|permitrootlogin) '
sudo ufw status verbose
ss -ltn | grep ':9090'
ss -ltn | grep -E '127\.0\.0\.1:(5050|5540)'
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml exec -T app \
wget -qO- http://127.0.0.1:3000/api/health
Pass conditions:
SSH password authentication and root login are no;
Tailscale is connected;
Cockpit listens only on 127.0.0.1:9090;
enabled database tools listen only on 127.0.0.1:5050 and
127.0.0.1:5540;
no UFW allow rule exists for 9090 or 8443 on the public interface;
no UFW allow rule exists for 5050 or 5540 on the public interface;
Tailscale Serve reports only the intended private 8443 proxy;
application containers and the app health endpoint remain healthy.
From Windows
Connect with VS Code Remote SSH over the original public hostname.
Connect again using the private MagicDNS name and the same SSH key.
Open the private Cockpit URL while Tailscale is connected.
Turn Tailscale off temporarily and confirm the Cockpit URL stops working.
Confirm the public hostname does not accept TCP 9090 or 8443.
With the SSH tunnel open, confirm pgAdmin and RedisInsight load on local
ports 5050 and 5540; close the tunnel and confirm both stop responding.
Confirm the public hostname does not accept TCP 5050 or 5540.
Confirm the existing public app and observability URLs still behave as they
did before the change.
Do not test cron, OAuth, uploads, downloads, backup, sync, bisync, or rclone as
part of administrative-access verification.
Rollback
First verify a separate public key-only SSH session works. Do not take Tailscale
down while it is the only working administrative path.
Disable the Cockpit surface and restore any pre-existing Cockpit files:
cd /opt/stratofusion/deploy
sudo bash ./vm-admin-access.sh rollback-cockpit
This stops Tailscale Serve, disables cockpit.socket, restores prior Cockpit
configuration when one existed, and leaves Tailscale connected to avoid a
lockout. It does not touch Docker, Caddy, Compose, volumes, SSH keys, or
application data.
To disable only the optional database tools while preserving their volumes,
remove management from COMPOSE_PROFILES, then remove the two stopped
containers through Compose:
deluser leaves the home directory by default. Archive or remove that directory
only after reviewing it; do not use an automatic recursive delete.
To disconnect Tailscale, first prove public SSH in a second terminal, then:
sudo tailscale down
Use sudo tailscale logout only when the device should also be removed from its
tailnet identity. Remove the device in the Tailscale admin console and remove
its grant/tag separately.
Package removal is optional and should happen only after rollback verification:
Review /etc/apt/sources.list.d/tailscale.list and the Tailscale archive keyring
manually before removing repository files. Package removal must not be combined
with Docker, Caddy, volume, or application cleanup.
CI Follow-Up
The VM deploy workflow currently connects over public SSH. Keep port 22
available until a separate change adds an ephemeral Tailscale identity to the
GitHub Actions job, validates deployment and rollback from CI, and preserves a
provider-console escape path. That follow-up must use short-lived workload
identity rather than a reusable Tailscale auth key.
Unsupported Additions
Do not install a graphical Ubuntu desktop, XRDP, VNC, Guacamole, code-server,
or Portainer on this VM through this runbook. Portainer would receive
root-equivalent Docker control and requires a separate threat-model and change
approval.