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

Don't expose this to the internet

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:

  1. The URL must parse as absolute http or https — never file:, gopher:, or anything else.
  2. It must have a non-empty host.
  3. Unless home_assistant.allow_remote_url is true, 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).
Worth knowing

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/status and GET /api/homeassistant/config report 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.