Skip to content

Platform configuration and branding

This page describes how platform configuration is loaded and checked, and how branding is defined, validated, and delivered. Operator-facing configuration steps: sys-admin: configuration.

Configuration pipeline

flowchart LR
    V["values.yaml (sitesConfig)"] -->|"Helm render"| CM["ConfigMap sites-config"]
    CM -->|"mounted at container start"| L["sites-core loader"]
    E["environment variables SECTION_FIELD"] --> L
    L --> S{"schema validation + consistency checks"}
    S -- error --> X["startup stops, lists all problems"]
    S -- "warnings" --> C["SitesConfiguration singleton (Hub, operator, jobs)"]
  • The chart writes sitesConfig into the sites-config ConfigMap and injects three values itself with mergeOverwrite: namespace (= release namespace), site.version (= images.tag), and routing. These three always come from the chart: a site.version in sitesConfig is overridden, and a sitesConfig.routing key is rejected outright ("Do not set sitesConfig.routing. Use the top-level routing values.").
  • The loader (ConfigurationModelSchema.load() in core/src/ecmwf/sites/core/config/schema/models.py, instantiated by core/.../config/main.py) reads YAML files from the directory in CONFIG_FILESYSTEM_PATH — resolved at call time, not at import. In directory mode it merges *.yaml and *.yml files in sorted lexicographic order: the merge is shallow (top-level keys), so the last file wins for a section. A single file is loaded directly. An unset or missing path yields pure defaults.
  • Environment variables overlay the YAML. pydantic-settings maps names of the form SECTION_FIELD with nested delimiter _ and env_nested_max_split=1 (exactly one split), so HUB_MASTER_TOKEN sets hub.master_token. Environment values win over YAML values. List fields accept a comma-separated string (DecodeArrayMixin).
  • Validation is schema-first: a bad value stops startup with the field name in the error, before any other component reads it. The consistency rules (below) run as the last model validator, so every component runs the same checks on the same text.
  • The dev flag sets verify_tls to false for the Site API, Splunk, and OpenSearch clients, and selects unredacted logs (the secret-masking policy is documented with 09-operator-logging). It is for isolated development only; the flag does not change rendered workloads.
  • namespace must be non-empty. Everything else has a default; the shipped defaults point at example.org placeholders that the consistency rules detect when an integration is enabled.

The schema is core/.../config/schema/models.py. Sections: hub, branding, site, operator, jobs, cache, kubernetes, routing, migration, email, jira, session, oidc, directory, ldap, splunk, opensearch, integrations, plus the globals debug, dev, namespace (required), trusted_ips, trusted_domains.

Secret injection

Secret values never travel in the ConfigMap. The Hub and operator workloads add the two Secrets named by secrets.sitesSecrets.name (default sites-secrets) and secrets.oidcSecrets.name (default oidc-secrets) with envFrom: secretRef; the four job templates mount only the sites Secret. Because the expected keys are themselves SECTION_FIELD names — HUB_MASTER_TOKEN, JIRA_API_TOKEN, JIRA_API_USERNAME, LDAP_PASSWORD, SESSION_SECRET_KEY, SPLUNK_API_PASSWORD, OPENSEARCH_API_PASSWORD, and CACHE_REDIS_PASSWORD in the sites Secret, OIDC_CLIENT_ID and OIDC_CLIENT_SECRET in the OpenID Connect (OIDC) Secret — the schema picks them up through the same environment-variable path as any override.

session.secret_key is the shared platform key: cookies, the Hub session, the per-site OAuth2 Proxy cookie secrets (Secret <id>-oauth2, 10-security), and the Redis payload signature are derived from it (13-redis-cache), so every component must read the same Secret. The root schema validator validate_session_secret_key (models.py) rejects a key that is missing or shorter than SESSION_SECRET_KEY_MIN_BYTES (32 UTF-8 bytes), so a process that loads the configuration never signs with an empty or weak key.

Upgrade guards

The removed settings in the table below fail loudly with the name of their replacement, at the first layer that sees them. A helm upgrade fails at render time when the chart values still contain a removed key (helm/.../_helpers.tpl), and the schema rejects it when it reaches configuration (models.py). Other historical removals (for example the OIDC 2FA client settings and site.extra_options_file) are silently ignored. The implemented rejections:

