Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,46 @@ matching a now-known channel and decrypts them. Watch the startup log for

## Configuration

### Admin authentication

The `/api/v1/admin` subtree requires `Authorization: Bearer <key>`. Set the
operator key with `BEACON_API_KEY` or `auth.api_key` in YAML. A set environment
variable overrides YAML; an explicitly empty value disables admin access.
With no key, admin requests return JSON 503 while public reads and WebSockets
continue normally. With a key, missing, incorrect or duplicate Authorization
headers return JSON 401 with `WWW-Authenticate: Bearer`.

`GET /api/v1/admin/config` returns selected running settings: CORS options with
Beacon defaults applied, `auth.configured`, and `ingest.broker_count` (configured
broker workers, not connection status or a tunable processing-worker pool).
The CORS lists are the options supplied to the middleware; its normal matching
normalization still applies. The response excludes
credential fields, broker addresses, channel material, database settings and
other configuration. Account operations are not implemented; unknown admin paths
return 404 and unsupported
methods on the config endpoint return 405 after authentication.
Global CORS preflights remain public. Use a long, randomly generated key, keep
it out of source control and logs, and send it only in the Authorization header,
never the URL or request body. Require HTTPS at the reverse proxy and restrict
direct access to Beacon's HTTP listener to that proxy or a private connection.
Changing the key requires a restart. No API key is issued automatically.

`PUT /api/v1/admin/config` accepts only
`{"cors":{"allowed_origins":["https://example.org"]}}`. It replaces the entire
origin list immediately and updates the reported configuration with the same
policy. Requests already in progress may use the previous policy. Concurrent
valid updates are serialized; updates take effect one at a time. The response
contains `config`, `persisted: false` and `requires_restart: false`.

Updates are **runtime-only**: no file or database is written, and restarting
reloads the saved configuration. Keep 1–32 ASCII HTTP(S) origins, at most 512 bytes
each, with an optional single hostname wildcard; a sole `*` permits all origins.
Empty/null lists, URL paths/queries/credentials, control characters and unknown
fields are rejected. Requests must be JSON, at most 16 KiB. Other CORS options,
auth/credential fields and broker count cannot be changed here; there is no
configurable `ingest.worker_count`. Cross-origin admin clients need PUT allowed
in the saved CORS methods. CORS controls browser access, not authentication.

### Environment variables (`.env`)

| Variable | Default | Description |
Expand Down
7 changes: 6 additions & 1 deletion cmd/beacon/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ var version = "dev"

// @schemes http https

// @securityDefinitions.apikey AdminKey
// @in header
// @name Authorization
// @description Enter Bearer followed by the configured operator key. Use HTTPS.

// @tag.name IATAs
// @tag.description Airport/location codes that group observers and packets
// @tag.name Regions
Expand Down Expand Up @@ -275,7 +280,7 @@ func main() {
go scheduler.Start(ctx)

// ── HTTP server ──────────────────────────────────────────────────────────
r := router.New(h, reader, []*ingest.Worker{broker1, broker2}, resolved.MaxConnsPerIP, cfg.CORS, cfg.Server)
r := router.New(h, reader, []*ingest.Worker{broker1, broker2}, resolved.MaxConnsPerIP, cfg.CORS, cfg.Server, cfg.Auth)

srv := &http.Server{
Addr: addr,
Expand Down
5 changes: 5 additions & 0 deletions config.yaml.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Beacon configuration file
# Copy to config.yaml and adjust as needed.

# Public reads stay available. No key means admin routes return 503.
# Prefer BEACON_API_KEY in the service environment; never commit a real key.
auth:
api_key: ""

server:
# Only these direct proxy peers may set the client IP through X-Real-IP.
# The proxy must overwrite that header, not pass through client input.
Expand Down
232 changes: 232 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,131 @@ const docTemplate = `{
"host": "{{.Host}}",
"basePath": "{{.BasePath}}",
"paths": {
"/admin/config": {
"get": {
"security": [
{
"AdminKey": []
}
],
"description": "Returns current CORS options, auth configuration status and configured broker count. Credential fields and other configuration are excluded. Runtime origin updates are lost on restart.",
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "Inspect selected running configuration",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.AdminConfig"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
}
}
},
"put": {
"security": [
{
"AdminKey": []
}
],
"description": "Replaces only cors.allowed_origins immediately. Updates are serialized; concurrent valid updates are applied one at a time. Already-running requests may finish with the previous policy. Nothing is persisted; restart reloads saved configuration.",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Admin"
],
"summary": "Update runtime CORS origins",
"parameters": [
{
"description": "1-32 ASCII HTTP(S) origins, at most 512 bytes each; one hostname wildcard is supported, or a sole *. Empty/null lists and unsupported fields are rejected. Body at most 16 KiB.",
"name": "config",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.UpdateAdminConfigRequest"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.UpdateAdminConfigResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
},
"413": {
"description": "Request Entity Too Large",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
},
"415": {
"description": "Unsupported Media Type",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
},
"503": {
"description": "Service Unavailable",
"schema": {
"type": "object",
"additionalProperties": {
"$ref": "#/definitions/internal_api_handlers.APIError"
}
}
}
}
}
},
"/brokers": {
"get": {
"produces": [
Expand Down Expand Up @@ -2390,6 +2515,66 @@ const docTemplate = `{
}
},
"definitions": {
"github_com_MeshCore-Beacon_beacon-server_internal_api.AdminAuthConfig": {
"type": "object",
"properties": {
"configured": {
"type": "boolean"
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.AdminCORSConfig": {
"type": "object",
"properties": {
"allow_credentials": {
"type": "boolean"
},
"allowed_headers": {
"type": "array",
"items": {
"type": "string"
}
},
"allowed_methods": {
"type": "array",
"items": {
"type": "string"
}
},
"allowed_origins": {
"type": "array",
"items": {
"type": "string"
}
},
"max_age": {
"type": "integer"
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.AdminConfig": {
"type": "object",
"properties": {
"auth": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.AdminAuthConfig"
},
"cors": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.AdminCORSConfig"
},
"ingest": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.AdminIngestConfig"
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.AdminIngestConfig": {
"type": "object",
"properties": {
"broker_count": {
"description": "BrokerCount counts configured broker workers, not active MQTT connections\nor a configurable processing-worker pool.",
"type": "integer"
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.AdvertObservation": {
"type": "object",
"properties": {
Expand Down Expand Up @@ -4044,6 +4229,45 @@ const docTemplate = `{
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.UpdateAdminCORSRequest": {
"type": "object",
"required": [
"allowed_origins"
],
"properties": {
"allowed_origins": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.UpdateAdminConfigRequest": {
"type": "object",
"required": [
"cors"
],
"properties": {
"cors": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.UpdateAdminCORSRequest"
}
}
},
"github_com_MeshCore-Beacon_beacon-server_internal_api.UpdateAdminConfigResponse": {
"type": "object",
"properties": {
"config": {
"$ref": "#/definitions/github_com_MeshCore-Beacon_beacon-server_internal_api.AdminConfig"
},
"persisted": {
"type": "boolean"
},
"requires_restart": {
"type": "boolean"
}
}
},
"internal_api_handlers.APIError": {
"type": "object",
"properties": {
Expand All @@ -4069,6 +4293,14 @@ const docTemplate = `{
}
}
},
"securityDefinitions": {
"AdminKey": {
"description": "Enter Bearer followed by the configured operator key. Use HTTPS.",
"type": "apiKey",
"name": "Authorization",
"in": "header"
}
},
"tags": [
{
"description": "Airport/location codes that group observers and packets",
Expand Down
Loading
Loading