Skip to content
17 changes: 17 additions & 0 deletions src/_nebari/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Member Author

@Adam-D-Lewis Adam-D-Lewis Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add hsts to nebari config on nebari init ... if cert type is letsencrypt or 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)
Expand Down
29 changes: 29 additions & 0 deletions src/_nebari/stages/kubernetes_ingress/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {}


Expand Down Expand Up @@ -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:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if hsts is not in config, then default to enabling it for lets-encrypt and existing certs, otherwise default to disabling it

# 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": {
Expand All @@ -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(
Expand Down
5 changes: 5 additions & 0 deletions src/_nebari/stages/kubernetes_ingress/template/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
] : []
}


Expand Down Expand Up @@ -308,6 +314,7 @@ resource "kubernetes_deployment" "main" {
"--log.level=${var.loglevel}",
],
local.add-certificate,
local.hsts-middleware,
var.additional-arguments,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
24 changes: 24 additions & 0 deletions src/_nebari/stages/kubernetes_ingress/template/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Loading