Removed key Rejected by Replacement
sitesConfig.routing chart render top-level routing.* values
top-level ingress values chart render routing.host, routing.hsts, routing.tls, routing.ingress
routing.hstsEnabled / config routing.hsts_enabled chart render / schema routing.hsts
sitesConfig.kubernetes.ingress_class_name chart render / schema routing.ingress.className
sitesConfig.kubernetes.ingress_extra_annotations chart render / schema routing.ingress.routeAnnotations
secrets.imagePullSecretName chart render serviceAccount.imagePullSecretName
top-level hpa values chart render hub.hpa
sitesConfig.jobs.analytics_enabled chart render / schema integrations.analytics.providers and default
integrations.<feature>.provider or provider_legacy chart render / schema providers and default
ldap.default_users, ldap.validate_usernames, ldap.policies_list schema directory.*

Cross-field validators

These rules run inside the schema (a startup error) unless noted:

  • routing.mode must be ingress or gateway (chart render also checks); routing.host is required at chart render, and required again in the schema together with routing.gateway.name when the mode is gateway. routing.scheme must be http or https. Host and Gateway names are validated as DNS names.
  • routing.gateway.policies may only list features the configured policy_provider supports (14-policy-providers).
  • trusted_domains entries must be lowercase DNS domains (label ≤ 63, total ≤ 253); the chart repeats the check at render. The domains feed the Content Security Policy (CSP) frame-ancestors header and the CORS help (14).
  • migration.enabled requires migration.source_domain, and the source domain must not equal routing.host (a mirror of the platform onto itself stops startup). The domain must not include a scheme or path.
  • OIDC: setting any explicit endpoint requires oidc.issuer_url. When the six required endpoints are all set, discovery is skipped (10-oidc).
  • integrations.<feature>.providers must be non-empty and its default must be one of them.
  • operator.dependency_log_level must be DEBUG, INFO, WARNING, or ERROR (case-insensitive, normalised).

In-band settings editor

The Hub admin page /hub/admin/hub-settings/ edits the live sites-config ConfigMap (admin, access, and site-view users and groups, and the site applications version) and can rewrite HUB_MASTER_TOKEN in the platform Secret; it then restarts the Hub Deployment to reload (hub/src/ecmwf/sites/hub/app/admin/views.py, form SitesHUBConfigurationForm in core/.../form/forms.py). An empty site version removes the site.version key so the default applies. An empty Master Token field keeps the stored HUB_MASTER_TOKEN and does not block the save, so a deployment that has no token yet can still change the other settings. A separate admin-only route /hub/admin/generate/master-token/ returns a fresh generated master token. These edits live in the cluster only: the next helm upgrade overwrites them with the chart values. For durable changes, update the Helm values.

hub.master_token is optional. When it is empty, SitesHubMasterTokenHandler rejects every bearer master token, and the Hub logs a warning at startup (hub/src/ecmwf/sites/hub/server.py).

Hub labels

Separate from branding, the hub.labels section configures the Hub title and the menu, page, and action labels (13 keys: hub_title, list_my, list_all, administration, about, create, view, details, sharing, settings, upgrade_all, force_upgrade_all, restart_all), delivered to templates as the context key labels (from the labels_config singleton, core/src/ecmwf/sites/core/config/labels.py). Defaults match the shipped interface.

Consistency checks

core/src/ecmwf/sites/core/config/consistency.py runs at startup. Errors stop the process and list all problems; warnings are logged and shown on the Hub Dependencies page. Each finding carries a short code (shown in brackets). Two warnings also change the effective configuration, which the message states. The implemented table:

