Security model
Goalfeed's threat model, stated plainly: it has no API authentication and is designed to live on a trusted home network behind Home Assistant, not on the open internet.
The short version
Neither the REST API nor the WebSocket feed require authentication, and CORS accepts requests from any origin. This is a deliberate trade-off for a single-user, home-network tool that talks to Home Assistant — not an oversight to be "fixed" by bolting on a login screen. Run it behind Home Assistant's own ingress, on a network you already trust, or behind a reverse proxy you control. Don't port-forward it directly to a public IP.
Why no auth, specifically
Every endpoint under /api and the /ws feed accept any
request from any origin (AllowOrigins: ["*"],
AllowCredentials: true on the REST side; the WebSocket upgrader's
CheckOrigin always returns true). Goalfeed's job is to sit
next to Home Assistant and talk to it, in the same trust boundary a Home Assistant
add-on already lives in — ingress is the access control layer, not something
Goalfeed reimplements. Two consequences follow from this, and both have a real
config key guarding them.
1. The Home Assistant URL is validated to stop token exfiltration
Goalfeed authenticates to Home Assistant with a long-lived access token, sent as a
bearer credential on every outbound request. That token is a real credential worth
protecting: home_assistant.url can be set through an unauthenticated
runtime endpoint (POST /api/homeassistant/config), an environment
variable, or a hand-edited config.yaml. If Goalfeed sent the token to
whatever host that value named without question, a poisoned or mistakenly-edited
config could redirect the token to an attacker-controlled server.
Every outbound Home Assistant request is validated first
(utils.ValidateHomeAssistantURL, called from
targets/homeassistant/guard.go immediately before any request goes out).
The rules:
- The URL must parse as absolute
httporhttps— neverfile:,gopher:, or anything else. - It must have a non-empty host.
- Unless
home_assistant.allow_remote_urlistrue, the host must resolve to something clearly local: a loopback or RFC 1918/RFC 4193 private IP literal, or a hostname that's obviously local (localhost,homeassistant,homeassistant.local,supervisor, anything ending in.local, or a bare single-label hostname with no dot).
Link-local addresses (169.254.0.0/16, fe80::/10)
are deliberately rejected even under the "private" rule — that range
is not treated as local, on purpose. It contains
169.254.169.254, the canonical cloud instance metadata endpoint used
by AWS/GCP/Azure to serve credentials to whatever is running on the host. If
Goalfeed ever runs on a cloud VM and a poisoned config pointed
home_assistant.url at that address, treating link-local as "safe
because private" would hand an attacker a path to the metadata service. Rejecting
it by default closes that off; there is no config key to re-enable link-local
specifically, only the broader allow_remote_url escape hatch below.
If your Home Assistant instance is genuinely remote (not on the same private
network as Goalfeed), set home_assistant.allow_remote_url: true
explicitly — this is an opt-in, not a default, and it relaxes only the
local-address check, not the scheme/host validation. See the exact rejection message
and next step on the troubleshooting page, and
every key's type/default on the configuration
reference.
2. The runtime config API no longer rewrites config.yaml by default
POST /api/homeassistant/config is unauthenticated, like every other
endpoint. Historically, posting to it could persist straight to
config.yaml on disk. Combined with no authentication, that meant anyone
who could reach the API at all could permanently rewrite the on-disk Home Assistant
URL and token — not just for the current process, but for every future restart.
By default now, that endpoint only updates the running process's in-memory
configuration for the current session; the response says so explicitly
("Configuration updated for this session only (not persisted to disk). Set
web.allow_config_writes: true in config.yaml to allow this API to write
config.yaml."). A restart reverts to whatever is actually on disk or in the
environment. Set web.allow_config_writes: true only if you specifically
want this unauthenticated endpoint to be able to persist changes — understanding
that doing so means anyone who can reach the API can rewrite your config file on
disk.
What's not covered here
- CORS is wide open (
AllowOrigins: ["*"], credentials allowed) on the REST API. This is consistent with "no auth, trusted network" — it is not a separate gap to file, but it does mean a malicious page open in a browser on the same network as Goalfeed could call its API. - The upstream league APIs are all unofficial and undocumented. Goalfeed has no control over their availability or shape; see Architecture for that risk, which is operational, not a security boundary.
GET /api/homeassistant/statusandGET /api/homeassistant/configreport whether a token is set, as a boolean (tokenSet), never the token value itself. The token is not disclosed by either endpoint.
Next
Every config key mentioned above, with its default and env var — configuration reference. The exact error text when a URL gets rejected — Troubleshooting.