Candidate access for one operator's VPS
This optional infrastructure profile gives one trusted operator a private SSH road to their own VPS and a restricted forward to its local Altar. It is candidate tooling, not native Tether, Veil, general remote IAM, shared multiuser isolation, or a qualified remote browser deployment. The existing Basic credential still grants the full local Magus authority. Never share it. Installed two-machine, hostile-browser, reboot, and revocation acceptance remains pending.
Boundaries
The administrator owns WireGuard through host wg-quick and systemd. The application receives
no networking capability or tunnel keys. A separate sshd binds only the tunnel address on TCP
2222; it permits one destination, 127.0.0.1:7134, and denies sessions and other forwarding.
The existing administrator sshd and its ingress are preserved. The Altar remains loopback-only.
Both transport units bind their lifetime to firewalld; losing the firewall stops the private road.
That is a declared systemd dependency, still requiring the real firewall-loss acceptance below.
flowchart LR
B[Dedicated laptop browser\n127.0.0.1:7134] --> F[Loopback SSH forward]
F --> W[WireGuard\nVPS UDP 51820]
W --> S[Restricted VPS sshd\ntunnel address TCP 2222]
S --> A[Altar on VPS\n127.0.0.1:7134 + Basic authentication]
The candidate admits one IPv4 /32 peer, no default/LAN routes, no DNS changes, no NAT, and no IP forwarding. It does not manage arbitrary pre-existing networking. Installation is fresh-only; an exactly matching completed installation is verified without writes. Changed intent, partial installation, foreign files, enabled forwarding, or ambiguous firewall ownership refuses. Revocation is an explicit, durable administrator operation; updates and key rotation require a separately reviewed replacement. A revoked installation is never re-enabled by apply.
Optional fresh-pilot preparation
For the standard fresh pilot, lychd_access_prepare: true and a separate
lychd_access_prepare_target: HOST opt into prerequisite automation. Both are disabled by default;
the ordinary lychd_access_enabled and lychd_access_target gates still apply. Run the same
playbook with --check first and review its package/policy/reload plan. Existing NetworkManager
and firewalld must already be running, SELinux must already enforce, and the actual external
zone must already include the standard SSH service permanently. Keep a tested provider console.
Preparation installs only missing named distribution package capabilities using existing
repositories without refresh or distribution upgrade. It reserves only lychd-wg through a new
NetworkManager device section, adds absent exact SELinux SSH port/file mappings, and creates the
empty DROP zone. Conflicting custom mappings, foreign interfaces/accounts/configuration, and
partial preparation refuse. The check plan does not qualify later package-dependent policy
checks: apply observes those before adding policy. This is a bootstrap for the standard pilot,
not an automatic repair of arbitrary existing host networking.
This opt-in may reload NetworkManager configuration and firewalld. A firewalld reload activates
all pending permanent settings and discards runtime-only changes; a NetworkManager configuration
reload can activate other pending changes. Review that state before authorizing the exact prepare
target. This is the explicit exception to the no-reload manual-preparation path below. Existing
completed access enters read-only verification instead. A failure leaves the root-owned
/etc/lychd-access-prepare.json incomplete marker and requires administrator recovery; re-running
does not silently continue partial preparation. Custom hosts can keep lychd_access_prepare=false
and use the explicit manual prerequisites.
Prepare the host and public inputs manually
Use the existing administrator and retain a tested provider-console recovery path. Install the
distribution's wireguard-tools, OpenSSH, firewalld, and Python 3 first. Keep host IP forwarding
disabled. The candidate requires a running firewalld with no source-bound zones or direct rules.
It admits the unchanged vendor allow-host-ipv6 ICMPv6 bootstrap policy and the five unchanged,
administratively disabled vendor gateway policies shipped with firewalld 2.5.2. The gateway
objects are gateway-dmz-to-HOST, gateway-lan-to-HOST, gateway-lan-to-work,
gateway-lan-to-world, and gateway-world-to-HOST. Both runtime and permanent inventories must
match; every gateway object's disabled state, target, priority, zone sets, services and absence
of extra rules are checked. Their permanent paths must remain under /usr/lib/firewalld/policies
with no corresponding /etc/firewalld/policies override. The guard observes these objects; it
does not enable, disable, delete or reload them. Firewalld's
administrative disable
prevents activation even when zone membership changes. Unknown policies still refuse even when
currently inactive, as do enabled or changed gateway policies. Do not disable the vendor IPv6
policy to make this profile pass. This narrow prerequisite may exclude a custom workstation.
Before the first access bootstrap, review the permanent firewall configuration and policies,
then establish a trusted fresh firewalld runtime through a controlled boot or an explicitly
reviewed reload. Retain independent administrator SSH access and a tested provider console:
a reload also applies unrelated pending permanent configuration and discards runtime-only
changes. Observing disabled=yes reports current state; it does not prove the absence of a
pending timeout that will remove the disable. Do not perform timed policy operations during
the managed access transport's lifetime. The privileged firewall administrator is trusted;
the lychd account must not have authority to change the firewall. These are operator
prerequisites, not properties proved by the policy validator.
When NetworkManager runs, reserve lychd-wg as unmanaged in the administrator's network policy
before installation, so it does not adopt the interface created by wg-quick. The candidate
refuses a NetworkManager connection naming that interface and does not reload NetworkManager.
Prepare an empty lychd-access firewalld zone with target DROP in both runtime and permanent
configuration. Creating a zone requires a reload, which may activate unrelated permanent changes;
review and perform that administrative step separately. For a new zone, the commands are
firewall-cmd --permanent --new-zone=lychd-access,
firewall-cmd --permanent --zone=lychd-access --set-target=DROP, and a separately approved
firewall-cmd --reload. Do not use these to replace an existing zone. With prerequisite automation
disabled, the playbook does not reload firewalld and adds only its declared interface and exact rules.
Copy deployments/ansible/access.example.yml to private working material outside the checkout.
Supply the laptop's WireGuard public key and a dedicated ed25519 SSH public key. Generate
and retain laptop private keys locally; never put them in inventory or send them to the VPS.
Choose unused, non-overlapping tunnel addresses and the actual external interface/zone.
For the example address pair, the laptop's private WireGuard configuration has this shape:
[Interface]
Address = 10.203.71.2/32
PrivateKey = REPLACE_WITH_LAPTOP_PRIVATE_KEY_LOCALLY
[Peer]
PublicKey = REPLACE_WITH_VPS_PUBLIC_KEY
Endpoint = REPLACE_WITH_VPS_PUBLIC_ADDRESS:51820
AllowedIPs = 10.203.71.1/32
Keep that file mode 0600 outside the checkout. Do not add a default route or DNS field. Add
PersistentKeepalive = 25 only when the laptop's NAT requires it. Use one laptop interface owner
(NetworkManager or wg-quick), and verify its only route is the selected VPS address. Keep the
dedicated SSH private key passphrase protected; the foreground wrapper lets SSH prompt for that
passphrase and does not use an ambient SSH agent.
This candidate requires SELinux Enforcing, with getenforce, semanage, matchpathcon, and
restorecon installed. Prepare the exact SSH port and file-context mappings before running the
playbook. Inspect semanage port -l, semanage fcontext -l -C, and the installed SSH policy first.
When the port and paths are unclaimed, the administrator's explicit additions are:
semanage port -a -t ssh_port_t -p tcp 2222
semanage fcontext -a -t sshd_key_t '/etc/lychd-access/ssh_host_ed25519_key(\.pub)?'
semanage fcontext -a -t ssh_home_t '/etc/lychd-access/authorized_keys'
If a mapping already exists, inspect it; do not replace another service's policy with -m to make
this profile pass. matchpathcon -n on the three declared key paths must report the expected types
even before files exist. The role refuses an unadmitted port, missing mappings, or permissive SSH
domains before creating its directory. It applies restorecon only to its newly owned files and
checks labels again on verification. The dedicated sshd uses PidFile none to avoid an undeclared
runtime PID-file label. Activation never disables enforcement, changes policy, or generates broad
audit2allow rules; optional preparation adds only the declared absent mappings. Confined SSH
forwarding to port 7134 and the installed service's actual process
domain still require a successful two-device acceptance and AVC-log inspection under Enforcing.
These commands follow SUSE's port and file-context procedure;
the installed Tumbleweed policy remains the compatibility authority.
From deployments/ansible, first run ansible-playbook -i PRIVATE_INVENTORY playbooks/access.yml
--limit HOST -e @PRIVATE_ACCESS_VARS --check. The default is disabled. Activation additionally
requires lychd_access_enabled=true and lychd_access_target=HOST. It changes only the named
host, creates host keys once, and refuses partial state instead of guessing recovery.
For a fresh pilot, edit the private copy with the actual public keys and network information:
lychd_access_enabled: true
lychd_access_target: pilot
lychd_access_prepare: true
lychd_access_prepare_target: pilot
Use the controller from the Server guide.
Keep its shell's LYCHD_DEPLOY_WORK set to that private directory. Preview, apply and observe:
cd deployments/ansible
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" -i "$LYCHD_DEPLOY_WORK/inventory.yml" \
playbooks/access.yml -e "@$LYCHD_DEPLOY_WORK/access.yml" --limit pilot -K --check
# After reviewing the preparation/reload plan, repeat without --check.
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" -i "$LYCHD_DEPLOY_WORK/inventory.yml" \
playbooks/access-verify.yml -e "@$LYCHD_DEPLOY_WORK/access.yml" --limit pilot -K
An unchanged second apply verifies without regenerating keys, reloading configuration or
restarting services. access-verify.yml never provisions. Its result proves the configured
host boundary; a successful two-device connection is a separate observation. Verification and
revocation fail when their enable/target inputs are missing, rather than returning a skipped success.
The result exposes only the WireGuard public key and dedicated SSH host public key. Verify those
over the existing trusted administrator connection, then prepare the laptop's WireGuard peer and
a dedicated known_hosts file. Its host entry is [VPS_TUNNEL_ADDRESS]:2222. Do not use
StrictHostKeyChecking=no or treat an unauthenticated ssh-keyscan result as trust evidence.
Laptop WireGuard configuration uses its own /32 address, the VPS public key and public UDP
endpoint, and only VPS_TUNNEL_ADDRESS/32 in AllowedIPs. NAT keepalive is optional and explicit.
Open the single-operator browser road
The client preparation tool renders a private WireGuard configuration, pinned SSH known-host entry and connection profile from explicit public server material and locally retained keys. It never imports a connection, changes routes, opens a browser or starts a service. Existing different output refuses; unchanged output is observed without rewriting it. Private input and output must stay outside checkouts. Forwarding requires an explicitly named, up WireGuard interface and an actual route through that interface; a LAN or default-route fallback refuses.
Install wireguard-tools and OpenSSH clients on the laptop through its trusted distribution
repositories. Create a private workspace outside every checkout, and generate device-specific
keys once. These commands refuse to overwrite the WireGuard files; retain existing keys on repeat:
LYCHD_ACCESS_WORK="$HOME/.local/state/lychd-access/pilot"
mkdir -p "$LYCHD_ACCESS_WORK"
chmod 700 "$LYCHD_ACCESS_WORK"
(umask 077; set -o noclobber; wg genkey > "$LYCHD_ACCESS_WORK/wireguard.key")
(umask 077; set -o noclobber; wg pubkey < "$LYCHD_ACCESS_WORK/wireguard.key" > "$LYCHD_ACCESS_WORK/wireguard.pub")
ssh-keygen -t ed25519 -f "$LYCHD_ACCESS_WORK/forward_ed25519" -C lychd-forward
Protect the SSH key with a passphrase. Put only wireguard.pub and the first two fields of
forward_ed25519.pub into the private server inventory; the host input deliberately accepts no
SSH key options or comment. Neither private key leaves the laptop.
Copy client.example.json
to $LYCHD_ACCESS_WORK/client.json, mode 0600. Fill the matching addresses, unique interface name,
public UDP endpoint, local private-key paths and the two server public keys obtained through the
trusted administrator connection. Do not trust an unauthenticated network key scan.
Use distinct interface names and non-overlapping address pairs for multiple VPS connections.
The browser forward still occupies one fixed local port, so open one instance's forward at a time.
python3 deployments/access/client.py --config "$LYCHD_ACCESS_WORK/client.json" \
--output-directory "$LYCHD_ACCESS_WORK/generated" --check
python3 deployments/access/client.py --config "$LYCHD_ACCESS_WORK/client.json" \
--output-directory "$LYCHD_ACCESS_WORK/generated"
The output contains lychd-vps.conf, exact known_hosts and connection.json. The directory is
0700, files are 0600, and a second identical render changes nothing. A different or partial output
refuses. The WireGuard configuration contains a private key: never attach it to an issue or put
it in a source/archive build. No interface, route or service has been changed by rendering.
On the KDE laptop, choose NetworkManager as the interface owner. Confirm that the selected connection/interface name is unused before importing; stop on a collision. For the example name:
sudo nmcli connection import type wireguard file "$LYCHD_ACCESS_WORK/generated/lychd-vps.conf"
sudo nmcli connection modify lychd-vps connection.interface-name lychd-vps \
connection.autoconnect no ipv4.never-default yes ipv6.method disabled
sudo nmcli connection up lychd-vps
Only this new tunnel has IPv6 disabled; the laptop's ordinary IPv6 configuration is unchanged.
Do not also start wg-quick for the same interface. Automatic connection at login/boot is an
explicit later choice after the first acceptance. The generated peer route is only the VPS /32.
After the laptop WireGuard connection is established, use the rendered connection profile. First check local files, the up WireGuard device, route and free port:
python3 deployments/access/forward.py --profile "$LYCHD_ACCESS_WORK/generated/connection.json" --check
python3 deployments/access/forward.py --profile "$LYCHD_ACCESS_WORK/generated/connection.json"
The check starts no SSH session and proves no remote handshake, application or browser result.
Explicit connection options remain available with --peer, --identity, --known-hosts and
--interface; omitting the interface refuses. The tool uses a fixed 127.0.0.1:7134 local bind and refuses an
occupied port. Verify the actual listener with ss -ltn before opening the browser; the SSH
server cannot enforce the client-side bind address. End the foreground process to close it.
SSH accepts only the pinned server key and the selected device key, permits a local key-passphrase
prompt, and offers no password/keyboard-interactive fallback. Its connection timeout is bounded.
Use a dedicated browser profile per VPS instance, without untrusted extensions. Do not reuse
the same profile across servers behind the same localhost origin: HTTP Basic credentials may be
cached and sent to the next server. Retrieve this instance's local credential through the trusted
administrator/runtime path and enter it only at http://127.0.0.1:7134. Host, Origin, CORS, CSRF,
and Basic checks remain intact; there is no header rewriting or public listener. An occupied
local port also prevents accidental collision with a laptop-local LychD.
Create and launch a separate Firefox profile for the instance through about:profiles; a normal
new tab or private window is not a replacement for that separation. Machine-wide Dark Reader
policy can also install the extension into this profile; disable it here unless deliberately
trusted for Altar content. No browser profile or login state is copied by the deployment tools.
Close the forward before changing/disabling the VPN. On unexpected loss, SSH keepalives terminate the failed connection; the tool selects no alternate endpoint or public Altar URL. Route preflight is an observation before SSH starts, not a continuously enforced client firewall.
| Observation | Interpretation and next check |
|---|---|
No recent server wg show lychd-wg latest-handshakes after a laptop connection attempt |
Check UDP endpoint/port, provider firewall, public keys and NAT; inspect only public handshake output. |
| Handshake succeeds, TCP 2222 fails | Check the tunnel route, exact firewalld policy and dedicated sshd/SELinux AVCs. |
| SSH refuses a host key | Stop and compare against the trusted administrator's host public key; never bypass checking. |
| Local 7134 is occupied | Close the existing forward/local instance. Do not use an arbitrary substitute port or browser profile. |
| Altar returns 401 | Transport reached authentication; enter this instance's private local credential in its dedicated browser. |
access-verify.yml returns revoked |
Transport is deliberately closed. Apply does not recover or re-enroll it. |
Revoke, recover, and qualify
Use the independent administrator connection for explicit revocation. The same private intent and exact target remain required; no application credential or runtime account is needed:
cd deployments/ansible
"$LYCHD_DEPLOY_WORK/controller/bin/ansible-playbook" -i "$LYCHD_DEPLOY_WORK/inventory.yml" \
playbooks/access-revoke.yml -e "@$LYCHD_DEPLOY_WORK/access.yml" --limit pilot -K --check
# Repeat without --check to revoke; use access-verify.yml for readback.
Revocation persists and fsyncs /etc/lychd-access/revoked.json before disabling/stopping
lychd-access-sshd.service and wg-quick@lychd-wg.service. Both boot paths check that marker,
including the receipt-owned WireGuard instance drop-in. An interrupted revocation can be repeated
to finish; a completed repeat changes nothing. Apply refuses revoked intent. The SSH service uses
control-group termination so established forwarded sessions are stopped with it. Verify both units inactive,
the interface absent, and a previously established browser connection unable to reach Altar.
Disablement must be preserved when restoring backups. Removing an authorized key alone does not
close existing connections. Preserve a separate revocation record outside old host snapshots;
never re-enable from a stale receipt without reviewing it. The host marker cannot survive a restore
that omits it: reconcile the independent revocation record before booting a restored host. Keep
the old device's access closed and use fresh keys for a separately admitted replacement; deleting
the marker or editing the receipt is not rotation.
Partial setup preserves evidence and requires manual inspection of /etc/lychd-access,
/etc/wireguard/lychd-wg.conf, the two named units, the owned
wg-quick@lychd-wg.service.d/10-lychd-access.conf drop-in, lychd-forward, and the declared firewall
rules. Do not blindly delete these paths or reuse another installation's keys. LychD's active
stop/restart and credential-rotation recovery remain separate unfinished contracts. A compromised
laptop can require quarantining/rebuilding its instance after transport revocation.
Before calling this supported, retain evidence for two-machine startup/reboot/reconnect, a no-change second apply, exact IPv4/IPv6 ingress, forbidden destinations and shell/subsystem/socket forwarding, Basic/Host/Origin/CSRF refusals, hostile framing and DNS rebinding, SSE, lost-tunnel failure, and revocation of an already established connection. Include a controlled firewalld-loss test proving both transports stop and no unfiltered private path remains; retain the provider console during that test. Repository tests and syntax checks alone prove none of these installed-host results.