From 18bb793b2d3cf6e5e4df56eed70577215bb46b25 Mon Sep 17 00:00:00 2001 From: Jennifer Richards Date: Mon, 15 Jul 2024 18:45:51 -0300 Subject: [PATCH] feat: add nginx, robots.txt, HTTP headers (#7683) * feat: nginx + robots.txt * feat: minimal /health/ endpoint * ci: startupProbe for datatracker pod * ci: probe auth pod; set timeoutSeconds * feat: add CSP and other headers to nginx * fix: typo in nginx.conf * feat: split auth/dt nginx confs * test: test health endpoint * ci: auth service on port 80 We'll remove http-old (8080) in the future. * ci: rename auth container/nginx cfg --- ietf/urls.py | 2 ++ ietf/utils/tests.py | 9 ++++++++ k8s/auth.yaml | 43 +++++++++++++++++++++++++++++++++----- k8s/datatracker.yaml | 37 ++++++++++++++++++++++++++++---- k8s/kustomization.yaml | 2 ++ k8s/nginx-auth.conf | 34 ++++++++++++++++++++++++++++++ k8s/nginx-datatracker.conf | 23 ++++++++++++++++++++ 7 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 k8s/nginx-auth.conf create mode 100644 k8s/nginx-datatracker.conf diff --git a/ietf/urls.py b/ietf/urls.py index 78dbb861b2..4b29a3aa81 100644 --- a/ietf/urls.py +++ b/ietf/urls.py @@ -5,6 +5,7 @@ from django.contrib import admin from django.contrib.sitemaps import views as sitemap_views from django.contrib.staticfiles.urls import staticfiles_urlpatterns +from django.http import HttpResponse from django.urls import include, path from django.views import static as static_view from django.views.generic import TemplateView @@ -35,6 +36,7 @@ urlpatterns = [ url(r'^$', views_search.frontpage), + url(r'^health/', lambda _: HttpResponse()), url(r'^accounts/', include('ietf.ietfauth.urls')), url(r'^admin/', admin.site.urls), url(r'^admin/docs/', include('django.contrib.admindocs.urls')), diff --git a/ietf/utils/tests.py b/ietf/utils/tests.py index 08adefc826..476c257a38 100644 --- a/ietf/utils/tests.py +++ b/ietf/utils/tests.py @@ -679,3 +679,12 @@ class TestForm(Form): self.assertTrue(changed_form.has_changed()) unchanged_form = TestForm(initial={'test_field': [1]}, data={'test_field': [1]}) self.assertFalse(unchanged_form.has_changed()) + + +class HealthTests(TestCase): + def test_health(self): + self.assertEqual( + self.client.get("/health/").status_code, + 200, + ) + diff --git a/k8s/auth.yaml b/k8s/auth.yaml index bc99af79d3..66627ed450 100644 --- a/k8s/auth.yaml +++ b/k8s/auth.yaml @@ -24,10 +24,6 @@ spec: - name: auth image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" imagePullPolicy: Always - ports: - - containerPort: 8000 - name: http - protocol: TCP volumeMounts: - name: dt-vol mountPath: /a @@ -49,6 +45,14 @@ spec: envFrom: - secretRef: name: dt-secrets-env + startupProbe: + httpGet: + port: 8000 + path: /health/ + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 30 + timeoutSeconds: 3 securityContext: allowPrivilegeEscalation: false capabilities: @@ -58,6 +62,28 @@ spec: runAsUser: 1000 runAsGroup: 1000 # ----------------------------------------------------- + # Nginx Container + # ----------------------------------------------------- + - name: nginx + image: "ghcr.io/nginxinc/nginx-unprivileged:1.27" + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + protocol: TCP + livenessProbe: + httpGet: + port: 8080 + path: /health/nginx + securityContext: + readOnlyRootFilesystem: true + volumeMounts: + - name: nginx-tmp + mountPath: /tmp + - name: dt-cfg + mountPath: /etc/nginx/conf.d/auth.conf + subPath: nginx-auth.conf + # ----------------------------------------------------- # ScoutAPM Container # ----------------------------------------------------- - name: scoutapm @@ -97,6 +123,9 @@ spec: - name: dt-cfg configMap: name: files-cfgmap + - name: nginx-tmp + emptyDir: + sizeLimit: "500Mi" dnsPolicy: ClusterFirst restartPolicy: Always terminationGracePeriodSeconds: 60 @@ -108,9 +137,13 @@ metadata: spec: type: ClusterIP ports: - - port: 8080 + - port: 80 targetPort: http protocol: TCP name: http + - port: 8080 + targetPort: http + protocol: TCP + name: http-old selector: app: auth diff --git a/k8s/datatracker.yaml b/k8s/datatracker.yaml index 81dc048d0b..59a35fbbc6 100644 --- a/k8s/datatracker.yaml +++ b/k8s/datatracker.yaml @@ -24,10 +24,6 @@ spec: - name: datatracker image: "ghcr.io/ietf-tools/datatracker:$APP_IMAGE_TAG" imagePullPolicy: Always - ports: - - containerPort: 8000 - name: http - protocol: TCP volumeMounts: - name: dt-vol mountPath: /a @@ -49,6 +45,14 @@ spec: envFrom: - secretRef: name: dt-secrets-env + startupProbe: + httpGet: + port: 8000 + path: /health/ + initialDelaySeconds: 10 + periodSeconds: 5 + failureThreshold: 30 + timeoutSeconds: 3 securityContext: allowPrivilegeEscalation: false capabilities: @@ -58,6 +62,28 @@ spec: runAsUser: 1000 runAsGroup: 1000 # ----------------------------------------------------- + # Nginx Container + # ----------------------------------------------------- + - name: nginx + image: "ghcr.io/nginxinc/nginx-unprivileged:1.27" + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + protocol: TCP + livenessProbe: + httpGet: + port: 8080 + path: /health/nginx + securityContext: + readOnlyRootFilesystem: true + volumeMounts: + - name: nginx-tmp + mountPath: /tmp + - name: dt-cfg + mountPath: /etc/nginx/conf.d/datatracker.conf + subPath: nginx-datatracker.conf + # ----------------------------------------------------- # ScoutAPM Container # ----------------------------------------------------- - name: scoutapm @@ -126,6 +152,9 @@ spec: - name: dt-cfg configMap: name: files-cfgmap + - name: nginx-tmp + emptyDir: + sizeLimit: "500Mi" dnsPolicy: ClusterFirst restartPolicy: Always terminationGracePeriodSeconds: 60 diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml index ab381369b2..ba8b8a5826 100644 --- a/k8s/kustomization.yaml +++ b/k8s/kustomization.yaml @@ -3,6 +3,8 @@ namePrefix: dt- configMapGenerator: - name: files-cfgmap files: + - nginx-auth.conf + - nginx-datatracker.conf - settings_local.py resources: - auth.yaml diff --git a/k8s/nginx-auth.conf b/k8s/nginx-auth.conf new file mode 100644 index 0000000000..4cbc8a0a51 --- /dev/null +++ b/k8s/nginx-auth.conf @@ -0,0 +1,34 @@ +server { + listen 8080 default_server; + server_name _; + + # Note that regex location matches take priority over non-regex "prefix" matches. Use regexes so that + # our deny all rule does not squelch the other locations. + location ~ ^/health/nginx$ { + return 200; + } + + location ~ ^/robots.txt$ { + add_header Content-Type text/plain; + return 200 "User-agent: *\nDisallow: /\n"; + } + + location ~ ^/accounts/create.* { + return 302 https://datatracker.ietf.org/accounts/create; + } + + # n.b. (?!...) is a negative lookahead group + location ~ ^(/(?!(api/openid/|accounts/login/|accounts/logout/|accounts/reset/|person/.*/photo|group/groupmenu.json)).*) { + deny all; + } + + location / { + add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' data: https://datatracker.ietf.org/ https://www.ietf.org/ http://ietf.org/ https://analytics.ietf.org https://static.ietf.org; frame-ancestors 'self' ietf.org *.ietf.org meetecho.com *.meetecho.com gather.town *.gather.town"; + proxy_set_header Host $${keepempty}host; + proxy_set_header Connection close; + proxy_set_header X-Request-Start "t=${msec}"; + proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $${keepempty}remote_addr; + proxy_pass http://localhost:8000; + } +} diff --git a/k8s/nginx-datatracker.conf b/k8s/nginx-datatracker.conf new file mode 100644 index 0000000000..63c985463c --- /dev/null +++ b/k8s/nginx-datatracker.conf @@ -0,0 +1,23 @@ +server { + listen 8080 default_server; + server_name _; + + location /health/nginx { + return 200; + } + + location /robots.txt { + add_header Content-Type text/plain; + return 200 "User-agent: *\nDisallow: /doc/pdf/\n"; + } + + location / { + add_header Content-Security-Policy "default-src 'self' 'unsafe-inline' data: https://datatracker.ietf.org/ https://www.ietf.org/ http://ietf.org/ https://analytics.ietf.org https://static.ietf.org; frame-ancestors 'self' ietf.org *.ietf.org meetecho.com *.meetecho.com"; + proxy_set_header Host $${keepempty}host; + proxy_set_header Connection close; + proxy_set_header X-Request-Start "t=${msec}"; + proxy_set_header X-Forwarded-For $${keepempty}proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $${keepempty}remote_addr; + proxy_pass http://localhost:8000; + } +}