From d7010678d8ce6b3f966f0e4b7b5e027f62eb31bc Mon Sep 17 00:00:00 2001 From: Micah Nagel Date: Tue, 17 Dec 2024 07:26:25 -0700 Subject: [PATCH] feat: experimental opt-in classification banner (#1127) ## Description This PR adds an experimental (opt-in) classification banner provided via an envoyfilter. This filter injects html/script code to add a top (and optionally bottom) banner with the desired classification level and the expected colors. This has primarily been validated against Keycloak and Grafana, other UIs may not display perfectly (part of why this is noted as experimental). ## Related Issue Fixes https://github.com/defenseunicorns/uds-core/issues/1079 ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Other (security config, docs update, etc) ## Steps to Validate 1. Modify the slim-dev bundle to add the below overrides: ```yaml overrides: istio-controlplane: uds-global-istio-config: values: - path: classificationBanner.text value: "UNCLASSIFIED" # Possible values: UNCLASSIFIED, CUI, CONFIDENTIAL, SECRET, TOP SECRET, TOP SECRET//SCI, UNKNOWN - path: classificationBanner.addFooter value: true - path: classificationBanner.enabledHosts value: - keycloak.admin.{{ .Values.domain }} - sso.{{ .Values.domain }} ``` 2. Deploy slim-dev: `uds run slim-dev --set flavor=unicorn` 3. Validate that the banner with the expected classification level appears on the Keycloak admin and tenant interfaces and does not overlap with any content. ## Checklist before merging - [x] Test, docs, adr added or updated as needed - [x] [Contributor Guide](https://github.com/defenseunicorns/uds-template-capability/blob/main/CONTRIBUTING.md) followed --- .../configuration/uds-optional-features.md | 31 ++++ docs/reference/deployment/flavors.md | 2 +- .../templates/classification-banner.yaml | 161 ++++++++++++++++++ src/istio/common/chart/values.schema.json | 20 +++ src/istio/common/chart/values.yaml | 13 ++ src/istio/common/zarf.yaml | 2 + 6 files changed, 228 insertions(+), 1 deletion(-) create mode 100644 docs/reference/configuration/uds-optional-features.md create mode 100644 src/istio/common/chart/templates/classification-banner.yaml create mode 100644 src/istio/common/chart/values.schema.json diff --git a/docs/reference/configuration/uds-optional-features.md b/docs/reference/configuration/uds-optional-features.md new file mode 100644 index 000000000..6a943546c --- /dev/null +++ b/docs/reference/configuration/uds-optional-features.md @@ -0,0 +1,31 @@ +--- +title: Optional Features +--- + +UDS Core adds features to support specific needs that we commonly see across deployments and/or to meet the constraints and controls required by environments. This document contains features we have identified that are conditionally required or requested in environments that are present in core, but must be opted-into to use. + +## Classification Banner (_EXPERIMENTAL_) + +UDS Core includes a configurable [EnvoyFilter](https://istio.io/latest/docs/reference/config/networking/envoy-filter/) that will add/inject classification banners into user interfaces exposed via the Istio gateways. This is fully configurable to any classification level and can be applied to a set of hosts that you specify. The classification level set via values will also determine the color of the banner background and text, corresponding with the [standard colors](https://www.astrouxds.com/components/classification-markings) required for these markings. + +Due to the wide variety of ways that user interfaces can be architected, this approach may not work for all applications and should be validated in a development or staging environment before adoption. For custom built applications, native handling of the banner within the application is often a better path. You can configure the classification banner with bundle overrides, such as the example below: + +```yaml +packages: + - name: uds-core + repository: ghcr.io/defenseunicorns/packages/uds/core + ref: x.x.x + overrides: + istio-controlplane: + uds-global-istio-config: + values: + - path: classificationBanner.text + value: "UNCLASSIFIED" # Possible values: UNCLASSIFIED, CUI, CONFIDENTIAL, SECRET, TOP SECRET, TOP SECRET//SCI, UNKNOWN + - path: classificationBanner.addFooter + value: true + - path: classificationBanner.enabledHosts # Opt-in for specific hosts + value: + - keycloak.admin.{{ .Values.domain }} # Note the support for helm templating + - sso.{{ .Values.domain }} + - grafana.admin.uds.dev +``` diff --git a/docs/reference/deployment/flavors.md b/docs/reference/deployment/flavors.md index 26349aa16..6f87bcb4b 100644 --- a/docs/reference/deployment/flavors.md +++ b/docs/reference/deployment/flavors.md @@ -14,7 +14,7 @@ Demo and dev bundles (`k3d-core-demo` and `k3d-core-slim-dev`) are only publishe | --------------------- | ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `registry1` | `ghcr.io/defenseunicorns/packages/uds` | [Ironbank](https://p1.dso.mil/services/iron-bank) - DoD hardened images (only supports amd64 architecture currently) | | `upstream` | `ghcr.io/defenseunicorns/packages/uds` | Various sources, typically DockerHub/GHCR/Quay, these are the default images used by helm charts | -| **ALPHA** `unicorn` | `ghcr.io/defenseunicorns/packages/private/uds` | Industry best images designed with security and minimalism in mind | +| `unicorn` | `ghcr.io/defenseunicorns/packages/private/uds` | Industry best images designed with security and minimalism in mind | :::note The `unicorn` flavored packages are only available in a private repository. These packages are available for all members of the Defense Unicorns organization/company, if you are outside the organization [contact us](https://www.defenseunicorns.com/contactus) if you are interested in using this flavor for your mission. diff --git a/src/istio/common/chart/templates/classification-banner.yaml b/src/istio/common/chart/templates/classification-banner.yaml new file mode 100644 index 000000000..31c757cac --- /dev/null +++ b/src/istio/common/chart/templates/classification-banner.yaml @@ -0,0 +1,161 @@ +# Copyright 2024 Defense Unicorns +# SPDX-License-Identifier: AGPL-3.0-or-later OR LicenseRef-Defense-Unicorns-Commercial + +{{- if .Values.classificationBanner.enabledHosts }} +apiVersion: networking.istio.io/v1alpha3 +kind: EnvoyFilter +metadata: + name: classification-banner + namespace: istio-system +spec: + configPatches: + - applyTo: HTTP_FILTER + match: + context: GATEWAY + listener: + filterChain: + filter: + name: "envoy.filters.network.http_connection_manager" + subFilter: + name: "envoy.filters.http.router" + patch: + operation: INSERT_BEFORE + value: + name: envoy.filters.http.compressor + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.compressor.v3.Compressor + response_direction_config: + common_config: + min_content_length: 100 + content_type: + - text/html + disable_on_etag_header: true + request_direction_config: + common_config: + enabled: + default_value: false + runtime_key: request_compressor_enabled + compressor_library: + name: text_optimized + typed_config: + "@type": type.googleapis.com/envoy.extensions.compression.gzip.compressor.v3.Gzip + memory_level: 3 + window_bits: 10 + compression_level: BEST_COMPRESSION + compression_strategy: DEFAULT_STRATEGY + - applyTo: HTTP_FILTER + match: + context: GATEWAY + listener: + filterChain: + filter: + name: "envoy.filters.network.http_connection_manager" + subFilter: + name: "envoy.filters.http.router" + patch: + operation: INSERT_BEFORE + value: # Lua script configuration + name: envoy.lua + typed_config: + "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua" + inlineCode: |- + -- Setup colors, text and banner div(s) + local classColorMap = { + UNCLASSIFIED = { backgroundColor = "#007a33", textColor = "#ffffff" }, + CUI = { backgroundColor = "#502b85", textColor = "#ffffff" }, + CONFIDENTIAL = { backgroundColor = "#0033a0", textColor = "#ffffff" }, + SECRET = { backgroundColor = "#c8102e", textColor = "#ffffff" }, + ["TOP SECRET"] = { backgroundColor = "#ff8c00", textColor = "#000000" }, + ["TOP SECRET//SCI"] = { backgroundColor = "#fce83a", textColor = "#000000" }, + UNKNOWN = { backgroundColor = "#000000", textColor = "#ffffff" }, + } + local classification = "{{ .Values.classificationBanner.text }}" + local colors = classColorMap[classification] or classColorMap["UNKNOWN"] + local backgroundColor = colors.backgroundColor + local textColor = colors.textColor + local style = "background-color: " .. backgroundColor .. "; color: " .. textColor .. "; height: 24px; line-height: 24px; border: 1px solid transparent; border-radius: 0; position: fixed; left: 0; width: 100vw; text-align: center; margin: 0; z-index: 10000;" + local header = "
" .. classification .. "
" + local footer = "
" .. classification .. "
" + + -- Add script to manage padding around the body of the response + local bodyPaddingScript = [[ + + ]] + + -- List of enabled hosts as a table for quick lookup, injected via Helm + local enabled_hosts = { + {{- range .Values.classificationBanner.enabledHosts }} + ["{{ tpl . $ }}"] = true, + {{- end }} + } + + -- Handle request: Extract `:authority` and store it as metadata + function envoy_on_request(request_handle) + local host = request_handle:headers():get(":authority") + request_handle:streamInfo():dynamicMetadata():set("envoy.lua", "host", tostring(host)) + end + + -- Inject the banner for any hosts where it is enabled + function envoy_on_response(response_handle) + local content_type = response_handle:headers():get("Content-Type") or "" + local host = response_handle:streamInfo():dynamicMetadata():get("envoy.lua")["host"] + + if string.find(content_type, "text/html") and enabled_hosts[host] then + local body = response_handle:body():getBytes(0, response_handle:body():length()) + local body_text = tostring(body) + + -- Insert banners into + {{- if .Values.classificationBanner.addFooter }} + body_text = body_text:gsub("]*)>", "" .. header .. footer) + {{- else }} + body_text = body_text:gsub("]*)>", "" .. header) + {{- end }} + + -- Insert script into + body_text = body_text:gsub("", "" .. bodyPaddingScript) + + response_handle:body():setBytes(body_text) + end + end + - applyTo: HTTP_FILTER + match: + context: GATEWAY + listener: + filterChain: + filter: + name: "envoy.filters.network.http_connection_manager" + subFilter: + name: "envoy.filters.http.router" + patch: + operation: INSERT_BEFORE + value: # Lua script configuration + name: envoy.filters.http.decompressor + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.decompressor.v3.Decompressor + decompressor_library: + name: small + typed_config: + "@type": "type.googleapis.com/envoy.extensions.compression.gzip.decompressor.v3.Gzip" + chunk_size: 65536 + request_direction_config: + common_config: + enabled: + default_value: false + runtime_key: request_decompressor_enabled +{{- end }} diff --git a/src/istio/common/chart/values.schema.json b/src/istio/common/chart/values.schema.json new file mode 100644 index 000000000..79a7d60ac --- /dev/null +++ b/src/istio/common/chart/values.schema.json @@ -0,0 +1,20 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "classificationBanner": { + "type": "object", + "properties": { + "addFooter": { + "type": "boolean", + "description": "Indicates whether to add a footer to the classification banner." + }, + "text": { + "type": ["string", "null"], + "pattern": "^(?i)(UNCLASSIFIED|CUI|CONFIDENTIAL|SECRET|TOP SECRET|TOP SECRET//SCI|UNKNOWN|)$", + "description": "The classification banner text must match one of the allowed values (case-insensitive): UNCLASSIFIED, CONTROLLED, CONFIDENTIAL, SECRET, TOP SECRET, TOP SECRET//SCI, UNKNOWN, or it can be an empty string or null." + } + } + } + } +} diff --git a/src/istio/common/chart/values.yaml b/src/istio/common/chart/values.yaml index c56503481..5ea684278 100644 --- a/src/istio/common/chart/values.yaml +++ b/src/istio/common/chart/values.yaml @@ -1 +1,14 @@ # SPDX-License-Identifier: AGPL-3.0-or-later OR Commercial +classificationBanner: + text: "UNKNOWN" + addFooter: true + # Hosts to enable the banner on + enabledHosts: [] + # Examples (supports helm templating) + # - keycloak.{{ .Values.adminDomain }} + # - sso.{{ .Values.domain }} + # - grafana.admin.uds.dev + +domain: "###ZARF_VAR_DOMAIN###" +# Note: This does not handle an empty admin domain zarf var +adminDomain: "###ZARF_VAR_ADMIN_DOMAIN###" diff --git a/src/istio/common/zarf.yaml b/src/istio/common/zarf.yaml index 9980e23db..2776fa194 100644 --- a/src/istio/common/zarf.yaml +++ b/src/istio/common/zarf.yaml @@ -25,6 +25,8 @@ components: namespace: istio-system version: 0.1.0 localPath: chart + valuesFiles: + - "chart/values.yaml" actions: onDeploy: before: