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
sitesConfiginto thesites-configConfigMap and injects three values itself withmergeOverwrite:namespace(= release namespace),site.version(=images.tag), androuting. These three always come from the chart: asite.versioninsitesConfigis overridden, and asitesConfig.routingkey is rejected outright ("Do not set sitesConfig.routing. Use the top-level routing values."). - The loader (
ConfigurationModelSchema.load()incore/src/ecmwf/sites/core/config/schema/models.py, instantiated bycore/.../config/main.py) reads YAML files from the directory inCONFIG_FILESYSTEM_PATH— resolved at call time, not at import. In directory mode it merges*.yamland*.ymlfiles 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-settingsmaps names of the formSECTION_FIELDwith nested delimiter_andenv_nested_max_split=1(exactly one split), soHUB_MASTER_TOKENsetshub.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
devflag setsverify_tlsto 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. namespacemust be non-empty. Everything else has a default; the shipped defaults point atexample.orgplaceholders 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.modemust beingressorgateway(chart render also checks);routing.hostis required at chart render, and required again in the schema together withrouting.gateway.namewhen the mode isgateway.routing.schememust behttporhttps. Host and Gateway names are validated as DNS names.routing.gateway.policiesmay only list features the configuredpolicy_providersupports (14-policy-providers).trusted_domainsentries 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-ancestorsheader and the CORS help (14).migration.enabledrequiresmigration.source_domain, and the source domain must not equalrouting.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>.providersmust be non-empty and itsdefaultmust be one of them.operator.dependency_log_levelmust beDEBUG,INFO,WARNING, orERROR(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.
- Input is either raw SVG markup or a base64 data URL with the exact
data:image/svg+xml;base64header (only SVG is accepted, strict base64). - Decoded content is at most 262 144 bytes and valid UTF-8.
- No
DOCTYPEor entity declarations. - The document parses as XML and has an SVG root; all elements stay in the root's namespace.
- Forbidden elements:
script,animate,animatemotion,animatetransform,audio,embed,foreignobject,iframe,object,set,video. - No event attributes (
on*) and noxml:base. href/srcattributes must be empty or fragment-only (#id). Any attribute value containing a CSSurl(...)reference must reference a fragment only. No attribute value may containjavascript:.- Inline CSS (in
styleattributes andstyleelements, 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:
- Update the
brandingsection in thesites-configConfigMap (normally viahelm upgrade). - 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) |