Skip to main content

Browser clients

In front of the API, Andromeda runs a few web-facing layers. The defaults suit machine clients, which is how most context brokers are used. Change them when a browser application talks to the broker, or when a proxy in front already does the same job. None of these layers run on the separate management port.

LayerDefaultSwitched with
CORSAny origin, without credentialshttp.cors.mode
CSRF protectionOffhttp.csrf.mode
Security headersOnhttp.security_headers.mode
Response compressiongzip, Brotli, deflate and zstdhttp.compression.mode
Request decompressionThe same fourhttp.decompression.mode
Server-TimingThe total timehttp.server_timing.mode

Browser applications: CORS​

By default, http.cors.mode is "permissive". Any origin, method and header is allowed, without credentials, so a browser application on any origin can read the API. That is handy in development and too open for most production setups. "disabled" sends no CORS headers at all, so browsers on other origins can't read responses.

For production, name the origins you serve with http.cors.mode = "configured".

[http.cors]
mode = "configured"
allow_origins = ["https://dashboard.example.org"]
allow_methods = ["GET", "POST", "PATCH", "PUT", "DELETE"]
allow_headers = "mirror"
expose_headers = ["Link", "Location", "NGSILD-Results-Count"]
preflight_lifetime = "1h"

expose_headers lists the response headers a browser script may read. An NGSI-LD client usually needs Link for pagination and the @context, Location after a create, and NGSILD-Results-Count when it asks for a count. "mirror" allows whatever headers the browser asks for in its preflight.

If the application sends cookies or HTTP authentication, add credentials = "allowed". Browsers don't allow credentials together with a wildcard, so the broker refuses to start when credentials is "allowed" and any of allow_origins, allow_methods, allow_headers or expose_headers is "any". Use lists, or "mirror" for methods and headers.

"very-permissive" is for development only

It mirrors the request's origin, method and headers and allows credentials, which lets any site make credentialed requests to the broker.

Browser applications: CSRF​

CSRF protection is off by default. Turning it on doesn't affect machine clients, because they send neither an Origin nor a Sec-Fetch-Site header, and such requests always go through. Turn it on when a browser application with credentials talks to the broker.

[http.csrf]
mode = "enabled"
trusted_origins = ["https://dashboard.example.org"]

A request then goes through when any of these holds.

  • Its method is GET, HEAD or OPTIONS.
  • Its Origin is in trusted_origins.
  • Its Sec-Fetch-Site is same-origin or none.
  • It has neither Origin nor Sec-Fetch-Site.
  • Its Origin names the same host and port as the request.

Anything else gets 403. The check relies on Origin and Host arriving as the browser sent them, so a proxy in front must pass both on unchanged.

Security headers​

With http.security_headers.mode = "enabled", the default, every API response carries these headers.

Content-Security-Policy: default-src 'self'; base-uri 'self'; font-src 'self' https: data:; form-action 'self'; frame-ancestors 'self'; img-src 'self' data:; object-src 'none'; script-src 'self'; script-src-attr 'none'; style-src 'self' https: 'unsafe-inline'; upgrade-insecure-requests
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Resource-Policy: same-origin
Origin-Agent-Cluster: ?1
Referrer-Policy: no-referrer
X-Content-Type-Options: nosniff
X-DNS-Prefetch-Control: off
X-Download-Options: noopen
X-Frame-Options: sameorigin
X-Permitted-Cross-Domain-Policies: none
X-XSS-Protection: 0

Each header has a key of its own in [http.security_headers]. Change one, or set it to "disabled" to drop it, and the others stay as they are. Content-Security-Policy is a table of its own, because its value is free text.

[http.security_headers]
x_frame_options = "deny"
referrer_policy = "strict-origin-when-cross-origin"
x_xss_protection = "disabled"

[http.security_headers.content_security_policy]
policy = "default-src 'none'; frame-ancestors 'none'"

A policy that can't be sent as a header value, such as one holding a line break, stops the broker at startup. Strict-Transport-Security is off by default, and TLS covers it.

If your proxy sets security headers of its own, turn off the broker's copy so that a response doesn't carry both. Use the header's own key for one header, or http.security_headers.mode = "disabled" for all of them.

Compression​

Responses are compressed with whichever of gzip, Brotli, deflate and zstd the client accepts, and responses of 32 bytes or less go out as they are. Request bodies may arrive in any of the four codings. To change the set, list the codings you want. Each table has a mode that turns it off, and while it is on, its list must name at least one coding.

[http.compression]
algorithms = ["gzip", "zstd"]
quality = "fastest"

[http.decompression]
mode = "disabled"

quality trades CPU for size, with "fastest", "best", or "default" for each coding's usual balance. If your proxy already compresses responses, set http.compression.mode = "disabled" and save the CPU.

Two rules apply to a compressed request body.

  • It needs a Content-Length that gives its size as sent, compressed.
  • request_limits.max_body counts its size after decompression, so a small body that expands past the limit still gets 413.

A body in a coding that isn't in http.decompression.algorithms is refused, unless http.decompression.unaccepted_encoding = "allowed" hands it to the API still encoded.

Server-Timing​

Every response carries a Server-Timing header with the time the request took, named total. Add "dispatch" to http.server_timing.phases to also get the time until the request reached its handler. Browser developer tools show both next to each request. Set http.server_timing.mode = "disabled" if you'd rather not publish timings.

A response can carry several Link values, such as the @context link and the pagination links. http.link_field decides how they are spread over header lines.

ValueLayout
"combined", the defaultAll on one line, separated by ,
"grouped"One line for the @context link, one for the pagination links, one for any others
"separate"Every value on a line of its own

"combined" suits clients that read only one Link line, such as Go's Header.Get, and "separate" suits clients that don't split a line at commas. Every layout keeps the same order, the @context link first, then first, prev, next and last, then anything else. HTTP notifications follow the same setting. An MQTT notification always carries its Link as one metadata member.

Checking it works​

Ask for a response the way a browser on your application's origin would, and look at the headers.

curl -si -H 'Origin: https://dashboard.example.org' \
-H 'Accept-Encoding: gzip' \
-o /dev/null -D - http://localhost:1056/ngsi-ld/v1/types

With the CORS example above, the answer carries access-control-allow-origin: https://dashboard.example.org, and carries no such header for an origin you didn't list. The security headers appear unless you turned them off, and server-timing carries total. A response over 32 bytes also carries content-encoding: gzip.

If it goes wrong​

  • The broker doesn't start after a CORS change

    credentials = "allowed" can't be combined with "any". Replace "any" with lists, or with "mirror" for methods and headers.

  • A browser application gets 403 on writes

    CSRF protection is on, and either the application's origin isn't in http.csrf.trusted_origins, or a proxy rewrites Origin or Host.

  • A browser script can't read Link or Location

    Add them to http.cors.expose_headers.

  • Responses carry two Strict-Transport-Security or Content-Security-Policy headers

    Both the broker and the proxy set them. Turn one of them off, for the broker's copy with the header's own key in [http.security_headers].