diff --git a/src/_nebari/initialize.py b/src/_nebari/initialize.py index 7566fe7b4..13826dc2d 100644 --- a/src/_nebari/initialize.py +++ b/src/_nebari/initialize.py @@ -178,6 +178,23 @@ def render_config( config["certificate"] = {"type": CertificateEnum.letsencrypt.value} config["certificate"]["acme_email"] = ssl_cert_email + # Add ingress configuration with HSTS settings + # Determine if HSTS should be enabled based on certificate type + cert_type = config.get("certificate", {}).get( + "type", CertificateEnum.selfsigned.value + ) + is_valid_cert = cert_type in [CertificateEnum.letsencrypt.value, "existing"] + hsts_enabled = is_valid_cert + + config["ingress"] = { + "hsts": { + "enabled": hsts_enabled, + "max_age": 31536000, # 1 year + "include_subdomains": True, + "preload": False, + } + } + if config_set: config_set = read_config_set(config_set) config = utils.deep_merge(config, config_set.config) diff --git a/src/_nebari/stages/kubernetes_ingress/__init__.py b/src/_nebari/stages/kubernetes_ingress/__init__.py index b83d5039f..99238ce01 100644 --- a/src/_nebari/stages/kubernetes_ingress/__init__.py +++ b/src/_nebari/stages/kubernetes_ingress/__init__.py @@ -145,7 +145,17 @@ class DnsProvider(schema.Base): auto_provision: Optional[bool] = False +class HSTS(schema.Base): + # See https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + # for more info + enabled: bool = True + max_age: int = 31536000 + include_subdomains: bool = True + preload: bool = False + + class Ingress(schema.Base): + hsts: Optional[HSTS] = None terraform_overrides: Dict = {} @@ -203,6 +213,24 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): cert_details["cloudflare-dns-api-token"] = os.environ.get( "CLOUDFLARE_TOKEN" ) + + # Initialize HSTS based on certificate type if not explicitly configured + hsts = self.config.ingress.hsts + if hsts is None: + # Only enable HSTS for valid certificates (lets-encrypt, existing) + # Do not enable for self-signed certs to avoid HSTS pinning issues during initial setup + is_valid_cert = cert_type in ["lets-encrypt", "existing"] + if is_valid_cert: + hsts = HSTS() + else: + hsts = HSTS(enabled=False) + + hsts_config = { + "hsts-enabled": hsts.enabled, + "hsts-max-age": hsts.max_age, + "hsts-include-subdomains": hsts.include_subdomains, + "hsts-preload": hsts.preload, + } return { **{ "traefik-image": { @@ -217,6 +245,7 @@ def input_vars(self, stage_outputs: Dict[str, Dict[str, Any]]): **self.config.ingress.terraform_overrides, }, **cert_details, + **hsts_config, } def set_outputs( diff --git a/src/_nebari/stages/kubernetes_ingress/template/main.tf b/src/_nebari/stages/kubernetes_ingress/template/main.tf index b8476b8b4..19bdbf670 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/main.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/main.tf @@ -16,4 +16,9 @@ module "kubernetes-ingress" { load-balancer-annotations = var.load-balancer-annotations load-balancer-ip = var.load-balancer-ip additional-arguments = var.additional-arguments + + hsts-enabled = var.hsts-enabled + hsts-max-age = var.hsts-max-age + hsts-include-subdomains = var.hsts-include-subdomains + hsts-preload = var.hsts-preload } diff --git a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/hsts-middleware.tf b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/hsts-middleware.tf new file mode 100644 index 000000000..c5912c740 --- /dev/null +++ b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/hsts-middleware.tf @@ -0,0 +1,20 @@ +resource "kubernetes_manifest" "hsts_middleware" { + count = var.hsts-enabled ? 1 : 0 + + manifest = { + apiVersion = "traefik.containo.us/v1alpha1" + kind = "Middleware" + metadata = { + name = "${var.name}-hsts" + namespace = var.namespace + } + spec = { + headers = { + stsSeconds = var.hsts-max-age + stsIncludeSubdomains = var.hsts-include-subdomains + stsPreload = var.hsts-preload + forceSTSheader = true + } + } + } +} diff --git a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf index 77e0b7a58..6f9575f59 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/main.tf @@ -35,6 +35,12 @@ locals { disabled = [] } add-certificate = local.certificate-settings[var.certificate-service] + + # HSTS middleware configuration + hsts-middleware = var.hsts-enabled ? [ + "--entrypoints.websecure.http.middlewares=${var.namespace}-${var.name}-hsts@kubernetescrd", + "--entrypoints.minio.http.middlewares=${var.namespace}-${var.name}-hsts@kubernetescrd", + ] : [] } @@ -308,6 +314,7 @@ resource "kubernetes_deployment" "main" { "--log.level=${var.loglevel}", ], local.add-certificate, + local.hsts-middleware, var.additional-arguments, ) diff --git a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf index a895d7b50..bed0c679a 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/modules/kubernetes/ingress/variables.tf @@ -90,3 +90,27 @@ variable "additional-arguments" { type = list(string) default = [] } + +variable "hsts-enabled" { + description = "Enable HSTS (HTTP Strict Transport Security) headers" + type = bool + default = false +} + +variable "hsts-max-age" { + description = "HSTS max-age in seconds (default 300s / 5 minutes; increase to 31536000 for production)" + type = number + default = 300 +} + +variable "hsts-include-subdomains" { + description = "Include subdomains in HSTS policy" + type = bool + default = true +} + +variable "hsts-preload" { + description = "Enable HSTS preload" + type = bool + default = false +} diff --git a/src/_nebari/stages/kubernetes_ingress/template/variables.tf b/src/_nebari/stages/kubernetes_ingress/template/variables.tf index 940d0597e..c592a6549 100644 --- a/src/_nebari/stages/kubernetes_ingress/template/variables.tf +++ b/src/_nebari/stages/kubernetes_ingress/template/variables.tf @@ -81,3 +81,27 @@ variable "additional-arguments" { type = list(string) default = [] } + +variable "hsts-enabled" { + description = "Enable HSTS (HTTP Strict Transport Security) headers" + type = bool + default = false +} + +variable "hsts-max-age" { + description = "HSTS max-age in seconds (default 300s / 5 minutes; increase to 31536000 for production)" + type = number + default = 300 +} + +variable "hsts-include-subdomains" { + description = "Include subdomains in HSTS policy" + type = bool + default = true +} + +variable "hsts-preload" { + description = "Enable HSTS preload" + type = bool + default = false +}