Condition Result
directory none + email provider Warning [directory-email]: addresses use <username>@<email.domain>; default_users keep their configured names
directory none + validate_usernames: true Warning [directory-validate-usernames]; the value is coerced to false
directory ldap + empty search_base or search_filter Error [ldap-search]
notifications none Warning [notifications-none]: no notifications; access/publish/storage requests return 503; retention still archives and deletes
notifications enabled + empty jobs.notification_default_assignee Error [notifications-assignee]
retention_broadcast_channels lists a non-provider id Warning [retention-broadcast-channels]; the id is removed
email provider + email.send_enabled: false Warning [email-disabled]: no mail is sent
email enabled + smtp_server/sender/domain still at example defaults Error [email-placeholders] (warning when dev)
backup_notifications_email set + email provider not enabled + send_enabled: false Warning [backup-email-disabled] (with the provider enabled, the [email-disabled] row applies)
jira provider + missing api_token/project or example api_url Error [jira-config]
analytics splunk + missing credentials Error [splunk-credentials]
analytics opensearch + example base_url Error [opensearch-url] (warning when dev)
retention_expired_archive_after_days ≥ retention_expired_delete_after_days Error [retention-days]
quota_trigger_percentage outside 1..100 Error [quota-percentage]

The chart-level render guards (routing.host, DNS validation, removed keys) run earlier and are listed under "Upgrade guards".

Branding schema

All branding values live under branding in the platform configuration. Every setting has a neutral default, so templates always render configured values.

Setting Default Rules
organisation_name Example Organisation ≤ 100 chars, trimmed, must not be empty, no control characters or {/}
logo, favicon packaged Sites SVG (core/.../config/assets/) raw SVG or data:image/svg+xml;base64,...; an absent or blank value falls back to the packaged asset; anything else is validated per the rules below
documentation_url https://example.org/docs HTTP(S) or root-relative; empty hides the link
support_url https://example.org/support same
privacy_url https://example.org/privacy same
terms_url https://example.org/terms same
policy_url empty (no link) same

