diff --git a/jupyterhub/templates/proxy/autohttps/_configmap-dynamic.yaml b/jupyterhub/templates/proxy/autohttps/_configmap-dynamic.yaml new file mode 100644 index 0000000000..070a80e2dd --- /dev/null +++ b/jupyterhub/templates/proxy/autohttps/_configmap-dynamic.yaml @@ -0,0 +1,109 @@ +{{- define "jupyterhub.dynamic.yaml" -}} +# Content of dynamic.yaml to be merged merged with +# proxy.traefik.extraDynamicConfig. +# ---------------------------------------------------------------------------- +http: + # Middlewares tweaks requests. We define them here and reference them in + # our routers. We use them to redirect http traffic and headers to proxied + # web requests. + # + # ref: https://docs.traefik.io/middlewares/overview/ + middlewares: + hsts: + # A middleware to add a HTTP Strict-Transport-Security (HSTS) response + # header, they function as a request for browsers to enforce HTTPS on + # their end in for a given time into the future, and optionally + # subdomains for requests to subdomains as well. + # + # ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + headers: + stsIncludeSubdomains: {{ .Values.proxy.traefik.hsts.includeSubdomains }} + stsPreload: {{ .Values.proxy.traefik.hsts.preload }} + stsSeconds: {{ .Values.proxy.traefik.hsts.maxAge | int64 }} + # A middleware to redirect to https + redirect: + redirectScheme: + permanent: true + scheme: https + # A middleware to add a X-Scheme (X-Forwarded-Proto) header that + # JupyterHub's Tornado web-server needs if expecting to serve https + # traffic. Without it we would run into issues like: + # https://github.com/jupyterhub/jupyterhub/issues/2284 + scheme: + headers: + customRequestHeaders: + # DISCUSS ME: Can we use the X-Forwarded-Proto header instead? It + # seems more recognized. Mozilla calls it the de-facto standard + # header for this purpose, and Tornado recognizes both. + # + # ref: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto + # ref: https://www.tornadoweb.org/en/stable/httpserver.html#http-server + X-Scheme: https + + # Routers routes web requests to a service and optionally tweaks them with + # middleware. + # + # ref: https://docs.traefik.io/routing/routers/ + routers: + # Route secure https traffic to the configurable-http-proxy managed by + # JupyterHub. + default: + entrypoints: + - "https" + middlewares: + - "hsts" + - "scheme" + rule: PathPrefix(`/`) + service: default + # Use our predefined TLS options and certificate resolver, enabling + # this route to act as a TLS termination proxy with high security + # standards. + tls: + certResolver: default + domains: + {{- range $host := .Values.proxy.https.hosts }} + - main: {{ $host }} + {{- end }} + options: default + + # Route insecure http traffic to https + insecure: + entrypoints: + - "http" + middlewares: + - "redirect" + rule: PathPrefix(`/`) + service: default + + # Services represents the destinations we route traffic to. + # + # ref: https://docs.traefik.io/routing/services/ + services: + # Represents the configurable-http-proxy (chp) server that is managed by + # JupyterHub to route traffic both to itself and to user pods. + default: + loadBalancer: + servers: + - url: 'http://proxy-http:8000/' + +# Configure TLS to give us an A+ in the ssllabs.com test +# +# ref: https://www.ssllabs.com/ssltest/ +tls: + options: + default: + # Allowed ciphers adapted from Mozillas SSL Configuration Generator + # configured for Intermediate support which doesn't support very old + # systems but doesn't require very modern either. + # + # ref: https://ssl-config.mozilla.org/#server=traefik&version=2.1.2&config=intermediate&guideline=5.4 + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305 + - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305 + minVersion: VersionTLS12 + sniStrict: true +{{- end }} diff --git a/jupyterhub/templates/proxy/autohttps/_configmap-traefik.yaml b/jupyterhub/templates/proxy/autohttps/_configmap-traefik.yaml new file mode 100644 index 0000000000..8e77d1e071 --- /dev/null +++ b/jupyterhub/templates/proxy/autohttps/_configmap-traefik.yaml @@ -0,0 +1,68 @@ +{{- define "jupyterhub.traefik.yaml" -}} +# Content of traefik.yaml to be merged merged with +# proxy.traefik.extraStaticConfig. +# ---------------------------------------------------------------------------- + +# Config of logs about web requests +# +# ref: https://docs.traefik.io/observability/access-logs/ +accessLog: + # Redact commonly sensitive headers + fields: + headers: + names: + Authorization: redacted + Cookie: redacted + Set-Cookie: redacted + X-Xsrftoken: redacted + # Only log errors + filters: + statusCodes: + - 500-599 + +# Automatically acquire certificates certificates form a Certificate +# Authority (CA) like Let's Encrypt using the ACME protocol's HTTP-01 +# challenge. +# +# ref: https://docs.traefik.io/https/acme/#certificate-resolvers +certificatesResolvers: + default: + acme: + caServer: {{ .Values.proxy.https.letsencrypt.acmeServer }} + email: {{ .Values.proxy.https.letsencrypt.contactEmail }} + httpChallenge: + entryPoint: http + storage: /etc/acme/acme.json + +# Let Traefik listen to port 80 and port 443 +# +# ref: https://docs.traefik.io/routing/entrypoints/ +entryPoints: + # Port 80, used for: + # - ACME HTTP-01 challenges + # - Redirects to HTTPS + http: + address: ':80' + # Port 443, used for: + # - TLS Termination Proxy, where HTTPS transitions to HTTP. + https: + address: ':443' + # Configure a high idle timeout for our websockets connections + transport: + respondingTimeouts: + idleTimeout: 10m0s + +# Config of logs about what happens to Traefik itself (startup, +# configuration, events, shutdown, and so on). +# +# ref: https://docs.traefik.io/observability/logs +log: + level: {{ if .Values.debug.enabled -}} DEBUG {{- else -}} INFO {{- end }} + +# Let Traefik monitor another file we mount for dynamic configuration. As we +# mount this file through this configmap, we can make a `kubectl edit` on the +# configmap and have Traefik update on changes to dynamic.yaml. +providers: + file: + filename: /etc/traefik/dynamic.yaml +{{- end }} diff --git a/jupyterhub/templates/proxy/autohttps/configmap.yaml b/jupyterhub/templates/proxy/autohttps/configmap.yaml index e407a9db42..5573e7f190 100644 --- a/jupyterhub/templates/proxy/autohttps/configmap.yaml +++ b/jupyterhub/templates/proxy/autohttps/configmap.yaml @@ -1,6 +1,18 @@ {{- $HTTPS := (and .Values.proxy.https.hosts .Values.proxy.https.enabled) }} {{- $autoHTTPS := (and $HTTPS (eq .Values.proxy.https.type "letsencrypt")) }} {{- if $autoHTTPS -}} +{{- $_ := required .Values.proxy.https.letsencrypt.contactEmail "proxy.https.letsencrypt.contactEmail is a required field" -}} + +# This configmap contains Traefik configuration files to be mounted. +# - traefik.yaml will only be read during startup (static configuration) +# - dynamic.yaml will be read on change (dynamic configuration) +# +# ref: https://docs.traefik.io/getting-started/configuration-overview/ +# +# The configuration files are first rendered with Helm templating to large YAML +# strings. Then we use the fromYAML function on these strings to get an object, +# that we in turn merge with user provided extra configuration. +# kind: ConfigMap apiVersion: v1 metadata: @@ -8,127 +20,9 @@ metadata: labels: {{- include "jupyterhub.labels" . | nindent 4 }} data: - # This configmap provides the complete configuration for our traefik proxy. - # traefik has 'static' config and 'dynamic' config (https://docs.traefik.io/getting-started/configuration-overview/) - # traefik.toml contains the static config, while dynamic.toml has the dynamic config. - traefik.toml: | + traefik.yaml: | + {{- include "jupyterhub.traefik.yaml" . | fromYaml | merge .Values.proxy.traefik.extraStaticConfig | toYaml | nindent 4 }} + dynamic.yaml: | + {{- include "jupyterhub.dynamic.yaml" . | fromYaml | merge .Values.proxy.traefik.extraDynamicConfig | toYaml | nindent 4 }} - # We wanna listen on both port 80 & 443 - [entryPoints] - [entryPoints.http] - # traefik is used only for TLS termination, so port 80 is just for redirects - # No configuration for timeouts, etc needed - address = ":80" - - [entryPoints.https] - address = ":443" - - [entryPoints.https.transport.respondingTimeouts] - # High idle timeout, because we have websockets - idleTimeout = "10m0s" - - [log] - level = "INFO" - - [accessLog] - [accessLog.filters] - # Only error codes - statusCodes = ["500-599"] - - # Redact possible sensitive headers from log - [accessLog.fields.headers] - [accessLog.fields.headers.names] - Authorization = "redact" - Cookie = "redact" - Set-Cookie = "redact" - X-Xsrftoken = "redact" - - # We want certificates to come from Let's Encrypt, with the HTTP-01 challenge - [certificatesResolvers.le.acme] - email = {{ required "proxy.https.letsencrypt.contactEmail is a required field" .Values.proxy.https.letsencrypt.contactEmail | quote }} - storage = "/etc/acme/acme.json" - {{- if .Values.proxy.https.letsencrypt.acmeServer }} - caServer = {{ .Values.proxy.https.letsencrypt.acmeServer | quote }} - {{- end}} - [certificatesResolvers.le.acme.httpChallenge] - # Use our port 80 http endpoint for the HTTP-01 challenge - entryPoint = "http" - - [providers] - [providers.file] - # Configuration for routers & other dynamic items come from this file - # This file is also provided by this configmap - filename = '/etc/traefik/dynamic.toml' - - dynamic.toml: | - # Configure TLS to give us an A+ in the ssllabs test - [tls] - [tls.options] - [tls.options.default] - sniStrict = true - # Do not support insecureTLS 1.0 or 1.1 - minVersion = "VersionTLS12" - # Ciphers to support, adapted from https://ssl-config.mozilla.org/#server=traefik&server-version=1.7.12&config=intermediate - # This supports a reasonable number of browsers. - cipherSuites = [ - "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", - "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", - "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", - "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305" - ] - - # traefik uses middlewares to set options in specific routes - # These set up the middleware options, which are then referred to by the routes - [http.middlewares] - # Used to do http -> https redirects - [http.middlewares.redirect.redirectScheme] - scheme = "https" - - # Used to set appropriate headers in requests sent to CHP - [http.middlewares.https.headers] - [http.middlewares.https.headers.customRequestHeaders] - # Tornado needs this for referrer checks - # You run into stuff like https://github.com/jupyterhub/jupyterhub/issues/2284 otherwise - X-Scheme = "https" - - # Used to set HSTS headers based on user provided options - [http.middlewares.hsts.headers] - stsSeconds = {{ int64 .Values.proxy.traefik.hsts.maxAge }} - {{ if .Values.proxy.traefik.hsts.includeSubdomains }} - stsIncludeSubdomains = true - {{- end }} - - - # Routers match conditions (rule) to options (middlewares) and a backend (service) - [http.routers] - # Listen on the http endpoint (port 80), redirect everything to https - [http.routers.httpredirect] - rule = "PathPrefix(`/`)" - service = "chp" - entrypoints = ["http"] - middlewares = ["redirect"] - - # Listen on https endpoint (port 443), proxy everything to chp - [http.routers.chp] - rule = "PathPrefix(`/`)" - entrypoints = ["https"] - middlewares = ["hsts", "https"] - service = "chp" - - [http.routers.chp.tls] - # use our nice TLS defaults, and get HTTPS from Let's Encrypt - options = "default" - certResolver = "le" - {{- range $host := .Values.proxy.https.hosts }} - [[http.routers.chp.tls.domains]] - main = "{{ $host }}" - {{- end}} - - # Set CHP to be our only backend where traffic is routed to - [http.services] - [http.services.chp.loadBalancer] - [[http.services.chp.loadBalancer.servers]] - url = "http://proxy-http:8000/" {{- end }} diff --git a/jupyterhub/templates/proxy/autohttps/deployment.yaml b/jupyterhub/templates/proxy/autohttps/deployment.yaml index 01f8f72af8..5cd99dbefc 100644 --- a/jupyterhub/templates/proxy/autohttps/deployment.yaml +++ b/jupyterhub/templates/proxy/autohttps/deployment.yaml @@ -1,6 +1,7 @@ {{- $HTTPS := (and .Values.proxy.https.hosts .Values.proxy.https.enabled) }} {{- $autoHTTPS := (and $HTTPS (eq .Values.proxy.https.type "letsencrypt")) }} {{- if $autoHTTPS -}} + apiVersion: apps/v1 kind: Deployment metadata: @@ -18,7 +19,11 @@ spec: {{- include "jupyterhub.matchLabels" . | nindent 8 }} hub.jupyter.org/network-access-proxy-http: "true" annotations: - checksum/config-map: {{ include (print .Template.BasePath "/proxy/autohttps/configmap.yaml") . | sha256sum }} + # Only force a restart through a change to this checksum when the static + # configuration is changed, as the dynamic can be updated after start. + # Any disruptions to this deployment impacts everything, it is the + # entrypoint of all network traffic. + checksum/static-config: {{ include "jupyterhub.traefik.yaml" . | fromYaml | merge .Values.proxy.traefik.extraStaticConfig | toYaml | sha256sum }} spec: {{- if .Values.rbac.enabled }} serviceAccountName: autohttps diff --git a/jupyterhub/values.yaml b/jupyterhub/values.yaml index 606645cd61..78edb1e730 100644 --- a/jupyterhub/values.yaml +++ b/jupyterhub/values.yaml @@ -151,9 +151,12 @@ proxy: name: traefik tag: v2.2 # ref: https://hub.docker.com/_/traefik?tab=tags hsts: - maxAge: 15724800 # About 6 months includeSubdomains: false + preload: false + maxAge: 15724800 # About 6 months resources: {} + extraStaticConfig: {} + extraDynamicConfig: {} secretSync: image: name: jupyterhub/k8s-secret-sync @@ -171,7 +174,7 @@ proxy: letsencrypt: contactEmail: '' # Specify custom server here (https://acme-staging-v02.api.letsencrypt.org/directory) to hit staging LE - acmeServer: '' + acmeServer: https://acme-v02.api.letsencrypt.org/directory manual: key: cert: