| title | FBS Interlock Gateway |
|---|---|
| subtitle | Project Overview and Operations Reference |
| author | William Veith |
| date | 2026-08-10 |
| lang | en-US |
Purpose
This README provides the authoritative project-level overview for
fbs-interlock-gateway, including system architecture, security boundaries, interlock hardware, configuration, Admin behavior, development, deployment packaging, updates, logging, testing, and release operations.
Safety and security boundary
The gateway controls software access signals. Hardware interlocks and fail-safe circuitry remain authoritative. Production listener ports must remain restricted to the authorized FBS source, the Admin UI should remain bound to loopback, and generated credentials, certificates, private keys, and production mappings must remain outside version control.
- Project Overview
- Capabilities
- Safety and Security Model
- System Architecture
- Interlock Hardware
- Repository Layout
- FBS-Facing Behavior
- Gateway-to-Shelly Communication
- Shelly Authentication Helper
- Shelly Mutual TLS
- Admin UI
- Admin API
- Configuration
- Development and Validation
- Building Deployment Packages
- Deployment Build Output
- Release Binaries
- Service Templates and Installed Layouts
- Automatic Updates and Log Maintenance
- Continuous Integration
- Release Workflow
- Branch and Pull-Request Workflow
- Local Testing
- Runtime Behavior
- Logging
- Repository Safety
- License
fbs-interlock-gateway is a Go service that lets the FBS interlock system control networked tool interlocks through a local gateway server.
The gateway receives FBS HTTP interlock requests, maps each request to a configured tool listener, communicates with the assigned Shelly relay or network interlock over HTTP or HTTPS RPC, and returns the simple JSON state response expected by FBS. Shelly communication can use HTTP Digest Authentication, mutual TLS, or both.
The project is intended for controlled facility deployments where FBS communicates with the gateway and the gateway communicates with configured interlock devices. A shared in-memory status store lets normal FBS traffic passively update the local Admin UI without creating recurring Shelly status traffic.
FBS server
-> source-restricted gateway listener
-> fbs-interlock-gateway
-> Shelly RPC over HTTP or HTTPS
-> Shelly-based interlock hardware
-> tool interlock / monitor circuit
The repository covers the complete interlock system boundary: application behavior, device communication, security, service supervision, deployment, maintenance, physical wiring, enclosure labeling, and auditable device identity.
| Document | Scope |
|---|---|
| Linux Installation and Operations Guide | Linux build, installation, systemd supervision, UFW, updates, logging, troubleshooting, and uninstall |
| Windows Installation and Operations Guide | Windows deployment, Task Scheduler supervision, firewall controls, updates, logging, rollback, and uninstall |
| macOS Installation and Operations Guide | macOS deployment, LaunchDaemons, Application Firewall, Packet Filter, updates, logging, rollback, and uninstall |
| Shelly Interlock Hardware Guide | Junction-box materials, per-assembly bills of materials, wiring configurations, label artwork, QR device identity, fabrication, verification, and maintenance |
| Security Policy | Supported versions, private vulnerability reporting, operational-safety limits, secret handling, coordinated disclosure, and safe-harbor expectations |
Use this README for project-wide behavior and architecture. Use the platform guides for installation and operations, and use the hardware guide for physical interlock construction and audit documentation.
- one FBS-facing listener port per configured tool
- FBS-compatible
/status,/on, and/offendpoints - supported query-based on/off command formats
- Shelly Gen2/Gen3 RPC over per-tool
httporhttps - optional Shelly HTTP Digest Authentication with reusable per-device digest sessions
- optional mutual TLS with Shelly server verification and gateway client authentication
- certificate-generation helpers for separate private server/client CAs, the gateway identity, and per-device Shelly identities
- canonical PKI source layout under
pki/, with only required runtime trust/identity files copied into deployment builds - version-controlled Shelly interlock wiring diagrams, junction-box label artwork, per-assembly bills of materials, hardware build documentation, and QR-encoded device identity records for rapid auditing
- shared HTTP connection reuse, TLS session caching, and per-device RPC serialization
- FBS-priority device scheduling that defers or cancels lower-priority Admin probes
- transient status retry for bounded timeout/connection failures and controlled recovery for persistent Shelly HTTP
423and429responses - request-phase diagnostics for DNS, TCP, TLS, response-header, and response-body failures
- an embedded Admin UI bound to localhost by default
- a shared status store passively updated by normal FBS traffic
- cache-only Admin UI polling that does not contact Shelly devices
- explicit on-demand fleet refresh with up to 32 concurrent workers
- incremental fleet-refresh publishing that preserves completed partial results
- revision-protected status merging so older Admin scans cannot overwrite newer FBS results
- visible disconnected, safe-output, protocol, and error states
- editable per-tool protocol and gateway mutual-TLS file paths
- password masking and preservation in the Admin API
- validated, atomic configuration writes with
.bakbackups - deep-cloned configuration snapshots that prevent unintended shared-state mutation
- Linux AMD64 and ARM64 deployment packages with runtime TLS files and rendered PDF guides
- Windows AMD64 deployment packages with runtime TLS files, production/development installers, managed updates, and rendered PDF guides
- macOS ARM64 and AMD64 deployment packages with runtime TLS files, production/development installers, managed updates, Packet Filter rules, and rendered PDF guides
- Linux systemd supervision, UFW source restriction, authenticated signed-checksum updates, downgrade protection, and standard or purge uninstall
- Windows Task Scheduler supervision under restricted runtime credentials, source-restricted Windows Firewall rules, authenticated signed-checksum updates, log rotation, downgrade protection, and rollback
- macOS LaunchDaemon supervision under a dedicated service account, Application Firewall registration, managed
pfsource restriction, authenticated signed-checksum updates, log rotation, downgrade protection, and rollback - an embedded Ed25519 update-trust public key and dependency-free
verify-update-checksumcommand used by installed updaters - Go 1.26 development with
go vet, Staticcheck, race testing, Bash syntax validation, ShellCheck, PowerShell validation, plist validation, and cross-platform build checks - weekly Dependabot maintenance for Go modules and GitHub Actions
- full-commit-SHA-pinned GitHub Actions and a separately pinned reusable trusted-builder workflow
- SLSA v1 build provenance for release binaries and checksum files
- Ed25519 detached signatures over authenticated release checksum files
- GPG-signed annotated release tags
- immutable GitHub Releases with release/asset attestation verification
- a unified
make verifyvalidation gate
The gateway controls access signals, but it is not a substitute for hardware safety controls.
FBS server
-> host firewall source restriction
-> gateway tool listener
-> Shelly RPC over HTTP or HTTPS
-> optional mutual TLS
-> optional Digest Authentication
-> Shelly relay or network interlock
-> tool monitor / enable / interlock circuit
Important operational rules:
- Hardware interlocks and fail-safe circuitry remain authoritative.
defaults.safe_state_on_errorcontrols the state reported by software when a device cannot be reached. It does not prove the physical relay state.- A successful
Switch.Setresult updates the Admin status cache to the requested state, but only a laterSwitch.GetStatusindependently verifies the device output. - Production gateway listener ports should be reachable only from the authorized FBS source.
- The Admin UI should remain bound to a loopback address unless remote access is intentionally secured.
- Shelly credentials are stored locally in
config.yaml. - Gateway TLS private keys and private CA material must remain outside version control.
- Deployment packages contain only the gateway runtime trust certificate, gateway client certificate, and gateway client private key. CA private keys, CSRs,
client-ca.crt, and per-device Shelly private keys remain on the certificate-management machine. - Existing installed configurations and TLS identities are preserved during normal reinstallation on all supported platforms.
- Real credentials, certificates, private keys, and production mappings must not be committed to the repository.
The production and development installers apply the same platform security boundary. Development mode disables managed release replacement; it does not weaken runtime firewall controls.
| Platform | Installer behavior |
|---|---|
| Linux | Installs UFW when needed, sets default incoming traffic to deny, allows outgoing traffic, permits the configured FBS source IP to the configured gateway TCP port range, and enables UFW. The uninstaller removes the gateway-specific UFW rule without disabling UFW or reversing system-wide defaults. |
| Windows | Sets the Domain, Private, and Public profiles' default inbound action to Block, removes the older unrestricted gateway rule, and adds an inbound rule restricted by executable, TCP listener range, and authorized FBS source IP. |
| macOS | Adds the executable to the Application Firewall allow list and installs a managed Packet Filter anchor that permits loopback, permits the authorized FBS source to the gateway listener range, and blocks other inbound TCP traffic to that range. The installer validates the complete candidate pf.conf and preserves unrelated rules. |
The generated platform rules use these Makefile variables:
FBS_SOURCE_IP = <authorized-fbs-source>
FBS_PORT_RANGE = 8081:8981Review them before building a production deployment. Keep the Admin UI on 127.0.0.1; the generated listener-range rules are not a replacement for Admin authentication or remote-access controls.
Release integrity is intentionally split across build, publication, and deployed-update trust boundaries.
source commit on main
|
v
pinned reusable trusted builder
|
+-> validated release binaries + SHA-256 files
|
+-> SLSA v1 provenance via GitHub OIDC/Sigstore
|
v
publish job re-verifies provenance
|
+-> Ed25519-signs each authenticated .sha256 file
|
+-> verifies the embedded updater public key
|
+-> creates and locally verifies a GPG-signed tag
|
v
immutable GitHub Release
|
+-> release attestation
+-> exact asset attestation verification
|
v
installed gateway verifies .sha256.sig
before trusting an update checksum
Important trust properties:
- The release caller uses the reusable workflow in
williamveith/fbs-interlock-gateway-builderat an exact full commit SHA. - The trusted build receives only the permissions required for build/provenance work; the release GPG key and update-signing private key are not exposed to repository build commands.
- The publish job verifies SLSA provenance against the expected repository, exact source commit,
refs/heads/main, exact trusted-builder workflow revision, SLSA provenance predicate, and a GitHub-hosted runner requirement before authorizing publication. - Only after provenance verification are the small
.sha256files signed with the release-environment Ed25519 update-signing key. - The gateway executable embeds only the matching Ed25519 public key. The private update-signing key remains an environment secret and is never deployed.
- The
verify-update-checksumcommand verifies the detached 64-byte Ed25519 signature over the exact checksum-file bytes, validates the SHA-256 format, and requires the signed asset filename to match the requested release asset. - Production updaters use the currently installed trusted gateway executable to verify the checksum signature; a downloaded candidate is never allowed to authenticate itself.
- Automatic updates require stable
vMAJOR.MINOR.PATCHmetadata and reject authenticated downgrades. Reinstalling the same version is allowed so an authenticated release can repair a locally altered binary. - GitHub release immutability and
gh release verify/gh release verify-assetprovide a separate publication-level check in addition to SLSA build provenance.
The repository's Security Policy defines vulnerability-reporting, operational-safety, secret-handling, and coordinated-disclosure expectations.
+------------+ HTTP +-----------------------+ +------------------+
| | /status /on /off | | HTTP or HTTPS RPC | |
| FBS Server | <-----------------> | fbs-interlock-gateway | <--------------------> | Shelly Relay Box |
| | | | Digest and/or mTLS | |
+------------+ +-----------------------+ +------------------+
| |
successful or | |
failed result | v
v Tool monitor / enable
+---------------------+ circuit changes state
| Shared status store |
+---------------------+
^
cache-only polling |
explicit fleet refresh |
|
+------------------------+
| Local-only Admin UI |
| http://127.0.0.1:18090 |
+------------------------+
Normal FBS traffic updates only the affected tool row in the shared status store. The browser reads that in-memory snapshot every three seconds without contacting any Shelly. The Refresh Status action explicitly runs a fleet-wide Switch.GetStatus scan with up to 32 workers and publishes each completed result immediately. FBS requests have priority over Admin probes for the same device.
The software is designed to operate with custom Shelly-based interlock junction boxes. Documentation on how to build the hardware interlocks is maintained under docs/hardware/.
The hardware documentation includes:
- a wiring configuration powered from an external 12 VDC supply
- a wiring configuration powered from the tool's 110–240 VAC supply
- junction-box label artwork for both power configurations
- a general materials list for building the interlock boxes
- bills of materials with procurement links and recorded per-assembly costs for both supported power configurations
- the recorded 30 W fiber-laser settings used to mark the box lids
- QR-code label data encoded as serialized JSON containing the Shelly device name, model number, and unique device ID
- a phone-based audit workflow for rapidly collecting and verifying installed-device identity information
Each junction-box QR code records the installed Shelly identity in a compact JSON object, such as {"device":"Shelly 1 Gen4","model":"S4SW-001X16EU","id":"A085E3B5325C"}. Scanning the label with a phone provides a fast, auditable record of the device type, model, and unique Shelly ID.
The hardware guide records estimated material costs of $43.88 per external-12-VDC assembly and $34.93 per tool-powered 110–240 VAC assembly using the source-workbook quantities and prices. Procurement prices are references rather than fixed quotations.
The diagrams document the associated interlock hardware but do not replace qualified electrical review, applicable codes, equipment ratings, or facility safety requirements.
cmd/fbs-interlock-gateway/
main.go
application entry point, CLI flags, version output, signal handling
internal/admin/
server.go
server_test.go
web/
embedded Admin UI, local Admin API, incremental fleet refresh,
and FBS-priority Admin status behavior
internal/config/
YAML loading, relative-path resolution, defaults, validation,
deep cloning, atomic writes, and backups
internal/fbs/
FBS-compatible HTTP request handling, responses, and status recording
internal/gateway/
application lifecycle, listener startup, restart, configuration ownership,
and shared dependencies
internal/process/
listener-port process utilities
internal/shelly/
HTTP/HTTPS RPC client, Digest Authentication, mutual TLS,
per-device request priority, retry, request timing, and controlled recovery
internal/status/
revision-protected shared in-memory Admin status store
internal/updateauth/
embedded Ed25519 update-trust public key, signed-checksum verifier,
CLI command implementation, and security-focused tests
scripts/pandoc/
inline-code wrapping filter used by deployment-guide PDF rendering
scripts/tls/
private-CA, gateway-client, and per-Shelly certificate generation
services/linux/
systemd service, production installer, development installer,
uninstaller, and updater templates
services/windows/
production/development launchers, PowerShell installer,
startup supervisor, updater, and uninstaller templates
services/macos/
production/development installers, startup wrapper, gateway and update
LaunchDaemon property lists, Packet Filter anchor, updater, and uninstaller
docs/
deployment guides/
platform-specific Markdown installation and operations guides rendered to PDF
during deployment builds
hardware/
Shelly interlock wiring diagrams, junction-box label artwork, and build notes
.github/dependabot.yml
weekly Go-module and GitHub Actions dependency maintenance
.github/workflows/
CI plus trusted-build, provenance, signing, immutable-release,
and publication-verification automation
SECURITY.md
vulnerability reporting, operational safety, disclosure, and safe-harbor policy
The generated pki/ and build/ directories are intentionally excluded from version control. Runtime TLS files are copied directly from the private pki/ source tree into generated deployment directories; there is no repository-root runtime tls/ staging directory.
The gateway exposes one HTTP listener per enabled tool. Each configured listener port represents one interlock target.
Example request path for an HTTPS Shelly:
FBS
-> http://<gateway-host>:<tool-port>/on
-> fbs-interlock-gateway
-> https://<shelly-host>/rpc/Switch.Set?id=<switch-id>&on=true
-> Shelly output command succeeds
-> shared Admin status row is updated
-> FBS receives the requested state
FBS only needs the gateway hostname and assigned gateway port. Shelly hostnames, per-tool protocols, switch IDs, credentials, certificates, and enable states remain in the local gateway configuration.
The FBS server does not impose a fixed three-second response write deadline. The Shelly client's configured end-to-end timeout controls the device operation, allowing valid HTTPS and mutual-TLS requests to complete when defaults.timeout_ms is intentionally greater than three seconds.
http://<gateway-host>:<port>/status
http://<gateway-host>:<port>/on
http://<gateway-host>:<port>/off
The gateway also accepts common query-based command formats:
?turn=on
?turn=off
?state=1
?state=0
?value=1
?value=0
Responses remain intentionally simple for FBS compatibility:
{"Success":1,"State":1}{"Success":1,"State":0}State: 1 means the reported interlock output is on.
State: 0 means the reported interlock output is off.
A successful /status, /on, or /off operation updates the corresponding Admin status row. A failed Shelly operation records connected: false, the configured safe output, and the error before returning the safe state to FBS.
Each tool selects its RPC scheme with tools[].protocol. Supported values are http and https; an omitted value remains backward-compatible and defaults to http.
Status requests use:
<protocol>://<shelly-host>/rpc/Switch.GetStatus?id=<switch-id>
Command requests use:
<protocol>://<shelly-host>/rpc/Switch.Set?id=<switch-id>&on=true
<protocol>://<shelly-host>/rpc/Switch.Set?id=<switch-id>&on=false
The tools[].ip field contains only the hostname or address. Do not include http://, https://, a path, or a port in that field.
For HTTPS tools, the gateway:
- verifies the Shelly server certificate against
defaults.shelly_tls.server_ca_file - validates the certificate hostname against the configured Shelly hostname
- presents
defaults.shelly_tls.client_cert_fileandclient_key_fileduring the mutual-TLS handshake - requires TLS 1.2 or newer
- reuses idle HTTP connections and caches TLS sessions where supported
When a username and password are configured, the gateway also uses HTTP Digest Authentication. Mutual TLS and Digest Authentication can be enabled together.
When credentials are blank or null, the gateway performs the RPC without a Digest Authorization header.
Each Shelly request attempt is bounded by defaults.timeout_ms. DNS, TCP connect, TLS handshake, and response-header phases are bounded within that attempt. Switch.GetStatus may perform one additional transient-network retry after a short delay, so a failed status operation can consume two attempt budgets plus the retry delay.
Additional behavior:
- requests to the same Shelly protocol/host are serialized so a previous response is fully consumed before the next RPC begins
- requests to different Shellys can proceed concurrently
- normal FBS
/status,/on, and/offoperations use high-priority device acquisition - Admin status probes are opportunistic and never wait behind an occupied device slot
- an arriving FBS operation cancels an active Admin probe for the same Shelly and takes the slot as soon as cancellation unwinds
- Admin deferral is treated as an expected scheduling result, not a connectivity failure
- the FBS operation records the authoritative result in the same shared status store used by the Admin UI
- Digest challenges are retained in a per-device session, allowing subsequent requests to send a preemptive Authorization header with an increasing nonce count
- digest sessions expire after 55 minutes or 30,000 nonce uses and are replaced when the device returns a new challenge
- retryable status failures are limited to network timeouts, EOF/unexpected EOF, connection reset, connection refused, and broken-pipe conditions and are retried once after 150 milliseconds
- deprecated
net.Error.Temporary()classification is not used - certificate validation errors, caller cancellation/deadline completion, and Admin deferrals are not retried
- HTTP
429 Too Many Requestswaits two seconds and retries once - a persistent HTTP
429or an HTTP423 Lockedschedules one controlledShelly.Rebootrequest - reboot recovery uses a 500-millisecond device reboot delay, suppresses duplicate in-flight attempts, and enforces a five-minute per-device cooldown
- response and error bodies are bounded to 4 KiB
- network failures identify the observed phase such as DNS lookup, TCP connect, TLS handshake, response headers, or response body, and report whether the connection was reused or the TLS session resumed
The repository includes:
scripts/set-shelly-auth.sh
Run it directly:
chmod +x scripts/set-shelly-auth.sh
./scripts/set-shelly-auth.shor through Make:
make shelly-authThe helper:
- reads device information from
Shelly.GetDeviceInfo - obtains the Shelly authentication realm
- computes the
ha1value for theadminaccount - calls
Shelly.SetAuth - verifies authenticated access with
Switch.GetStatus
The runtime client caches the resulting Digest challenge state per device, reducing repeated unauthenticated challenge round trips.
The project uses two private certificate authorities so server and client trust are separated.
| File | Role | Installed or uploaded to |
|---|---|---|
server-ca.crt |
Trust anchor used by the gateway to verify Shelly HTTPS server certificates. | Gateway runtime |
server-ca.key |
Signs per-device Shelly server certificates. | Certificate-management machine only |
client-ca.crt |
Trust anchor used by each Shelly to verify the gateway client certificate. | Shelly devices |
client-ca.key |
Signs the gateway client certificate. | Certificate-management machine only |
gateway-client.crt |
Gateway mutual-TLS client identity. | Gateway runtime |
gateway-client.key |
Gateway client private key. | Gateway runtime |
shelly-server.crt |
Unique HTTPS server identity for one Shelly hostname. | Matching Shelly device |
shelly-server.key |
Private key for that Shelly server certificate. | Matching Shelly device |
Generate both private CAs:
make caThis creates:
pki/ca/
├── server-ca.crt
├── server-ca.key
├── client-ca.crt
└── client-ca.key
The CA script refuses to overwrite existing CA material. By default, CA certificates are valid for 100 years; set CA_VALID_DAYS to override that duration.
Generate the gateway client identity:
make gateway-certThis creates the full gateway material under pki/gateway/. Gateway and Shelly leaf certificates default to 100 years; set CERT_VALID_DAYS to override that duration. Upload pki/ca/client-ca.crt to every Shelly as the CA authorized to verify the gateway client certificate.
Run:
make shelly-certThe helper prompts for:
- the interlock name, used as the output-directory name
- the Shelly UT Austin Dynamic DNS hostname, such as
2c41389b0d77.dynamic.utexas.edu
The hostname is normalized to lowercase and must not include a URL scheme, path, or port. The generated certificate contains the DDNS hostname as both its common name and DNS subject alternative name; no IP address is embedded.
Output is written to:
pki/shellys/<interlock-name>/
├── shelly-server.cnf
├── shelly-server.csr
├── shelly-server.crt
└── shelly-server.key
Install the generated shelly-server.crt, shelly-server.key, and pki/ca/client-ca.crt on the matching device. Keep all CA private keys and generated device private keys securely outside the repository.
The Admin UI is embedded in the Go executable with Go's embed package.
Default address:
http://127.0.0.1:18090
No separate runtime web/ directory is required.
The interface provides:
- a status table for all configured tools
- connected, disconnected, output, error, and protocol states
- one explicit fleet refresh when the page initially loads
- a Refresh Status button for on-demand fleet verification
- cache-only status polling every three seconds
- passive row updates from normal FBS
/status,/on, and/offtraffic - paused cache polling while the page is hidden
- an immediate cache read when the page becomes visible again
- prevention of overlapping browser requests and duplicate fleet refreshes
- up to 32 concurrent Shelly status workers during an explicit fleet refresh
- incremental row publication as each worker completes
- preservation of completed partial results if the overall refresh times out or is canceled
- FBS-priority scheduling that skips or cancels Admin probes when production traffic needs the same device
- editable configuration fields, including per-tool
httporhttps - editable mutual-TLS server CA, client certificate, and client-key paths
- automatic selection of the next available listener port
- duplicate-name, duplicate-address, duplicate-port, protocol, TLS-path, and field validation
- add and delete tool controls
- password-set indicators without returning stored passwords
- explicit password replacement or clearing
- notifications for loading, validation, save, refresh, and restart results
- safe text rendering for names, addresses, and error messages
- cache-disabled API requests
- an automatic restart request after a successful save
Disabled tools remain visible in configuration and status data but are not contacted.
The Admin UI is not a second device-control loop.
FBS request
-> Shelly operation
-> gateway records the result for that tool
-> browser reads the in-memory snapshot
An ordinary browser status poll does not contact a Shelly. This preserves a near-live operational view without restoring the previous fleet-wide status scan every three seconds.
The first page load and the Refresh Status button use an explicit fleet refresh. During that refresh, the browser checks the in-memory snapshot once per second until the server reports completion. Those checks do not start additional device requests.
Fleet workers write each successful or failed device result directly into the shared status store as soon as it completes. Healthy rows therefore become visible while slower devices are still pending, and completed rows remain available if the two-minute refresh context expires.
The shared store uses monotonically increasing revisions. A slow manual refresh that started earlier cannot overwrite a newer FBS result that completed later.
A successful /on or /off result represents the state accepted by Switch.Set. Use Refresh Status when an independent Switch.GetStatus verification is required.
Admin status probes use a distinct low-priority Shelly operation:
- an Admin probe does not queue behind another request
- an active Admin probe is canceled when an FBS operation for the same Shelly arrives
- the canceled Admin probe preserves the existing row instead of recording a false connectivity error
- the FBS status or command result updates the same shared row the Admin probe was attempting to refresh
- Digest nonce use and relay operations remain serialized because only one actual Shelly RPC is active per device
This keeps Admin fleet verification responsive without allowing it to consume the FBS request timeout budget.
The Admin server includes:
- explicit HTTP method handling
- strict single-object JSON decoding
- rejection of unknown JSON fields
- a request body size limit
- read, write, idle, header, and shutdown timeouts
- a two-minute explicit-refresh timeout
- at most 32 concurrent device-status requests during a fleet refresh
- one active fleet refresh at a time
- immediate per-tool publishing instead of fleet-sized all-or-nothing commits
- FBS-priority preemption of Admin device probes
- revision-protected merging of refresh results into the shared store
- no-store headers for API responses
- same-origin checks for state-changing requests
- rejection of cross-site state-changing requests
Content-Security-PolicyX-Content-Type-Options: nosniffX-Frame-Options: DENY- restrictive referrer and permissions policies
Keep the Admin UI on loopback whenever possible.
Set an explicit address:
./fbs-interlock-gateway \
-config config.yaml \
-admin 127.0.0.1:18090Disable the Admin UI:
./fbs-interlock-gateway \
-config config.yaml \
-admin ""ssh -L 18090:127.0.0.1:18090 fbs-gateway@<gateway-host>Then open:
http://127.0.0.1:18090
GET /api/config
PUT /api/config
GET /api/status
GET /api/status?refresh=1
POST /api/restart
Returns the loaded configuration without returning stored passwords. The response includes normalized per-tool protocols and the gateway defaults.shelly_tls paths.
A tool with a stored password reports:
{
"password_set": true
}An omitted tool protocol is returned as http.
Accepts edited configuration, validates it, preserves existing passwords unless replacement or clearing is explicitly requested, writes the file atomically, and creates config.yaml.bak from the previous file when possible.
The request can update:
- per-tool
httporhttpsprotocol - gateway Shelly-server CA path
- gateway client-certificate path
- gateway client-key path
A successful response indicates that a restart is required.
Returns the current shared in-memory snapshot and does not contact Shelly devices:
[
{
"interlock_name": "EQU-EXAMPLE-TOOL-01",
"ip": "interlock-01.example.local",
"protocol": "https",
"port": 8081,
"switch_id": 0,
"enabled": true,
"connected": true,
"output": false
}
]The response includes:
X-Status-Refresh-In-Progress: false
Enabled tools begin with the configured safe output and status not yet refreshed until an FBS operation or explicit fleet refresh records a result. Disabled tools remain visible without a placeholder error.
When an interlock operation fails, connected is false, the configured safe output is reported, and the error is included.
Starts one asynchronous fleet-wide Switch.GetStatus refresh and immediately returns the current snapshot.
While the refresh is active, responses include:
X-Status-Refresh-In-Progress: true
Additional explicit refresh requests share the in-progress scan rather than creating duplicate Shelly traffic. The browser performs cache-only follow-up reads until the header changes to false.
Refresh results are merged by tool and revision. A result from an older scan cannot overwrite a newer FBS-derived status.
Requests a clean process restart. The platform service supervisor starts the process again in an installed deployment.
The service loads YAML from the path supplied with -config.
When -config is omitted, the gateway looks for config.yaml beside the executable.
Relative mutual-TLS paths are resolved against the directory containing the loaded configuration file. For example, when the installed config is /etc/fbs-interlock-gateway/config.yaml, ./tls/server-ca.crt resolves to /etc/fbs-interlock-gateway/tls/server-ca.crt. Windows and macOS installers likewise run the gateway with their configuration directories as the working directory.
Create a starter config:
make init-configThe target preserves an existing local config.yaml. The generated starter uses a five-second Shelly request-attempt timeout and relative TLS paths that work with the installed layout on every supported platform. If defaults.timeout_ms is omitted from an existing configuration, the runtime default remains three seconds:
bind: 0.0.0.0
defaults:
timeout_ms: 5000
safe_state_on_error: "off"
shelly_tls:
server_ca_file: "./tls/server-ca.crt"
client_cert_file: "./tls/gateway-client.crt"
client_key_file: "./tls/gateway-client.key"
tools:
- interlock_name:
ip:
port:
switch_id:
username:
password:
enabled:Example with HTTP and HTTPS tools:
bind: "0.0.0.0"
defaults:
timeout_ms: 5000
safe_state_on_error: "off"
shelly_tls:
server_ca_file: "./tls/server-ca.crt"
client_cert_file: "./tls/gateway-client.crt"
client_key_file: "./tls/gateway-client.key"
tools:
- interlock_name: "EQU-EXAMPLE-TOOL-01"
ip: "interlock-01.example.local"
protocol: "https"
port: 8081
switch_id: 0
username: "admin"
password: "example-password"
enabled: true
- interlock_name: "EQU-EXAMPLE-TOOL-02"
ip: "interlock-02.example.local"
protocol: "http"
port: 8082
switch_id: 0
username: null
password: null
enabled: trueExisting configurations that omit tools[].protocol continue to use http. Existing configurations that omit defaults.shelly_tls continue to run for HTTP-only tools.
When any TLS path is populated, the gateway initializes its TLS client from those files during startup. When an enabled tool uses https, all three TLS path fields are required.
| Field | Purpose |
|---|---|
bind |
Address used by FBS-facing listeners. Use 0.0.0.0 to listen on all interfaces. |
defaults.timeout_ms |
Timeout applied to each Shelly request attempt. The generated starter uses 5000 milliseconds; an omitted value defaults to 3000 milliseconds at runtime. A retried status operation can use a second attempt plus the 150-millisecond retry delay. |
defaults.safe_state_on_error |
State reported when an interlock cannot be reached. Usually off. |
defaults.shelly_tls.server_ca_file |
CA certificate used to verify HTTPS Shelly server certificates. |
defaults.shelly_tls.client_cert_file |
Gateway client certificate presented to HTTPS Shellies. |
defaults.shelly_tls.client_key_file |
Private key matching the gateway client certificate. |
tools[].interlock_name |
Tool or interlock name used in logs and the Admin UI. |
tools[].ip |
Shelly or network-interlock hostname/IP without a URL scheme, path, or port. |
tools[].protocol |
Shelly RPC scheme: http or https. Omitted values default to http. |
tools[].port |
FBS-facing gateway listener port. |
tools[].switch_id |
Shelly relay ID. This is commonly 0 for a single-output device. |
tools[].username |
Optional Shelly RPC username. |
tools[].password |
Optional Shelly RPC password used for Digest Authentication. |
tools[].enabled |
Whether the gateway starts the listener and contacts the device. |
Validation includes:
- required, nonblank interlock names
- required device addresses
- rejection of URL schemes in
tools[].ip httporhttpsprotocol normalization and validation- listener ports within
8081through8981 - duplicate listener ports
- valid nonnegative switch IDs
- required TLS file paths when an applicable tool uses HTTPS
- valid defaults
Invalid configuration is not written. Missing or unreadable configured TLS files also prevent the gateway from starting.
The gateway deep-clones configuration when it is accepted and whenever a snapshot is returned. The clone includes the tools backing array and the optional username/password string values. Callers therefore cannot mutate the gateway's internal configuration by editing a returned snapshot or by retaining references to a configuration supplied to New or UpdateConfig.
The module declares Go 1.26 in go.mod. Staticcheck is recorded as a Go tool dependency, so the repository uses the pinned module version through go tool staticcheck rather than relying on an arbitrary globally installed Staticcheck binary.
Create a local configuration:
make init-configGenerate local TLS material when the config contains the starter TLS paths:
make ca
make gateway-certCommon targets:
make fmt
make test
make test-race
make vet
make staticcheck
make shellcheck
make verifymake verify runs:
gofmtverification without modifying source filesgo.modandgo.sumconsistency checksgo vet ./...go tool staticcheck ./...- all Go tests under the race detector, including Admin/FBS preemption, incremental fleet refresh, independent configuration-copy, shared-status, Digest, TLS, retry/recovery, and signed-update authorization tests
- Bash syntax checks for top-level helpers, TLS helpers, and Linux/macOS deployment scripts
- ShellCheck across
scripts/andservices/shell source/templates - PowerShell parser validation for the Windows installer, updater, and uninstaller when
pwshis available - a platform-independent check for ambiguous PowerShell variable interpolation before a colon
- validation of both macOS LaunchDaemon property lists with
plutilor Python'splistlib - Linux AMD64 build validation
- Linux ARM64 build validation
- Windows AMD64 build validation
- macOS ARM64 build validation
- macOS AMD64 build validation
Individual validation targets:
make fmt-check
make tidy-check
make vet
make staticcheck
make test-race
make scripts-check
make shellcheck
make build-checkTLS helper targets:
make ca
make gateway-cert
make shelly-certDeployment package builds additionally require the PDF toolchain described below because the platform Markdown guides are rendered into the build directories.
The private pki/ tree is ignored by Git. Do not commit generated CA material, gateway or Shelly private keys, production configuration, or release-signing secrets.
Every Linux, Windows, and macOS deployment build requires three runtime identity/trust files, sourced directly from the canonical PKI tree:
pki/
├── ca/
│ └── server-ca.crt
└── gateway/
├── gateway-client.crt
└── gateway-client.key
Generate them with:
make ca
make gateway-certThe Makefile verifies those source files and copies only the required runtime material into the platform deployment directory's tls/ subdirectory. There is no repository-root staging tls/ directory.
The complete pki/ directory, CA private keys, CSRs, client-ca.crt, and per-device Shelly private keys remain excluded from deployment packages. The copied gateway private key uses mode 0600 in generated build output; certificates use 0644. Platform installers then apply the native installed permissions required by their service accounts.
Deployment builds render every matching Markdown guide from docs/deployment guides/ into a PDF in the corresponding platform build directory.
Default guide patterns:
LINUX_DEPLOYMENT_GUIDE_PATTERN = Linux*.md
WINDOWS_DEPLOYMENT_GUIDE_PATTERN = Windows*.md
MACOS_DEPLOYMENT_GUIDE_PATTERN = macOS*.mdRequired tools and defaults:
PANDOC = pandoc
PDF_ENGINE = xelatex
PDF_MARGIN = 0.3in
PDF_FONT_SIZE = 12pt
PDF_MAIN_FONT = IBMPlexMono-Regular
PDF_MONO_FONT = IBMPlexMono-Regular
PANDOC_INLINE_CODE_FILTER = scripts/pandoc/wrap-inline-code.luaInstall Pandoc, XeLaTeX, the configured fonts, and the LaTeX support needed by fvextra before building deployment packages. The Lua filter wraps long inline-code spans, and fvextra enables line breaking in verbatim/code blocks so generated deployment PDFs do not run past the page width. Override a pattern or PDF variable at invocation time when needed, for example:
make build-linux-amd64 \
LINUX_DEPLOYMENT_GUIDE_PATTERN='*.md' \
PDF_MAIN_FONT='IBM Plex Mono'The build fails when no guide matches the selected pattern.
make build-linux-amd64
make build-linux-arm64Both commands generate build/linux/. Run only the architecture target needed for the deployment machine after make clean.
The normal install.sh installs and enables managed automatic updates. install-dev.sh disables existing updater units and installs the current build without managed release replacement. Both apply the same service-account, TLS, firewall, and configuration protections. The package also includes uninstall.sh.
make build-windows-amd64The Windows package includes production and development launchers, the PowerShell installer, startup supervisor, authenticated signed-checksum updater, uninstaller, runtime TLS files, and rendered guide PDFs.
make build-darwin-arm64make build and make build-mac remain aliases for the Apple Silicon deployment build.
make build-darwin-amd64Both macOS packages include production and development installers, the startup wrapper, gateway and update LaunchDaemon property lists, authenticated signed-checksum updater, Packet Filter anchor, uninstaller, runtime TLS files, and rendered guide PDFs.
make windows-deployment-files
make macos-arm64-deployment-files
make macos-amd64-deployment-files
make macos-deployment-filesThese targets render scripts and service definitions but do not build the executable or copy runtime TLS material.
make linux-deployment-guides
make windows-deployment-guides
make macos-arm64-deployment-guides
make macos-amd64-deployment-guidesGenerated deployment files are build artifacts. Edit source templates, Markdown guides, or Makefile variables instead of editing generated copies.
build/linux/
├── fbs-interlock-gateway
├── config.yaml
├── fbs-interlock-gateway.service
├── install.sh
├── install-dev.sh
├── uninstall.sh
├── update.sh
├── fbs-interlock-gateway-update.service
├── fbs-interlock-gateway-update.timer
├── tls/
│ ├── server-ca.crt
│ ├── gateway-client.crt
│ └── gateway-client.key
└── Linux Install Instructions.pdf
build/windows/
├── fbs-interlock-gateway.exe
├── config.yaml
├── install.bat
├── install-dev.bat
├── install.ps1
├── start.bat
├── update.bat
├── update.ps1
├── uninstall.bat
├── uninstall.ps1
├── tls/
│ ├── server-ca.crt
│ ├── gateway-client.crt
│ └── gateway-client.key
└── Windows Install Instructions.pdf
build/darwin/arm64/
├── fbs-interlock-gateway
├── config.yaml
├── install.sh
├── install-dev.sh
├── start.sh
├── uninstall.sh
├── update.sh
├── com.williamveith.fbs-interlock-gateway.plist
├── com.williamveith.fbs-interlock-gateway-update.plist
├── com.williamveith.fbs-interlock-gateway.pf
├── tls/
│ ├── server-ca.crt
│ ├── gateway-client.crt
│ └── gateway-client.key
└── macOS Install Instructions.pdf
build/darwin/amd64/
├── fbs-interlock-gateway
├── config.yaml
├── install.sh
├── install-dev.sh
├── start.sh
├── uninstall.sh
├── update.sh
├── com.williamveith.fbs-interlock-gateway.plist
├── com.williamveith.fbs-interlock-gateway-update.plist
├── com.williamveith.fbs-interlock-gateway.pf
├── tls/
│ ├── server-ca.crt
│ ├── gateway-client.crt
│ └── gateway-client.key
└── macOS Install Instructions.pdf
The exact PDF filenames follow the matching source Markdown basenames. Generated deployment files are build artifacts; edit their templates, guides, or Makefile variables and rebuild.
Build all local release binaries and SHA-256 checksum files through the validation gate:
make release VERSION=<version>Build individual assets:
make release-linux-amd64 VERSION=<version>
make release-linux-arm64 VERSION=<version>
make release-windows-amd64 VERSION=<version>
make release-darwin-arm64 VERSION=<version>
make release-darwin-amd64 VERSION=<version>The local make release output contains executables and their .sha256 files. The GitHub release workflow later adds detached Ed25519 .sha256.sig files after the trusted build and SLSA provenance have been verified. Generated TLS trust, certificates, CA material, and private keys are never GitHub release assets.
Local release files are written to:
build/release/
├── fbs-interlock-gateway-linux-amd64
├── fbs-interlock-gateway-linux-amd64.sha256
├── fbs-interlock-gateway-linux-arm64
├── fbs-interlock-gateway-linux-arm64.sha256
├── fbs-interlock-gateway-windows-amd64.exe
├── fbs-interlock-gateway-windows-amd64.exe.sha256
├── fbs-interlock-gateway-darwin-arm64
├── fbs-interlock-gateway-darwin-arm64.sha256
├── fbs-interlock-gateway-darwin-amd64
└── fbs-interlock-gateway-darwin-amd64.sha256
A published production release contains the five executables, five checksum files, and five detached signatures:
<asset>
<asset>.sha256
<asset>.sha256.sig
The signature is a raw Ed25519 detached signature over the exact bytes of the corresponding .sha256 file. Updaters authenticate the checksum with the public key embedded in the currently installed gateway before the checksum is used to authorize a candidate binary.
Display embedded metadata:
./build/release/fbs-interlock-gateway-linux-amd64 -versionOutput format:
fbs-interlock-gateway version=<version> commit=<commit> date=<UTC-build-time>
The executable also provides the internal updater-verification command:
fbs-interlock-gateway verify-update-checksum \
--checksum <asset>.sha256 \
--signature <asset>.sha256.sig \
--asset <asset>On success, the command prints only the authenticated lowercase SHA-256 digest and exits with status 0.
services/linux/
├── app.service.in
├── install-linux.sh.in
├── install-linux-dev.sh.in
├── uninstall-linux.sh.in
├── update-linux.sh.in
├── update.service.in
└── update.timer.in
Installed layout:
/opt/fbs-interlock-gateway/
├── fbs-interlock-gateway
├── uninstall.sh
└── update.sh # production mode
/etc/fbs-interlock-gateway/
├── config.yaml
├── config.yaml.bak
└── tls/
├── server-ca.crt
├── gateway-client.crt
└── gateway-client.key
/etc/systemd/system/
├── fbs-interlock-gateway.service
├── fbs-interlock-gateway-update.service
└── fbs-interlock-gateway-update.timer
The production Linux installer:
- verifies or installs
lsof,curl,ca-certificates, andufw - configures UFW default-deny inbound behavior and the authorized FBS source/range rule
- verifies all three packaged runtime TLS files
- creates the service user and group when needed
- installs the executable, service files, uninstaller, and updater
- preserves an existing production config and installed TLS identity
- sets installed TLS files to
root:<service-group>with mode0640 - verifies that the service account can read the config and TLS files
- enables and starts the gateway and update timer
The development Linux installer applies the same dependency, firewall, binary, config, TLS, and service setup, but disables and removes managed updater units so a local build is not replaced.
The systemd service runs from /etc/fbs-interlock-gateway, writes to journald, restarts after exits with bounded rapid-restart behavior, and applies NoNewPrivileges=true.
The installed uninstaller removes the executable, updater, systemd units, and gateway-specific UFW rule. Standard uninstall preserves /etc/fbs-interlock-gateway/config.yaml, the installed tls/ directory, and the service account. --purge removes the complete configuration directory while still preserving the service account and normal journal history.
services/windows/
├── install.bat.in
├── install-dev.bat.in
├── install.ps1.in
├── start.bat.in
├── update.bat.in
├── update.ps1.in
├── uninstall.bat.in
└── uninstall.ps1.in
Installed layout:
C:\FBS\fbs-interlock-gateway\
├── fbs-interlock-gateway.exe
├── config.yaml
├── start.bat
├── update.bat # production mode only
├── update.ps1 # production mode only
├── tls\
│ ├── server-ca.crt
│ ├── gateway-client.crt
│ └── gateway-client.key
└── logs\
├── gateway.log
├── gateway-error.log
├── update.log
└── update-error.log
The Windows installer:
- elevates through User Account Control and validates a runnable amd64 PE binary
- backs up existing executable files and Task Scheduler definitions for rollback
- preserves an existing production config and installed TLS identity
- applies restricted NTFS access to Administrators,
SYSTEM, and the runtime account - runs the gateway task as
NT AUTHORITY\LOCAL SERVICE - runs the production update task as
SYSTEM - uses
start.batas a two-second restart supervisor with bounded rapid-restart behavior - sets default inbound firewall behavior to block and installs a source-, port-, and executable-restricted allow rule
- starts the gateway and validates the Admin API
- installs the hourly updater only in production mode
Development installation removes the managed update task and scripts while preserving the normal gateway task, config, TLS files, permissions, and firewall controls.
The standard uninstaller removes tasks, running processes, application and updater files, executable backups, and gateway firewall rules while preserving config.yaml, the installed tls\ directory, and logs\. Purge mode also removes the preserved configuration, TLS files, and logs.
services/macos/
├── com.williamveith.fbs-interlock-gateway.plist.in
├── com.williamveith.fbs-interlock-gateway-update.plist.in
├── fbs-interlock-gateway.pf.in
├── install-macos.sh.in
├── install-macos-dev.sh.in
├── start.sh.in
├── update-macos.sh.in
└── uninstall-macos.sh.in
Installed layout:
/usr/local/libexec/fbs-interlock-gateway/
├── fbs-interlock-gateway
├── start.sh
└── update.sh # production mode only
/Library/Application Support/fbs-interlock-gateway/
├── config.yaml
└── tls/
├── server-ca.crt
├── gateway-client.crt
└── gateway-client.key
/Library/LaunchDaemons/
├── com.williamveith.fbs-interlock-gateway.plist
└── com.williamveith.fbs-interlock-gateway-update.plist # production mode only
/etc/pf.anchors/com.williamveith.fbs-interlock-gateway
/Library/Logs/fbs-interlock-gateway/
├── gateway.log
├── gateway-error.log
├── update.log
└── update-error.log
The macOS installer:
- validates the operating system, current architecture, packaged Mach-O executable, scripts, property lists, Packet Filter anchor, and TLS files before replacement
- creates the hidden non-login
_fbs-gatewayaccount and group when needed - preserves the active production config and installed TLS identity
- installs config and TLS files with service-account-readable ownership and mode
0640 - installs the main LaunchDaemon and the hourly update LaunchDaemon in production mode
- registers the executable with the Application Firewall
- installs and validates a managed
pfanchor and managed block in/etc/pf.conf - creates separate gateway and updater stdout/stderr logs
- performs Admin API health validation
- rolls back the executable, wrappers, plists, Packet Filter files, and prior service state when installation fails after replacement
Development installation preserves all production security controls but removes and disables the updater and update LaunchDaemon.
Standard uninstall removes executables, LaunchDaemons, updater, Packet Filter anchor and managed pf.conf block while preserving configuration, TLS data, logs, and the service account. Purge mode removes persistent configuration and logs as documented in the macOS deployment guide.
Detailed procedures are maintained in:
docs/deployment guides/Linux Install Instructions.md
docs/deployment guides/Windows Install Instructions.md
docs/deployment guides/macOS Install Instructions.md
Deployment packages contain rendered PDF copies of the matching guides.
Production installers enable managed hourly release checks. Development installers keep the locally built gateway and disable managed release replacement. All production updaters modify only the application executable and, where applicable, gateway log archives; they do not replace config.yaml or installed TLS files.
The update protocol is deliberately lightweight. Every normal hourly check downloads only the architecture-specific .sha256 file and its 64-byte Ed25519 detached signature. The multi-megabyte executable is downloaded only when the authenticated SHA differs from the installed executable.
hourly check
|
+-> download <asset>.sha256
+-> download <asset>.sha256.sig
|
v
CURRENT installed gateway
verify-update-checksum
|
+-> invalid signature / asset / SHA -> fail closed
|
v
compare installed SHA
|
+-> same -> no binary download
|
v
download candidate
|
+-> SHA must match authenticated checksum
+-> candidate version must be stable SemVer
+-> authenticated downgrade is rejected
|
v
backup -> install/stage -> verify installed SHA
|
+-> restart/health failure -> rollback
The updaters do not require GitHub CLI, Cosign, GPG, OpenSSL, or another signature-verification package on deployed hosts. Ed25519 verification is performed by the already-installed gateway executable using Go's standard-library implementation and the embedded update-trust public key.
Because the current executable is the trust anchor, the signed-update updater requires an existing trusted gateway binary that implements verify-update-checksum. A fresh installation or the documented one-time bootstrap is required before switching an older deployment to the hardened updater.
Equal semantic versions are permitted when their bytes differ, allowing an authenticated release to repair a locally modified or corrupted binary. Lower semantic versions are rejected automatically.
The generated systemd timer runs after boot and then periodically.
The Linux updater:
- selects the matching Linux AMD64 or ARM64 release asset
- downloads only the matching
.sha256and.sha256.sigmetadata for the normal hourly check - requires HTTPS/TLS 1.2 or newer for GitHub release downloads
- invokes the currently installed gateway to authenticate the checksum signature and asset name
- hashes the installed executable and exits without downloading or restarting when it already matches
- downloads the candidate binary only when the authenticated SHA differs
- verifies the candidate SHA against the authenticated checksum
- reads current and candidate
-versionmetadata and rejects stable-SemVer downgrades - backs up the installed executable
- installs and re-hashes the final executable
- restarts the service only when required
- rolls back when the installed checksum is wrong or the service fails to start
Inspect or disable the timer:
sudo systemctl status fbs-interlock-gateway-update.timer
sudo systemctl list-timers fbs-interlock-gateway-update.timer
sudo systemctl disable --now fbs-interlock-gateway-update.timerRun an update manually:
sudo /opt/fbs-interlock-gateway/update.shProduction installation creates the FBS Interlock Gateway Update Task Scheduler task. It begins at minute 17 and repeats once per hour under SYSTEM.
Each run:
- acquires an update lock and cleans a stale lock after the configured age
- downloads the Windows AMD64
.sha256and 64-byte.sha256.sig - forces TLS 1.2 for Windows PowerShell 5.1 GitHub downloads
- uses the currently installed
fbs-interlock-gateway.exeto authenticate the checksum - compares the authenticated SHA with
Get-FileHashof the installed executable - skips the executable download when the SHA already matches
- verifies the candidate SHA, Windows PE format, and AMD64 machine type
- reads current and candidate version metadata and rejects authenticated downgrades
- preserves the existing executable ACL, creates a timestamped backup, and stages the replacement
- verifies the staged and final installed executable hashes
- restarts the gateway task and waits for the Admin API health check
- restores the previous executable and ACL when installation or health validation fails
- rotates
gateway.logandgateway-error.logwhen either reaches 10 MiB
Windows retains up to 30 numbered ZIP archives per gateway log.
Run the update task manually:
Start-ScheduledTask -TaskName "FBS Interlock Gateway Update"Production installation creates system/com.williamveith.fbs-interlock-gateway-update. The LaunchDaemon runs at minute 17 of every hour with low-priority I/O.
Each run:
- acquires an update lock
- detects Apple Silicon or Intel architecture
- downloads the matching
.sha256and 64-byte.sha256.sig - requires HTTPS/TLS 1.2 or newer for GitHub release downloads
- uses the currently installed gateway to authenticate the checksum
- hashes the installed binary and skips the candidate download when it already matches and no log maintenance is required
- verifies the candidate SHA and Mach-O architecture
- reads current and candidate version metadata and rejects authenticated downgrades
- creates a timestamped backup and stages the replacement
- verifies the staged and final installed binary hashes
- restarts the gateway only when needed and waits for the Admin API health check
- restores the previous binary when installation or health validation fails
- rotates
gateway.logandgateway-error.logwhen either reaches 10 MiB
macOS retains up to 30 numbered gzip archives per gateway log.
Run maintenance manually:
sudo /usr/local/libexec/fbs-interlock-gateway/update.shUse the packaged development installer on any platform when testing an unpublished local build, then run the normal production installer to restore managed updates.
The CI workflow runs for pull requests and pushes to main.
It:
- checks out the repository with
actions/checkoutpinned to a full immutable commit SHA - installs the Go version declared in
go.modwithactions/setup-gopinned to a full commit SHA - runs the unified
make verifygate, including Staticcheck and ShellCheck - executes the Linux AMD64 binary with
-version - verifies the generated formats for:
- Linux AMD64
- Linux ARM64
- Windows AMD64
- macOS ARM64
- macOS AMD64
The macOS and Windows binaries are cross-compiled and format-checked on the Linux runner. They are not executed by that runner.
GitHub Actions dependencies are pinned to full commit SHAs rather than mutable major-version tags. Dependabot is configured to check:
- Go modules every Monday at 06:00 America/Chicago, grouping minor/patch updates
- GitHub Actions every Monday at 06:15 America/Chicago, grouping minor/patch updates
Dependabot pull requests still pass through the normal repository validation/review path.
Releases are created through the manually triggered Validate, Attest, Sign, Tag, and Release GitHub Actions workflow. The caller uses least-privilege job permissions and delegates build/provenance generation to the separately maintained reusable workflow in williamveith/fbs-interlock-gateway-builder, pinned to an exact full commit SHA.
The workflow is split into three trust stages.
The preflight job:
- requires
refs/heads/main - checks out complete history without persisting checkout credentials
- validates the requested release version
- rejects a tag that already exists
The caller invokes the pinned reusable trusted builder with only the permissions required for artifact/provenance work. The trusted builder performs the full repository validation and release build, validates the expected assets and embedded metadata, and generates SLSA v1 provenance for the release binaries and checksum files using GitHub-hosted infrastructure and GitHub OIDC/Sigstore.
The build/provenance stage does not receive the long-lived GPG release-signing key or Ed25519 update-signing private key.
The protected release environment publish job:
- downloads the exact artifacts produced by the trusted builder
- re-verifies every SHA-256 checksum
- verifies SLSA provenance for every binary and checksum subject
- requires the expected repository, exact source commit,
refs/heads/main, exact reusable-builder workflow and builder commit, SLSA v1 predicate, and GitHub-hosted runner - loads the Ed25519 update-signing private key from the protected release environment
- signs each authenticated
.sha256file and verifies every generated 64-byte signature - executes the built Linux AMD64 binary's
verify-update-checksumcommand to prove that the embedded public key matches the configured private signing key - imports the protected GPG key only after build/provenance/update-authorization checks have succeeded
- creates and locally verifies a GPG-signed annotated tag at the exact source commit
- pushes the signed tag
- creates the GitHub Release with all five binaries, five checksum files, and five detached Ed25519 signatures
- waits for and verifies the immutable GitHub release attestation
- verifies every published release asset against the immutable-release attestation
Required release environment secrets:
GPG_PRIVATE_KEY
GPG_PASSPHRASE
UPDATE_SIGNING_PRIVATE_KEY
Required release environment variables used by GPG import/tagging:
GPG_FINGERPRINT
GIT_COMMITTER_NAME
GIT_COMMITTER_EMAIL
The update-signing private key must correspond exactly to the public key committed at:
internal/updateauth/update-signing-public.pem
The release workflow fails before publication when those keys do not match.
Start the workflow with GitHub CLI:
gh workflow run release.yml \
--ref main \
-f version=<version>After it succeeds, verify the signed Git tag and immutable release:
git fetch origin --tags
git tag -v <version>
gh release verify <version> \
--repo williamveith/fbs-interlock-gatewayVerify a downloaded release asset against the immutable release attestation:
gh release verify-asset <version> \
<asset> \
--repo williamveith/fbs-interlock-gatewayFor a stronger build-origin check, verify SLSA provenance with the exact trusted-builder workflow revision and source commit recorded for that release:
gh attestation verify <asset> \
--repo williamveith/fbs-interlock-gateway \
--signer-workflow williamveith/fbs-interlock-gateway-builder/.github/workflows/build-release.yml \
--signer-digest <trusted-builder-commit-sha> \
--source-ref refs/heads/main \
--source-digest <release-source-commit-sha> \
--predicate-type https://slsa.dev/provenance/v1 \
--deny-self-hosted-runnersThis release architecture is designed around a separated trusted-builder/provenance boundary; it should be described as a SLSA-hardened or SLSA Build Level 3-oriented design rather than as an external certification.
Start from current main:
git switch main
git pull --ff-only origin mainCreate a short-lived branch:
git switch -c feature/<description>Validate before committing:
make fmt
make verify
git status
git diffCommit and push:
git add -A
git commit -S -m "Describe the change"
git push --set-upstream origin feature/<description>Open a pull request:
gh pr create \
--base main \
--head feature/<description> \
--fillAfter merging:
git switch main
git pull --ff-only origin main
git fetch --prune
git branch -d feature/<description>Create a local config:
make init-configIf the generated TLS paths remain configured, generate the local certificate material:
make ca
make gateway-certRun through Make:
make runEquivalent command:
go run ./cmd/fbs-interlock-gateway \
-config ./config.yamlRun with an explicit Admin address:
go run ./cmd/fbs-interlock-gateway \
-config ./config.yaml \
-admin 127.0.0.1:18090Print build metadata:
go run ./cmd/fbs-interlock-gateway -versionRead the Admin cache without contacting Shellies:
curl -i "http://127.0.0.1:18090/api/status"Start an explicit fleet refresh:
curl -i "http://127.0.0.1:18090/api/status?refresh=1"The X-Status-Refresh-In-Progress header reports whether the background scan is active.
Read configuration:
curl -s "http://127.0.0.1:18090/api/config"Test an HTTP Shelly directly:
curl "http://<shelly-host>/rpc/Switch.GetStatus?id=<switch-id>"
curl "http://<shelly-host>/rpc/Switch.Set?id=<switch-id>&on=true"
curl "http://<shelly-host>/rpc/Switch.Set?id=<switch-id>&on=false"Test Digest Authentication over HTTP:
curl --anyauth -u "admin:<password>" \
"http://<shelly-host>/rpc/Switch.GetStatus?id=<switch-id>"Test mutual TLS directly from the development machine:
curl \
--cacert ./tls/server-ca.crt \
--cert ./tls/gateway-client.crt \
--key ./tls/gateway-client.key \
"https://<shelly-ddns-host>/rpc/Switch.GetStatus?id=<switch-id>"Add --anyauth -u "admin:<password>" when the HTTPS Shelly also requires Digest Authentication.
Test through the gateway:
curl "http://<gateway-host>:<port>/status"
curl "http://<gateway-host>:<port>/on"
curl "http://<gateway-host>:<port>/off"On startup, the gateway:
- parses
-config,-admin, and-version - resolves and loads configuration from the explicit path or beside the executable
- resolves relative TLS file paths against the configuration directory
- applies defaults and deep-clones the accepted configuration
- initializes the shared status store with safe-output placeholders
- loads the configured Shelly server CA and gateway client identity when TLS is configured
- validates enabled tools, protocols, listener ports, and required TLS fields
- starts the Admin server unless disabled
- starts one FBS-facing HTTP listener per enabled tool
- maps each gateway port to one configured interlock
- logs inbound FBS requests and outbound responses
- uses HTTP or HTTPS, optional mutual TLS, and optional Digest Authentication per tool
- records every completed Shelly result in the shared Admin status store
- reports the configured safe state and records the error when a device request fails
- shuts down cleanly on interrupt, termination, Admin restart request, or server error
During operation:
- ordinary Admin status polling reads memory only
- an explicit Admin refresh queries enabled tools with up to 32 workers
- refresh results are published independently as each device completes
- completed partial refresh results remain stored when the overall scan reaches its context deadline
- FBS requests passively update only the affected tool row
- FBS status and set requests take priority over Admin status probes for the same Shelly
- an Admin probe never queues behind another device request and can be canceled by arriving FBS traffic
- Admin deferral preserves the current row while the FBS operation supplies the authoritative update
- per-device revisions prevent stale scan results from replacing newer FBS results
- requests to one Shelly are serialized and may reuse an idle connection, TLS session, and Digest session
- transient status failures may receive one retry
- persistent Shelly HTTP
423or429conditions may schedule a cooldown-protected reboot request - configuration snapshots are independent deep copies rather than aliases of gateway-owned slices or credential pointers
Disabled tools do not receive FBS listeners and are not contacted by explicit status refreshes.
Before starting an enabled listener, the gateway may clear a process already using that configured port. Use dedicated gateway ports and confirm that unrelated services do not use the configured range.
A successful Admin UI save writes the updated configuration and requests a process restart. The installed platform supervisor rebuilds runtime listeners, the shared status store, the Shelly transport, TLS trust, Digest state, and per-device scheduling state by starting the process again.
Gateway FBS request logs use:
FBS_IN
FBS_OUT
Shelly-client operational logs may include:
shelly_status_retry
shelly_authentication_throttled
shelly_reboot_scheduled
shelly_reboot_suppressed
shelly_reboot_requested
shelly_reboot_failed
Network errors include the observed request phase and elapsed time, for example:
phase=dns_lookup
phase=tcp_connect
phase=tls_handshake
phase=response_headers
phase=response_body
They also report whether the HTTP connection was reused and whether the TLS session resumed. FBS status and set failures are logged before the configured safe state is returned.
Platform logs:
Linux gateway:
sudo journalctl -u fbs-interlock-gateway.service -f
Linux updater:
sudo journalctl -u fbs-interlock-gateway-update.service
Windows gateway stdout:
C:\FBS\fbs-interlock-gateway\logs\gateway.log
Windows gateway stderr:
C:\FBS\fbs-interlock-gateway\logs\gateway-error.log
Windows updater stdout/stderr:
C:\FBS\fbs-interlock-gateway\logs\update.log
C:\FBS\fbs-interlock-gateway\logs\update-error.log
macOS gateway stdout/stderr:
/Library/Logs/fbs-interlock-gateway/gateway.log
/Library/Logs/fbs-interlock-gateway/gateway-error.log
macOS updater stdout/stderr:
/Library/Logs/fbs-interlock-gateway/update.log
/Library/Logs/fbs-interlock-gateway/update-error.log
The Windows and macOS production updaters rotate gateway.log and gateway-error.log at 10 MiB and retain up to 30 compressed archives per file. Linux runtime retention remains controlled by the host's journald configuration.
Ignored local artifacts:
.DS_Store
build
pki
config.yaml
config.yaml.bak
*.patchpki/ contains private CA material, gateway certificate requests/identity material, and per-device Shelly keys. build/ contains generated binaries, deployment scripts, service definitions, copied runtime TLS material, generated update files, and deployment-guide PDFs. Patch files are ignored so local review or transfer patches are not committed accidentally.
There is no repository-root generated tls/ staging directory. Platform deployment builds copy the required runtime trust/identity files directly from pki/ca/ and pki/gateway/ into their generated build/.../tls/ directories.
Committed security material is limited to non-secret trust/configuration source such as certificate templates and internal/updateauth/update-signing-public.pem. The Ed25519 update-signing private key, GPG private key, CA private keys, production configuration, active credentials, generated certificates/CSRs, and production mappings must remain outside version control.
SECURITY.md directs vulnerability reports to GitHub Private Vulnerability Reporting and defines operational-safety restrictions for testing against a system that can affect physical interlocks.
This project is available under the FBS Interlock Gateway Public Benefit Source License 1.0.
Eligible individuals and organizations may use the project without charge. Current and former employers of the Licensor require a separate written license.
See Commercial and Employer Licensing for details.