The URL validator (validate_branding_url) rejects values with whitespace, control characters, or backslashes; accepts a scheme only as http/https with a hostname and no user:password@ part; and rejects protocol-relative values (//host) so only true root-relative paths take the platform origin.

Where each link appears (help menu, footer, error pages, status panel, notifications, API specs) is driven by core/src/ecmwf/sites/core/branding.py; root-relative URLs become absolute on the platform host in email/Jira notifications and OpenAPI documents. The help menu is hidden and the footer shows only the copyright when no links are configured. Root-relative links on the About page point staff users to the Hub governance page when policy_url is empty.

SVG validation rules

parse_branding_svg() applies these rules when configuration is loaded. An invalid configured value stops Hub and operator startup with a clear error; the platform never silently falls back to the packaged SVG. Results are cached, and each parsed SVG carries a version — the first 12 hex characters of its SHA-256 — used for cache busting.

  1. Input is either raw SVG markup or a base64 data URL with the exact data:image/svg+xml;base64 header (only SVG is accepted, strict base64).
  2. Decoded content is at most 262 144 bytes and valid UTF-8.
  3. No DOCTYPE or entity declarations.
  4. The document parses as XML and has an SVG root; all elements stay in the root's namespace.
  5. Forbidden elements: script, animate, animatemotion, animatetransform, audio, embed, foreignobject, iframe, object, set, video.
  6. No event attributes (on*) and no xml:base.
  7. href/src attributes must be empty or fragment-only (#id). Any attribute value containing a CSS url(...) reference must reference a fragment only. No attribute value may contain javascript:.
  8. Inline CSS (in style attributes and style elements, after comment stripping) must contain no backslashes, @import, expression(, javascript:, src:, or image references (url, image, image-set).

The same function validates local branding assets before deploy (18-local-development).

Where branding is rendered

Target Mechanism
Hub pages, Hub admin pages Template data at request time (organisation name, logo, favicon, footer/help links)
Hub favicon served at /favicon.svg?v=<content-hash>, /favicon.ico redirects there
Site proxy listing + sign-in error page SVG embedded as data URL; site favicon.ico requests 301 to the Hub
File Browser (site admin) Operator writes admin-logo.svg / admin-favicon.svg into the site files ConfigMap, mounted over the packaged assets
OAuth2 Proxy error page Operator renders oauth2-error.html per site (the sign-in page is never shown; skip_provider_button goes straight to the identity provider (IdP))
Site retention banner Operator renders retention-banner.html; injected by the proxy when RETENTION_DATE passed
Email/Jira notifications Organisation name plus absolute documentation/support links
OpenAPI specs Organisation name plus absolute terms/support links

On the Hub, proxy, and error pages the logo links to /hub/ with alternative text Sites; these two values are not configurable. The footer copyright text is the software copyright and never changes.

Applying branding changes

The Hub and the operator read the branding section at process start:

  1. Update the branding section in the sites-config ConfigMap (normally via helm upgrade).
  2. Restart both: kubectl rollout restart deployment/sites-hub deployment/sites-operator.

Hub pages use the new values immediately. Each site keeps its old branding until its next update, when the operator regenerates the files ConfigMap and restarts the pods (09-update-flow). The notification and analytics jobs reload configuration on every run. Use Force Upgrade All Sites to apply branding to existing sites when application versions are unchanged.

This section does not configure copyright text, PNG/ICO icons, or full template overrides. File Browser keeps its own layout.

Behaviour requirements

ID Requirement Implemented in Verified by
CFG-01 Configuration loads from the mounted ConfigMap directory with environment overrides, and fails startup on schema or consistency errors. core/.../config/main.py, schema/models.py, consistency.py core/tests/test_config_schema.py (directory merge order, call-time path, env precedence, required namespace), core/tests/test_config_consistency.py (16 rule tests)
CFG-02 The chart injects namespace, site.version, and routing; a user-supplied sitesConfig.routing fails rendering, and a user-supplied site.version is overridden. helm/.../_helpers.tpl, configmap-sites-config.yaml core/tests/test_helm_configuration.py (test_chart_injects_site_version_from_images_tag, test_user_supplied_site_version_in_sites_config_is_overridden, test_user_supplied_sites_config_routing_fails_rendering)
CFG-03 An invalid configured branding SVG stops startup; only an absent or empty value uses the packaged default. core/.../branding.py, config/schema/models.py core/tests/test_branding.py (test_invalid_logo_fails_validation, test_empty_logo_uses_default_logo, favicon pair), hub/tests/test_branding_rendering.py
CFG-04 SVG validation enforces the size, encoding, element, attribute, and reference rules listed above. core/.../branding.py core/tests/test_branding.py (test_rejects_entities_scripts_event_attributes_and_external_resources, size limit), utilities/tests/deploy-sites-branding.test.sh
CFG-05 The Hub favicon URL carries a content hash so a branding change busts caches. core/.../branding.py (version), Hub templates hub/tests/test_branding_rendering.py
CFG-06 Sites adopt new branding at their next update, when the operator re-renders the files ConfigMap. operator/.../site_update.py operator/tests/test_site_update.py
CFG-07 A removed or moved configuration key fails at Helm render or at schema load, and the error names the replacement key. helm/.../_helpers.tpl, schema/models.py core/tests/test_helm_configuration.py (test_removed_integration_keys_are_rejected, test_removed_job_and_secret_settings_are_rejected), core/tests/test_config_schema.py (test_removed_yaml_settings_are_rejected, test_removed_provider_keys_are_rejected_for_every_feature)
CFG-08 Cross-field rules reject a Gateway mode without host or Gateway name, a mirror source equal to the platform host, OIDC endpoints without an issuer, and an integration default outside its providers. schema/models.py, consistency.py core/tests/test_routing.py (test_gateway_mode_requires_host_and_gateway_name), core/tests/test_config_schema.py (test_source_domain_must_differ_from_routing_host), core/tests/test_oidc_discovery.py (test_endpoints_require_issuer); the unsupported-policy-feature rule is not tested
CFG-09 Secret values reach components only through envFrom secret references, never through the ConfigMap. helm/.../deployment-hub.yaml, deployment-operator.yaml, cronjob-*.yaml, values.yaml secret key comments — (render does not assert the secret references)
CFG-10 Consistency warnings for validate_usernames and unknown broadcast channels change the effective configuration value, not only the log. core/.../config/consistency.py core/tests/test_config_consistency.py (test_directory_none_disables_username_validation, test_unknown_broadcast_channels_removed)