From 5feec7f00856b0d5d768526e33ad783ebb53c84d Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Fri, 31 May 2024 16:16:06 +0100 Subject: [PATCH 01/62] WIP --- Dockerfile | 14 +++++--------- README.md | 2 +- compose.yml | 24 +++++++++++++----------- docker-entrypoint.sh | 3 --- nginx/Dockerfile | 4 ++++ nginx/nginx.conf | 14 ++++++++++++++ 6 files changed, 37 insertions(+), 24 deletions(-) delete mode 100755 docker-entrypoint.sh create mode 100644 nginx/Dockerfile create mode 100644 nginx/nginx.conf diff --git a/Dockerfile b/Dockerfile index ca27f4f..a99a9f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,18 +4,14 @@ RUN useradd containeruser WORKDIR /home/containeruser -COPY app app -COPY govuk-frontend-flask.py config.py docker-entrypoint.sh requirements.txt ./ -RUN pip install -r requirements.txt \ - && chmod +x docker-entrypoint.sh \ - && chown -R containeruser:containeruser ./ - # Set environment variables ENV FLASK_APP=govuk-frontend-flask.py \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 -USER containeruser +COPY app app +COPY govuk-frontend-flask.py config.py requirements.txt ./ +RUN pip install -r requirements.txt \ + && chown -R containeruser:containeruser ./ -EXPOSE 9876 -ENTRYPOINT ["./docker-entrypoint.sh"] \ No newline at end of file +USER containeruser \ No newline at end of file diff --git a/README.md b/README.md index 0c71321..9ab28dd 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ python -c 'import secrets; print(secrets.token_hex())' docker compose up --build ``` -You should now have the app running on . Accept the browsers security warning due to the self-signed HTTPS certificate to continue. +You should now have the app running on . Accept the browsers security warning due to the self-signed HTTPS certificate to continue. ## Demos diff --git a/compose.yml b/compose.yml index 8e44d6c..bb1dba0 100644 --- a/compose.yml +++ b/compose.yml @@ -1,28 +1,30 @@ services: web: - container_name: govuk-frontend-flask build: . + command: gunicorn --bind 0.0.0.0:5000 govuk-frontend-flask:app restart: always environment: - CONTACT_EMAIL=[contact email] - CONTACT_PHONE=[contact phone] - DEPARTMENT_NAME=[name of department] - DEPARTMENT_URL=[url of department] - - REDIS_URL=redis://cache:6379 + - REDIS_URL=redis://redis:6379 - SECRET_KEY=4f378500459bb58fecf903ea3c113069f11f150b33388f56fc89f7edce0e6a84 - SERVICE_NAME=[name of service] - SERVICE_PHASE=[phase] - SERVICE_URL=[url of service] - ports: - - "9876:9876" - volumes: - - .:/home/containeruser + expose: + - 5000 depends_on: - - cache - cache: - container_name: redis - image: redis:7.0-alpine + - redis + redis: + image: redis:7-alpine restart: always ports: - 6379:6379 - \ No newline at end of file + nginx: + build: ./nginx + ports: + - 1337:80 + depends_on: + - web diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100755 index 93d14e4..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash -openssl req -new -x509 -newkey rsa:4096 -nodes -out cert.pem -keyout key.pem -days 365 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" -exec gunicorn --reload --certfile cert.pem --keyfile key.pem -b :9876 --access-logfile - --error-logfile - govuk-frontend-flask:app \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..814c5ba --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,4 @@ +FROM nginx:stable + +RUN rm /etc/nginx/conf.d/default.conf +COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf new file mode 100644 index 0000000..d9fd3d2 --- /dev/null +++ b/nginx/nginx.conf @@ -0,0 +1,14 @@ +upstream hello_flask { + server web:5000; +} + +server { + listen 80; + + location / { + proxy_pass http://hello_flask; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_redirect off; + } +} \ No newline at end of file From 18d58560b44a5af915e4652381b4f62f1bde1a42 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 09:30:36 +0100 Subject: [PATCH 02/62] environment config --- README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/README.md b/README.md index 9ab28dd..327ef5d 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,46 @@ To run the tests: python -m pytest --cov=app --cov-report=term-missing --cov-branch ``` +## Environment + +```mermaid +flowchart LR + cache1(Redis):::CACHE + Client + db1[(Postgres)]:::DB + prox1(NGINX):::PROXY + web1(Flask):::WEB + web2[/Static/]:::WEB + + Client <-- https:443 --> prox1 <-- http:5000 --> web1 + prox1 -- Read --> web2 + web1 -- Write --> web2 + web1 <-- postgresql:5432 --> db1 + web1 <-- redis:6379 --> cache1 + + subgraph Proxy + prox1 + end + + subgraph Web + web1 + web2 + end + + subgraph Database + db1 + end + + subgraph Cache + cache1 + end + + classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px + classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px + classDef CACHE fill:#F8CECC,stroke:#B85450,stroke-width:2px + classDef DB fill:#DAE8FC,stroke:#6C8EBF,stroke-width:2px +``` + ## Features Please refer to the specific packages documentation for more details. From f9c9cc41befa6e0b68f3a683159f9ea421f62355 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 10:08:16 +0100 Subject: [PATCH 03/62] WIP --- compose.yml | 2 +- nginx/nginx.conf | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/compose.yml b/compose.yml index bb1dba0..66b9eb0 100644 --- a/compose.yml +++ b/compose.yml @@ -1,7 +1,7 @@ services: web: build: . - command: gunicorn --bind 0.0.0.0:5000 govuk-frontend-flask:app + command: gunicorn --bind localhost:5000 govuk-frontend-flask:app restart: always environment: - CONTACT_EMAIL=[contact email] diff --git a/nginx/nginx.conf b/nginx/nginx.conf index d9fd3d2..a105f0c 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,14 +1,13 @@ -upstream hello_flask { - server web:5000; -} - server { + # listen on port 80 (http) listen 80; + server_name _; location / { - proxy_pass http://hello_flask; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + # forward application requests to the gunicorn server + proxy_pass http://localhost:5000; proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } \ No newline at end of file From e9b64ff9b4aaee505a481f7013325680c1da8761 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 10:39:39 +0100 Subject: [PATCH 04/62] upgrade requirements --- requirements.txt | 2 +- requirements_dev.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 84f6be1..a92b0e6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -86,7 +86,7 @@ redis==5.0.4 # via limits rich==13.7.1 # via flask-limiter -typing-extensions==4.12.0 +typing-extensions==4.12.1 # via # flask-limiter # limits diff --git a/requirements_dev.txt b/requirements_dev.txt index d91d780..7f7bef7 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -16,7 +16,7 @@ black==24.4.2 # via -r requirements_dev.in build==1.2.1 # via pip-tools -certifi==2024.2.2 +certifi==2024.6.2 # via requests cffi==1.16.0 # via cryptography @@ -133,7 +133,7 @@ stevedore==5.2.0 # via bandit typer==0.12.3 # via safety -typing-extensions==4.12.0 +typing-extensions==4.12.1 # via # pydantic # pydantic-core From d1b7db99cd0c86babb1b0387e4c45ae536ee7e65 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 10:42:36 +0100 Subject: [PATCH 05/62] containers --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 327ef5d..e81335e 100644 --- a/README.md +++ b/README.md @@ -84,20 +84,20 @@ flowchart LR web1 <-- postgresql:5432 --> db1 web1 <-- redis:6379 --> cache1 - subgraph Proxy + subgraph Proxy container prox1 end - subgraph Web + subgraph Web container web1 web2 end - subgraph Database + subgraph Database container db1 end - subgraph Cache + subgraph Cache container cache1 end From 1b814d7fc33b7a524d6a79d39e0ac91a5330c72f Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 10:49:03 +0100 Subject: [PATCH 06/62] WIP --- nginx/nginx.conf | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index a105f0c..a73eaef 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,3 +1,7 @@ +upstream app { + server web:5000; +} + server { # listen on port 80 (http) listen 80; @@ -5,7 +9,7 @@ server { location / { # forward application requests to the gunicorn server - proxy_pass http://localhost:5000; + proxy_pass http://app; proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; From c900240f049b9ea96133260f9a34bac5056becab Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 13:09:34 +0100 Subject: [PATCH 07/62] wip --- compose.yml | 10 +++++++--- nginx.conf | 29 +++++++++++++++++++++++++++++ nginx/Dockerfile | 4 ---- nginx/nginx.conf | 17 ----------------- 4 files changed, 36 insertions(+), 24 deletions(-) create mode 100644 nginx.conf delete mode 100644 nginx/Dockerfile delete mode 100644 nginx/nginx.conf diff --git a/compose.yml b/compose.yml index 66b9eb0..d0aa2e4 100644 --- a/compose.yml +++ b/compose.yml @@ -1,7 +1,7 @@ services: web: build: . - command: gunicorn --bind localhost:5000 govuk-frontend-flask:app + command: gunicorn --bind 0.0.0.0:5000 govuk-frontend-flask:app restart: always environment: - CONTACT_EMAIL=[contact email] @@ -23,8 +23,12 @@ services: ports: - 6379:6379 nginx: - build: ./nginx + image: nginx:stable + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf + - ./cert.pem:/root/ssl/cert.pem + - ./key.pem:/root/ssl/key.pem ports: - - 1337:80 + - 443:443 depends_on: - web diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..72d8f9c --- /dev/null +++ b/nginx.conf @@ -0,0 +1,29 @@ +server { + # listen on port 80 (http) + listen 80; + server_name _; + + location / { + # redirect any requests to the same URL but on https + return 301 https://$host$request_uri; + } +} + +server { + # listen on port 443 (https) + listen 443 ssl; + server_name _; + + # location of the self-signed SSL certificate + ssl_certificate /root/ssl/cert.pem; + ssl_certificate_key /root/ssl/key.pem; + + location / { + # forward application requests to the gunicorn server + proxy_pass http://web:5000; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Real-IP $remote_addr; + } +} \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile deleted file mode 100644 index 814c5ba..0000000 --- a/nginx/Dockerfile +++ /dev/null @@ -1,4 +0,0 @@ -FROM nginx:stable - -RUN rm /etc/nginx/conf.d/default.conf -COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/nginx/nginx.conf b/nginx/nginx.conf deleted file mode 100644 index a73eaef..0000000 --- a/nginx/nginx.conf +++ /dev/null @@ -1,17 +0,0 @@ -upstream app { - server web:5000; -} - -server { - # listen on port 80 (http) - listen 80; - server_name _; - - location / { - # forward application requests to the gunicorn server - proxy_pass http://app; - proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - } -} \ No newline at end of file From c8fadf039526a56e7b5cd8fc327ce7fb91a686b5 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 13:21:00 +0100 Subject: [PATCH 08/62] add cert generation --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index e81335e..dcb9e8f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,12 @@ python -c 'import secrets; print(secrets.token_hex())' ./build.sh ``` +### Generate self-signed certificates + +```shell +openssl req -new -x509 -newkey rsa:4096 -nodes -out ./cert.pem -keyout ./key.pem -days 365 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" +``` + ### Run containers ```shell From 38e4246d83e529d8ab14dce04f2d05eb00128a1b Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 14:50:08 +0100 Subject: [PATCH 09/62] shared static assets --- compose.yml | 5 +++++ nginx.conf | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/compose.yml b/compose.yml index d0aa2e4..22b4546 100644 --- a/compose.yml +++ b/compose.yml @@ -13,6 +13,8 @@ services: - SERVICE_NAME=[name of service] - SERVICE_PHASE=[phase] - SERVICE_URL=[url of service] + volumes: + - static_volume:/home/containeruser/app/static expose: - 5000 depends_on: @@ -28,7 +30,10 @@ services: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./cert.pem:/root/ssl/cert.pem - ./key.pem:/root/ssl/key.pem + - static_volume:/home/containeruser/app/static ports: - 443:443 depends_on: - web +volumes: + static_volume: diff --git a/nginx.conf b/nginx.conf index 72d8f9c..c308ab4 100644 --- a/nginx.conf +++ b/nginx.conf @@ -26,4 +26,8 @@ server { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; } + + location /assets/ { + alias /home/containeruser/app/static/; + } } \ No newline at end of file From b1253115c4a00f616e97f58b87ca662976d346e2 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 14:52:53 +0100 Subject: [PATCH 10/62] update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index dcb9e8f..9eeb933 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ openssl req -new -x509 -newkey rsa:4096 -nodes -out ./cert.pem -keyout ./key.pem docker compose up --build ``` -You should now have the app running on . Accept the browsers security warning due to the self-signed HTTPS certificate to continue. +You should now have the app running on . Accept the browsers security warning due to the self-signed HTTPS certificate to continue. ## Demos From 36fdb00001de52bfcfeff09167961242307e82db Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 15:19:08 +0100 Subject: [PATCH 11/62] add cache control and expires header to static assets --- nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nginx.conf b/nginx.conf index c308ab4..8489ba2 100644 --- a/nginx.conf +++ b/nginx.conf @@ -28,6 +28,8 @@ server { } location /assets/ { + # handle static files directly, without forwarding to the application alias /home/containeruser/app/static/; + expires 30d; } } \ No newline at end of file From 47058be50c5993c915f53a8cd92bad0fb43a53a8 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 15:24:41 +0100 Subject: [PATCH 12/62] send gunicorn logs to stdout --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index 22b4546..010885f 100644 --- a/compose.yml +++ b/compose.yml @@ -1,7 +1,7 @@ services: web: build: . - command: gunicorn --bind 0.0.0.0:5000 govuk-frontend-flask:app + command: gunicorn --bind 0.0.0.0:5000 --access-logfile - --error-logfile - govuk-frontend-flask:app restart: always environment: - CONTACT_EMAIL=[contact email] From eef1c294659ba6a58b6d801e00899a60c3c1c3de Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 15:44:00 +0100 Subject: [PATCH 13/62] stop talisman forcing https redirects --- app/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index a67c326..4401bd3 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -95,7 +95,12 @@ def create_app(config_class=Config): compress.init_app(app) csrf.init_app(app) limiter.init_app(app) - talisman.init_app(app, content_security_policy=csp, permissions_policy=permissions_policy) + talisman.init_app( + app, + force_https=False, + content_security_policy=csp, + permissions_policy=permissions_policy, + ) WTFormsHelpers(app) # Create static asset bundles From 363c2a0599ab53d0308cf98107a4c136480441e3 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 16:21:54 +0100 Subject: [PATCH 14/62] set file permissions for static volume --- compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.yml b/compose.yml index 010885f..056d3be 100644 --- a/compose.yml +++ b/compose.yml @@ -14,7 +14,7 @@ services: - SERVICE_PHASE=[phase] - SERVICE_URL=[url of service] volumes: - - static_volume:/home/containeruser/app/static + - static_volume:/home/containeruser/app/static:rw expose: - 5000 depends_on: @@ -30,7 +30,7 @@ services: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./cert.pem:/root/ssl/cert.pem - ./key.pem:/root/ssl/key.pem - - static_volume:/home/containeruser/app/static + - static_volume:/home/containeruser/app/static:ro ports: - 443:443 depends_on: From 9d320c87d0587e5774642a52430ee29910d285bf Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 19:53:06 +0100 Subject: [PATCH 15/62] TB diagram --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9eeb933..26f7cb8 100644 --- a/README.md +++ b/README.md @@ -76,12 +76,12 @@ python -m pytest --cov=app --cov-report=term-missing --cov-branch ## Environment ```mermaid -flowchart LR +flowchart TB cache1(Redis):::CACHE Client db1[(Postgres)]:::DB prox1(NGINX):::PROXY - web1(Flask):::WEB + web1(Flask app):::WEB web2[/Static/]:::WEB Client <-- https:443 --> prox1 <-- http:5000 --> web1 From b842214c1459962d290bf5bbae560fb2b6bcda49 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 21:12:22 +0100 Subject: [PATCH 16/62] gzip static assets --- nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nginx.conf b/nginx.conf index 8489ba2..cd87d05 100644 --- a/nginx.conf +++ b/nginx.conf @@ -31,5 +31,7 @@ server { # handle static files directly, without forwarding to the application alias /home/containeruser/app/static/; expires 30d; + gzip on; + gzip_types text/css text/javascript image/png font/woff2 image/svg+xml; } } \ No newline at end of file From 87f5514e5f1de57b95514bd035763d49f74a52aa Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 21:13:27 +0100 Subject: [PATCH 17/62] reorder --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 26f7cb8..e10b6f7 100644 --- a/README.md +++ b/README.md @@ -107,10 +107,10 @@ flowchart TB cache1 end - classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px - classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px classDef CACHE fill:#F8CECC,stroke:#B85450,stroke-width:2px classDef DB fill:#DAE8FC,stroke:#6C8EBF,stroke-width:2px + classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px + classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px ``` ## Features From 86755c9f7cbb93ba771265b1ecca36e6b9a94c13 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 21:16:39 +0100 Subject: [PATCH 18/62] add comment --- nginx.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nginx.conf b/nginx.conf index cd87d05..3e01e0a 100644 --- a/nginx.conf +++ b/nginx.conf @@ -31,6 +31,8 @@ server { # handle static files directly, without forwarding to the application alias /home/containeruser/app/static/; expires 30d; + + # enable gzip compression of static assets gzip on; gzip_types text/css text/javascript image/png font/woff2 image/svg+xml; } From c1b0d1cc23dcfe591a5c71180066fc292af83b68 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 3 Jun 2024 23:32:24 +0100 Subject: [PATCH 19/62] move to server --- nginx.conf | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nginx.conf b/nginx.conf index 3e01e0a..e9ace4a 100644 --- a/nginx.conf +++ b/nginx.conf @@ -17,6 +17,10 @@ server { # location of the self-signed SSL certificate ssl_certificate /root/ssl/cert.pem; ssl_certificate_key /root/ssl/key.pem; + + # enable gzip compression + gzip on; + gzip_types text/html text/css text/javascript image/png image/svg+xml font/woff2; location / { # forward application requests to the gunicorn server @@ -31,9 +35,5 @@ server { # handle static files directly, without forwarding to the application alias /home/containeruser/app/static/; expires 30d; - - # enable gzip compression of static assets - gzip on; - gzip_types text/css text/javascript image/png font/woff2 image/svg+xml; } } \ No newline at end of file From 8fd8f020aeb7caee7907205e17624b4d945325b9 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 4 Jun 2024 09:24:38 +0100 Subject: [PATCH 20/62] add proxyfix --- app/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/__init__.py b/app/__init__.py index 4401bd3..dd2d130 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -7,6 +7,7 @@ from flask_wtf.csrf import CSRFProtect from govuk_frontend_wtf.main import WTFormsHelpers from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader +from werkzeug.middleware.proxy_fix import ProxyFix from config import Config @@ -33,6 +34,7 @@ def create_app(config_class=Config): ), ] ) + app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) # Set content security policy csp = { From 7d825fb46cb0b4d3556440731723cf622476622f Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 4 Jun 2024 09:24:47 +0100 Subject: [PATCH 21/62] remove duplicate mimetype --- nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index e9ace4a..45bb511 100644 --- a/nginx.conf +++ b/nginx.conf @@ -20,7 +20,7 @@ server { # enable gzip compression gzip on; - gzip_types text/html text/css text/javascript image/png image/svg+xml font/woff2; + gzip_types text/css text/javascript image/png image/svg+xml font/woff2; location / { # forward application requests to the gunicorn server From ce36033a1126adae58e4d77f9db03d5bb8e8ec58 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 4 Jun 2024 10:00:01 +0100 Subject: [PATCH 22/62] 1 year cache length --- nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index 45bb511..c340210 100644 --- a/nginx.conf +++ b/nginx.conf @@ -34,6 +34,6 @@ server { location /assets/ { # handle static files directly, without forwarding to the application alias /home/containeruser/app/static/; - expires 30d; + expires 1y; } } \ No newline at end of file From ed32ff3a7845ddfe72c48683d372d9809b99be8e Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 4 Jun 2024 10:03:45 +0100 Subject: [PATCH 23/62] upgrade requirements --- requirements_dev.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 7f7bef7..7019f71 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -84,17 +84,17 @@ platformdirs==4.2.2 # via black pluggy==1.5.0 # via pytest -pur==7.3.1 +pur==7.3.2 # via -r requirements_dev.in pycodestyle==2.11.1 # via flake8 pycparser==2.22 # via cffi -pydantic==2.7.2 +pydantic==2.7.3 # via # safety # safety-schemas -pydantic-core==2.18.3 +pydantic-core==2.18.4 # via pydantic pyflakes==3.2.0 # via flake8 From 5a8818fce111b3e21df87b7ff970c7eef87300dc Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 4 Jun 2024 10:15:16 +0100 Subject: [PATCH 24/62] remove db --- README.md | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e10b6f7..0cd2877 100644 --- a/README.md +++ b/README.md @@ -79,15 +79,13 @@ python -m pytest --cov=app --cov-report=term-missing --cov-branch flowchart TB cache1(Redis):::CACHE Client - db1[(Postgres)]:::DB prox1(NGINX):::PROXY web1(Flask app):::WEB - web2[/Static/]:::WEB + web2[/Static files/]:::WEB Client <-- https:443 --> prox1 <-- http:5000 --> web1 - prox1 -- Read --> web2 + prox1 -- Read only --> web2 web1 -- Write --> web2 - web1 <-- postgresql:5432 --> db1 web1 <-- redis:6379 --> cache1 subgraph Proxy container @@ -99,16 +97,11 @@ flowchart TB web2 end - subgraph Database container - db1 - end - subgraph Cache container cache1 end classDef CACHE fill:#F8CECC,stroke:#B85450,stroke-width:2px - classDef DB fill:#DAE8FC,stroke:#6C8EBF,stroke-width:2px classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px ``` From 94f7d3dd862bddbd211c28e9859cd0551291626a Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 4 Jun 2024 21:42:24 +0100 Subject: [PATCH 25/62] 4 workers --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index 056d3be..917edf2 100644 --- a/compose.yml +++ b/compose.yml @@ -1,7 +1,7 @@ services: web: build: . - command: gunicorn --bind 0.0.0.0:5000 --access-logfile - --error-logfile - govuk-frontend-flask:app + command: gunicorn --bind 0.0.0.0:5000 -w 4 --access-logfile - --error-logfile - govuk-frontend-flask:app restart: always environment: - CONTACT_EMAIL=[contact email] From f5350fed96b4c8735e0a783551d5968295274459 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 5 Jun 2024 08:44:58 +0100 Subject: [PATCH 26/62] cache control --- nginx.conf | 1 + 1 file changed, 1 insertion(+) diff --git a/nginx.conf b/nginx.conf index c340210..7afa8ad 100644 --- a/nginx.conf +++ b/nginx.conf @@ -21,6 +21,7 @@ server { # enable gzip compression gzip on; gzip_types text/css text/javascript image/png image/svg+xml font/woff2; + gzip_proxied no-cache no-store private expired auth; location / { # forward application requests to the gunicorn server From 11f63fa2e7a17dae76a713898b0e6b2ce1511a31 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 5 Jun 2024 10:12:12 +0100 Subject: [PATCH 27/62] upgrade requirements --- requirements_dev.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 7019f71..7d5de46 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -8,7 +8,7 @@ annotated-types==0.7.0 # via pydantic attrs==23.2.0 # via flake8-bugbear -authlib==1.3.0 +authlib==1.3.1 # via safety bandit==1.7.8 # via -r requirements_dev.in @@ -31,7 +31,7 @@ click==8.1.7 # typer coverage[toml]==7.5.3 # via pytest-cov -cryptography==42.0.7 +cryptography==42.0.8 # via authlib dparse==0.6.4b0 # via @@ -104,7 +104,7 @@ pyproject-hooks==1.1.0 # via # build # pip-tools -pytest==8.2.1 +pytest==8.2.2 # via pytest-cov pytest-cov==5.0.0 # via -r requirements_dev.in @@ -123,7 +123,7 @@ ruamel-yaml==0.18.6 # safety-schemas ruamel-yaml-clib==0.2.8 # via ruamel-yaml -safety==3.2.0 +safety==3.2.1 # via -r requirements_dev.in safety-schemas==0.0.2 # via safety From 779b58b3d833943f1bab454780885b4066f9175a Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 5 Jun 2024 10:43:24 +0100 Subject: [PATCH 28/62] remove flask talisman --- README.md | 6 ++--- app/__init__.py | 64 ------------------------------------------------ config.py | 1 + nginx.conf | 14 +++++++++-- requirements.in | 1 - requirements.txt | 2 -- 6 files changed, 15 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index 0cd2877..c2032fc 100644 --- a/README.md +++ b/README.md @@ -140,9 +140,7 @@ CSRF errors are handled by creating a [flash message](#flash-messages) notificat ### HTTP security headers -Uses [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to set HTTP headers that can help protect against a few common web application security issues. - -- Forces all connections to `https`, unless running with debug enabled. +- Forces all connections to `https`. - Enables [HTTP Strict Transport Security](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security). - Sets Flask's session cookie to `secure`, so it will never be set if your application is somehow accessed via a non-secure connection. - Sets Flask's session cookie to `httponly`, preventing JavaScript from being able to access its content. @@ -153,7 +151,7 @@ Uses [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to ### Content Security Policy -A strict default [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set using [Flask Talisman](https://github.com/GoogleCloudPlatform/flask-talisman) to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application. +A strict default [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application. ### Response compression diff --git a/app/__init__.py b/app/__init__.py index dd2d130..ffaf017 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -3,7 +3,6 @@ from flask_compress import Compress from flask_limiter import Limiter from flask_limiter.util import get_remote_address -from flask_talisman import Talisman from flask_wtf.csrf import CSRFProtect from govuk_frontend_wtf.main import WTFormsHelpers from jinja2 import ChoiceLoader, PackageLoader, PrefixLoader @@ -15,7 +14,6 @@ compress = Compress() csrf = CSRFProtect() limiter = Limiter(get_remote_address, default_limits=["2 per second", "60 per minute"]) -talisman = Talisman() def create_app(config_class=Config): @@ -36,73 +34,11 @@ def create_app(config_class=Config): ) app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) - # Set content security policy - csp = { - "default-src": "'self'", - "script-src": [ - "'self'", - "'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw='", - "'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='", - ], - } - - # Set permissions policy - permissions_policy = { - "accelerometer": "()", - "ambient-light-sensor": "()", - "autoplay": "()", - "battery": "()", - "camera": "()", - "cross-origin-isolated": "()", - "display-capture": "()", - "document-domain": "()", - "encrypted-media": "()", - "execution-while-not-rendered": "()", - "execution-while-out-of-viewport": "()", - "fullscreen": "()", - "geolocation": "()", - "gyroscope": "()", - "keyboard-map": "()", - "magnetometer": "()", - "microphone": "()", - "midi": "()", - "navigation-override": "()", - "payment": "()", - "picture-in-picture": "()", - "publickey-credentials-get": "()", - "screen-wake-lock": "()", - "sync-xhr": "()", - "usb": "()", - "web-share": "()", - "xr-spatial-tracking": "()", - "clipboard-read": "()", - "clipboard-write": "()", - "gamepad": "()", - "speaker-selection": "()", - "conversion-measurement": "()", - "focus-without-user-activation": "()", - "hid": "()", - "idle-detection": "()", - "interest-cohort": "()", - "serial": "()", - "sync-script": "()", - "trust-token-redemption": "()", - "unload": "()", - "window-management": "()", - "vertical-scroll": "()", - } - # Initialise app extensions assets.init_app(app) compress.init_app(app) csrf.init_app(app) limiter.init_app(app) - talisman.init_app( - app, - force_https=False, - content_security_policy=csp, - permissions_policy=permissions_policy, - ) WTFormsHelpers(app) # Create static asset bundles diff --git a/config.py b/config.py index 8d24d82..f2f5388 100644 --- a/config.py +++ b/config.py @@ -13,4 +13,5 @@ class Config(object): SERVICE_PHASE = os.environ.get("SERVICE_PHASE") SERVICE_URL = os.environ.get("SERVICE_URL") SESSION_COOKIE_HTTPONLY = True + SESSION_COOKIE_SAMESITE = "Lax" SESSION_COOKIE_SECURE = True diff --git a/nginx.conf b/nginx.conf index 7afa8ad..c043a4d 100644 --- a/nginx.conf +++ b/nginx.conf @@ -20,9 +20,19 @@ server { # enable gzip compression gzip on; - gzip_types text/css text/javascript image/png image/svg+xml font/woff2; gzip_proxied no-cache no-store private expired auth; - + gzip_types text/css text/javascript image/png image/svg+xml font/woff2 application/json; + + # set security policies + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'" always; + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()"; + + # set security headers + add_header Referrer-Policy "strict-origin-when-cross-origin"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; + add_header X-Content-Type-Options "nosniff"; + add_header X-Frame-Options "SAMEORIGIN"; + location / { # forward application requests to the gunicorn server proxy_pass http://web:5000; diff --git a/requirements.in b/requirements.in index 1a97612..d2c990d 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,6 @@ flask flask-assets flask-compress flask-limiter[redis] -flask-talisman govuk-frontend-jinja govuk-frontend-wtf gunicorn diff --git a/requirements.txt b/requirements.txt index a92b0e6..ed804fe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -34,8 +34,6 @@ flask-compress==1.15 # via -r requirements.in flask-limiter[redis]==3.7.0 # via -r requirements.in -flask-talisman==1.1.0 - # via -r requirements.in flask-wtf==1.2.1 # via govuk-frontend-wtf govuk-frontend-jinja==3.1.0 From 560a80a9109f7170f06454115f75ebb1b5a20693 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 5 Jun 2024 10:58:27 +0100 Subject: [PATCH 29/62] add forwarded protocol --- app/__init__.py | 2 +- nginx.conf | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/__init__.py b/app/__init__.py index ffaf017..7dadf7c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -32,7 +32,7 @@ def create_app(config_class=Config): ), ] ) - app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1) + app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1) # Initialise app extensions assets.init_app(app) diff --git a/nginx.conf b/nginx.conf index c043a4d..bd89ae6 100644 --- a/nginx.conf +++ b/nginx.conf @@ -39,6 +39,7 @@ server { proxy_redirect off; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Real-IP $remote_addr; } From 066ea5aaf9b3b85f156487b1f20273afcf71f9ad Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 5 Jun 2024 10:58:39 +0100 Subject: [PATCH 30/62] use alpine slim version --- compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compose.yml b/compose.yml index 917edf2..5ccac70 100644 --- a/compose.yml +++ b/compose.yml @@ -25,7 +25,7 @@ services: ports: - 6379:6379 nginx: - image: nginx:stable + image: nginx:stable-alpine-slim volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./cert.pem:/root/ssl/cert.pem From cb8b76e9a1a36fb9db2653ed411c2bf4c6a495a2 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 5 Jun 2024 12:10:08 +0100 Subject: [PATCH 31/62] rename user --- Dockerfile | 8 ++++---- compose.yml | 4 ++-- nginx.conf | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index a99a9f5..189b2fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,8 @@ FROM python:3.12-slim -RUN useradd containeruser +RUN useradd appuser -WORKDIR /home/containeruser +WORKDIR /home/appuser # Set environment variables ENV FLASK_APP=govuk-frontend-flask.py \ @@ -12,6 +12,6 @@ ENV FLASK_APP=govuk-frontend-flask.py \ COPY app app COPY govuk-frontend-flask.py config.py requirements.txt ./ RUN pip install -r requirements.txt \ - && chown -R containeruser:containeruser ./ + && chown -R appuser:appuser ./ -USER containeruser \ No newline at end of file +USER appuser \ No newline at end of file diff --git a/compose.yml b/compose.yml index 5ccac70..329b7d7 100644 --- a/compose.yml +++ b/compose.yml @@ -14,7 +14,7 @@ services: - SERVICE_PHASE=[phase] - SERVICE_URL=[url of service] volumes: - - static_volume:/home/containeruser/app/static:rw + - static_volume:/home/appuser/app/static:rw expose: - 5000 depends_on: @@ -30,7 +30,7 @@ services: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./cert.pem:/root/ssl/cert.pem - ./key.pem:/root/ssl/key.pem - - static_volume:/home/containeruser/app/static:ro + - static_volume:/home/appuser/app/static:ro ports: - 443:443 depends_on: diff --git a/nginx.conf b/nginx.conf index bd89ae6..9d5fc54 100644 --- a/nginx.conf +++ b/nginx.conf @@ -45,7 +45,7 @@ server { location /assets/ { # handle static files directly, without forwarding to the application - alias /home/containeruser/app/static/; + alias /home/appuser/app/static/; expires 1y; } } \ No newline at end of file From 89e7ca32e4033240860bc2894cec2e225795e5bb Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 6 Jun 2024 20:25:45 +0100 Subject: [PATCH 32/62] update docs --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c2032fc..56e2871 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ CSS is [minified](https://en.wikipedia.org/wiki/Minification_(programming)) usin ### Cache busting -Merged and compressed assets are browser cache busted on update by modifying their URL with their MD5 hash using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). The MD5 hash is appended to the file name, for example `custom-d41d8cd9.css` instead of a query string, to support certain older browsers and proxies that ignore the querystring in their caching behaviour. +Merged and minified assets are browser cache busted on update by modifying the filename with their MD5 hash using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). The MD5 hash is appended to the file name, for example `custom-d41d8cd9.css` instead of a query string, to support certain older browsers and proxies that ignore the querystring in their caching behaviour. ### Forms generation and validation @@ -145,13 +145,16 @@ CSRF errors are handled by creating a [flash message](#flash-messages) notificat - Sets Flask's session cookie to `secure`, so it will never be set if your application is somehow accessed via a non-secure connection. - Sets Flask's session cookie to `httponly`, preventing JavaScript from being able to access its content. - Sets [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options) to `SAMEORIGIN` to avoid [clickjacking](https://en.wikipedia.org/wiki/Clickjacking). -- Sets [X-XSS-Protection](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection) to enable a cross site scripting filter for IE and Safari (note Chrome has removed this and Firefox never supported it). - Sets [X-Content-Type-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options) to prevent content type sniffing. - Sets a strict [Referrer-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) of `strict-origin-when-cross-origin` that governs which referrer information should be included with requests made. ### Content Security Policy -A strict default [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application. +A strict [Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) (CSP) is set to mitigate [Cross Site Scripting](https://developer.mozilla.org/en-US/docs/Web/Security/Types_of_attacks#cross-site_scripting_xss) (XSS) and packet sniffing attacks. This prevents loading any resources that are not in the same domain as the application by default. + +### Permissions Policy + +A strict [Permissions Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Permissions-Policy) is set to deny the use of browser features by default. ### Response compression From ed2b8e54399db00c59621b80720e13825c287f67 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Fri, 7 Jun 2024 12:40:35 +0100 Subject: [PATCH 33/62] move govuk components into demos --- app/demos/routes.py | 4 ++-- build.sh | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/demos/routes.py b/app/demos/routes.py index cc4a67a..474658e 100644 --- a/app/demos/routes.py +++ b/app/demos/routes.py @@ -10,7 +10,7 @@ @bp.route("/components", methods=["GET"]) def components(): - components = os.listdir("govuk_components") + components = os.listdir("app/demos/govuk_components") components.sort() return render_template("components.html", components=components) @@ -19,7 +19,7 @@ def components(): @bp.route("/components/", methods=["GET"]) def component(component): try: - with open(f"govuk_components/{component}/{component}.yaml") as yaml_file: + with open(f"app/demos/govuk_components/{component}/{component}.yaml") as yaml_file: fixtures = yaml.safe_load(yaml_file) except FileNotFoundError: raise NotFound diff --git a/build.sh b/build.sh index a644e43..b97beac 100755 --- a/build.sh +++ b/build.sh @@ -18,16 +18,16 @@ rm -rf govuk_frontend.zip ##################################################################### # Remove existing GOV.UK Frontend test fixtures -rm -rf govuk_components +rm -rf app/demos/govuk_components # Get new release source code and move to a directory curl -L https://github.com/alphagov/govuk-frontend/archive/refs/tags/v5.4.0.zip > govuk_frontend_source.zip unzip -o govuk_frontend_source.zip -d govuk_frontend_source -mkdir govuk_components -mv govuk_frontend_source/govuk-frontend-5.4.0/packages/govuk-frontend/src/govuk/components/** govuk_components +mkdir app/demos/govuk_components +mv govuk_frontend_source/govuk-frontend-5.4.0/packages/govuk-frontend/src/govuk/components/** app/demos/govuk_components # Remove all files apart from test fixtures -find govuk_components -type f ! -name '*.yaml' -delete +find app/demos/govuk_components -type f ! -name '*.yaml' -delete # Tidy up rm -rf govuk_frontend_source From c12c71d2a65f7b5350a1a71bfca9e5b5686eec16 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 12 Jun 2024 11:45:07 +0100 Subject: [PATCH 34/62] update openssl command and cert outputs --- README.md | 2 +- compose.yml | 2 +- nginx.conf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 56e2871..f173a5d 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ python -c 'import secrets; print(secrets.token_hex())' ### Generate self-signed certificates ```shell -openssl req -new -x509 -newkey rsa:4096 -nodes -out ./cert.pem -keyout ./key.pem -days 365 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" +openssl req -x509 -noenc -newkey rsa:4096 -keyout ./key.pem -out ./req.pem -days 365 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" ``` ### Run containers diff --git a/compose.yml b/compose.yml index 329b7d7..41b27c4 100644 --- a/compose.yml +++ b/compose.yml @@ -28,7 +28,7 @@ services: image: nginx:stable-alpine-slim volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf - - ./cert.pem:/root/ssl/cert.pem + - ./req.pem:/root/ssl/req.pem - ./key.pem:/root/ssl/key.pem - static_volume:/home/appuser/app/static:ro ports: diff --git a/nginx.conf b/nginx.conf index 9d5fc54..d577ac3 100644 --- a/nginx.conf +++ b/nginx.conf @@ -15,7 +15,7 @@ server { server_name _; # location of the self-signed SSL certificate - ssl_certificate /root/ssl/cert.pem; + ssl_certificate /root/ssl/req.pem; ssl_certificate_key /root/ssl/key.pem; # enable gzip compression From b6ba5f482bd831451c158da7c86dbe0a19b2e97a Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 12 Jun 2024 14:53:23 +0100 Subject: [PATCH 35/62] use mozilla guideline ssl config --- README.md | 4 +++- compose.yml | 1 + nginx.conf | 41 ++++++++++++++++++++++++----------------- 3 files changed, 28 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f173a5d..b94c3bc 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,9 @@ python -c 'import secrets; print(secrets.token_hex())' ### Generate self-signed certificates ```shell -openssl req -x509 -noenc -newkey rsa:4096 -keyout ./key.pem -out ./req.pem -days 365 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" +openssl req -x509 -noenc -newkey rsa:2048 -keyout ./key.pem -out ./req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" + +openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048 ``` ### Run containers diff --git a/compose.yml b/compose.yml index 41b27c4..4edcf8c 100644 --- a/compose.yml +++ b/compose.yml @@ -30,6 +30,7 @@ services: - ./nginx.conf:/etc/nginx/conf.d/default.conf - ./req.pem:/root/ssl/req.pem - ./key.pem:/root/ssl/key.pem + - ./dhparam.pem:/root/ssl/dhparam.pem - static_volume:/home/appuser/app/static:ro ports: - 443:443 diff --git a/nginx.conf b/nginx.conf index d577ac3..ea1ecb0 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,38 +1,45 @@ +# generated 2024-06-12, Mozilla Guideline v5.7, nginx 1.26, OpenSSL 3.0.11, intermediate configuration, no OCSP +# https://ssl-config.mozilla.org/#server=nginx&version=1.26&config=intermediate&openssl=3.0.11&ocsp=false&guideline=5.7 server { - # listen on port 80 (http) - listen 80; - server_name _; + listen 80 default_server; + listen [::]:80 default_server; location / { - # redirect any requests to the same URL but on https return 301 https://$host$request_uri; } } server { - # listen on port 443 (https) listen 443 ssl; - server_name _; + listen [::]:443 ssl; + http2 on; - # location of the self-signed SSL certificate ssl_certificate /root/ssl/req.pem; ssl_certificate_key /root/ssl/key.pem; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; # about 40000 sessions + ssl_session_tickets off; + ssl_dhparam /root/ssl/dhparam.pem; + + # intermediate configuration + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; + ssl_prefer_server_ciphers off; + + # add security headers + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'" always; + add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Xss-Protection "1; mode=block" always; # enable gzip compression gzip on; gzip_proxied no-cache no-store private expired auth; gzip_types text/css text/javascript image/png image/svg+xml font/woff2 application/json; - # set security policies - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'" always; - add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()"; - - # set security headers - add_header Referrer-Policy "strict-origin-when-cross-origin"; - add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"; - add_header X-Content-Type-Options "nosniff"; - add_header X-Frame-Options "SAMEORIGIN"; - location / { # forward application requests to the gunicorn server proxy_pass http://web:5000; From 600ca549b4fff03cd4294d062a0c952df11bdfb7 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 12 Jun 2024 23:27:42 +0100 Subject: [PATCH 36/62] correct path --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b94c3bc..4f23860 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ python -c 'import secrets; print(secrets.token_hex())' ```shell openssl req -x509 -noenc -newkey rsa:2048 -keyout ./key.pem -out ./req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" -openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048 +openssl dhparam -out ./dhparam.pem 2048 ``` ### Run containers From 16217a894ac4eb4640eb7374d497c9c087669587 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 12 Jun 2024 23:35:25 +0100 Subject: [PATCH 37/62] upgrade requirements --- requirements_dev.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 7d5de46..cb943af 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -10,7 +10,7 @@ attrs==23.2.0 # via flake8-bugbear authlib==1.3.1 # via safety -bandit==1.7.8 +bandit==1.7.9 # via -r requirements_dev.in black==24.4.2 # via -r requirements_dev.in @@ -55,7 +55,7 @@ markdown-it-py==3.0.0 # via rich markupsafe==2.1.5 # via jinja2 -marshmallow==3.21.2 +marshmallow==3.21.3 # via safety mccabe==0.7.0 # via flake8 @@ -63,7 +63,7 @@ mdurl==0.1.2 # via markdown-it-py mypy-extensions==1.0.0 # via black -packaging==24.0 +packaging==24.1 # via # black # build @@ -90,7 +90,7 @@ pycodestyle==2.11.1 # via flake8 pycparser==2.22 # via cffi -pydantic==2.7.3 +pydantic==2.7.4 # via # safety # safety-schemas @@ -123,7 +123,7 @@ ruamel-yaml==0.18.6 # safety-schemas ruamel-yaml-clib==0.2.8 # via ruamel-yaml -safety==3.2.1 +safety==3.2.3 # via -r requirements_dev.in safety-schemas==0.0.2 # via safety @@ -133,7 +133,7 @@ stevedore==5.2.0 # via bandit typer==0.12.3 # via safety -typing-extensions==4.12.1 +typing-extensions==4.12.2 # via # pydantic # pydantic-core From 80dd7ee75723eaedb880aec9169f407d40ed309a Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 12 Jun 2024 23:39:16 +0100 Subject: [PATCH 38/62] remove flask compress --- app/__init__.py | 3 --- requirements.in | 1 - requirements.txt | 13 +++---------- 3 files changed, 3 insertions(+), 14 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 7dadf7c..96ccf98 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,6 +1,5 @@ from flask import Flask from flask_assets import Bundle, Environment -from flask_compress import Compress from flask_limiter import Limiter from flask_limiter.util import get_remote_address from flask_wtf.csrf import CSRFProtect @@ -11,7 +10,6 @@ from config import Config assets = Environment() -compress = Compress() csrf = CSRFProtect() limiter = Limiter(get_remote_address, default_limits=["2 per second", "60 per minute"]) @@ -36,7 +34,6 @@ def create_app(config_class=Config): # Initialise app extensions assets.init_app(app) - compress.init_app(app) csrf.init_app(app) limiter.init_app(app) WTFormsHelpers(app) diff --git a/requirements.in b/requirements.in index d2c990d..52cdcd7 100644 --- a/requirements.in +++ b/requirements.in @@ -2,7 +2,6 @@ cssmin email_validator flask flask-assets -flask-compress flask-limiter[redis] govuk-frontend-jinja govuk-frontend-wtf diff --git a/requirements.txt b/requirements.txt index ed804fe..06fde0e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,6 @@ # blinker==1.8.2 # via flask -brotli==1.1.0 - # via flask-compress click==8.1.7 # via flask cssmin==0.2.0 @@ -24,14 +22,11 @@ flask==3.0.3 # via # -r requirements.in # flask-assets - # flask-compress # flask-limiter # flask-wtf # govuk-frontend-wtf flask-assets==2.1.0 # via -r requirements.in -flask-compress==1.15 - # via -r requirements.in flask-limiter[redis]==3.7.0 # via -r requirements.in flask-wtf==1.2.1 @@ -72,7 +67,7 @@ mdurl==0.1.2 # via markdown-it-py ordered-set==4.1.0 # via flask-limiter -packaging==24.0 +packaging==24.1 # via # gunicorn # limits @@ -80,11 +75,11 @@ pygments==2.18.0 # via rich pyyaml==6.0.1 # via -r requirements.in -redis==5.0.4 +redis==5.0.5 # via limits rich==13.7.1 # via flask-limiter -typing-extensions==4.12.1 +typing-extensions==4.12.2 # via # flask-limiter # limits @@ -98,5 +93,3 @@ wtforms==3.1.2 # via # flask-wtf # govuk-frontend-wtf -zstandard==0.22.0 - # via flask-compress From 688858cabe6e91ab6cbbb89f62b418f520406af1 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 13 Jun 2024 08:19:28 +0100 Subject: [PATCH 39/62] update x frame options header --- nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx.conf b/nginx.conf index ea1ecb0..bd501b2 100644 --- a/nginx.conf +++ b/nginx.conf @@ -32,7 +32,7 @@ server { add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; - add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Frame-Options "DENY" always; add_header X-Xss-Protection "1; mode=block" always; # enable gzip compression From 53ab869e889da3847b4953555c92e3daa0eef5c4 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 13 Jun 2024 09:13:40 +0100 Subject: [PATCH 40/62] move cert generation into dockerfile --- README.md | 10 +--------- compose.yml | 6 +----- nginx/Dockerfile | 8 ++++++++ nginx.conf => nginx/nginx.conf | 6 +++--- 4 files changed, 13 insertions(+), 17 deletions(-) create mode 100644 nginx/Dockerfile rename nginx.conf => nginx/nginx.conf (95%) diff --git a/README.md b/README.md index 4f23860..e81ee11 100644 --- a/README.md +++ b/README.md @@ -47,14 +47,6 @@ python -c 'import secrets; print(secrets.token_hex())' ./build.sh ``` -### Generate self-signed certificates - -```shell -openssl req -x509 -noenc -newkey rsa:2048 -keyout ./key.pem -out ./req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" - -openssl dhparam -out ./dhparam.pem 2048 -``` - ### Run containers ```shell @@ -114,7 +106,7 @@ Please refer to the specific packages documentation for more details. ### Asset management -Custom CSS and JavaScript files are merged and compressed using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). This takes all `*.css` files in `app/static/src/css` and all `*.js` files in `app/static/src/js` and outputs a single compressed file to both `app/static/dist/css` and `app/static/dist/js` respectively. +Custom CSS and JavaScript files are merged and minified using [Flask Assets](https://flask-assets.readthedocs.io/en/latest/) and [Webassets](https://webassets.readthedocs.io/en/latest/). This takes all `*.css` files in `app/static/src/css` and all `*.js` files in `app/static/src/js` and outputs a single minified file to both `app/static/dist/css` and `app/static/dist/js` respectively. CSS is [minified](https://en.wikipedia.org/wiki/Minification_(programming)) using [CSSMin](https://github.com/zacharyvoase/cssmin) and JavaScript is minified using [JSMin](https://github.com/tikitu/jsmin/). This removes all whitespace characters, comments and line breaks to reduce the size of the source code, making its transmission over a network more efficient. diff --git a/compose.yml b/compose.yml index 4edcf8c..f443151 100644 --- a/compose.yml +++ b/compose.yml @@ -25,12 +25,8 @@ services: ports: - 6379:6379 nginx: - image: nginx:stable-alpine-slim + build: ./nginx volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf - - ./req.pem:/root/ssl/req.pem - - ./key.pem:/root/ssl/key.pem - - ./dhparam.pem:/root/ssl/dhparam.pem - static_volume:/home/appuser/app/static:ro ports: - 443:443 diff --git a/nginx/Dockerfile b/nginx/Dockerfile new file mode 100644 index 0000000..81e12e6 --- /dev/null +++ b/nginx/Dockerfile @@ -0,0 +1,8 @@ +FROM nginx:stable + +RUN rm /etc/nginx/conf.d/default.conf && \ + mkdir /etc/nginx/ssl && \ + openssl req -x509 -noenc -newkey rsa:2048 -keyout /etc/nginx/ssl/key.pem -out /etc/nginx/ssl/req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" && \ + openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048 + +COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file diff --git a/nginx.conf b/nginx/nginx.conf similarity index 95% rename from nginx.conf rename to nginx/nginx.conf index bd501b2..c4b7415 100644 --- a/nginx.conf +++ b/nginx/nginx.conf @@ -14,12 +14,12 @@ server { listen [::]:443 ssl; http2 on; - ssl_certificate /root/ssl/req.pem; - ssl_certificate_key /root/ssl/key.pem; + ssl_certificate /etc/nginx/ssl/req.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; - ssl_dhparam /root/ssl/dhparam.pem; + ssl_dhparam /etc/nginx/ssl/dhparam.pem; # intermediate configuration ssl_protocols TLSv1.2 TLSv1.3; From a0aa0afcafc9b42bbb1c4e00119ad23e2999e51c Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 13 Jun 2024 10:26:02 +0100 Subject: [PATCH 41/62] remove absent field check --- app/demos/forms.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/app/demos/forms.py b/app/demos/forms.py index 6059857..0594fc7 100644 --- a/app/demos/forms.py +++ b/app/demos/forms.py @@ -268,10 +268,6 @@ class KitchenSinkForm(FlaskForm): widget=GovPasswordInput(), validators=[ InputRequired("Password is required"), - EqualTo( - "password_retype_field", - message="Please ensure both password fields match", - ), ], description="PasswordField rendered using a GovPasswordInput widget.", ) From e257f7ab4ddd5a8b0e287aa6643a8c002f1b1702 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 13 Jun 2024 10:26:12 +0100 Subject: [PATCH 42/62] add base-uri to csp --- nginx/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index c4b7415..4688264 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -27,7 +27,7 @@ server { ssl_prefer_server_ciphers off; # add security headers - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'" always; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'; base-uri 'self'" always; add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; From 06c17fa3920d9662d1d85f4bf832653266f9fcc9 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 13 Jun 2024 21:55:36 +0100 Subject: [PATCH 43/62] modern config --- nginx/nginx.conf | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 4688264..5dac187 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,5 +1,5 @@ -# generated 2024-06-12, Mozilla Guideline v5.7, nginx 1.26, OpenSSL 3.0.11, intermediate configuration, no OCSP -# https://ssl-config.mozilla.org/#server=nginx&version=1.26&config=intermediate&openssl=3.0.11&ocsp=false&guideline=5.7 +# generated 2024-06-13, Mozilla Guideline v5.7, nginx 1.26.1, OpenSSL 3.0.11, modern configuration, no OCSP +# https://ssl-config.mozilla.org/#server=nginx&version=1.26.1&config=modern&openssl=3.0.11&ocsp=false&guideline=5.7 server { listen 80 default_server; listen [::]:80 default_server; @@ -19,11 +19,9 @@ server { ssl_session_timeout 1d; ssl_session_cache shared:MozSSL:10m; # about 40000 sessions ssl_session_tickets off; - ssl_dhparam /etc/nginx/ssl/dhparam.pem; - # intermediate configuration - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305; + # modern configuration + ssl_protocols TLSv1.3; ssl_prefer_server_ciphers off; # add security headers From 4040adf1182a585b28131a00a784994205b2d9d2 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 13 Jun 2024 21:55:49 +0100 Subject: [PATCH 44/62] remove dhparam --- nginx/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index 81e12e6..ae604bf 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -2,7 +2,6 @@ FROM nginx:stable RUN rm /etc/nginx/conf.d/default.conf && \ mkdir /etc/nginx/ssl && \ - openssl req -x509 -noenc -newkey rsa:2048 -keyout /etc/nginx/ssl/key.pem -out /etc/nginx/ssl/req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" && \ - openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048 + openssl req -x509 -noenc -newkey rsa:2048 -keyout /etc/nginx/ssl/key.pem -out /etc/nginx/ssl/req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file From ebaff80a854d78e57fc64148ff52c2b66a7d4fb6 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Fri, 14 Jun 2024 10:04:05 +0100 Subject: [PATCH 45/62] 10 year expires header --- nginx/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 5dac187..71f00e3 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -51,6 +51,6 @@ server { location /assets/ { # handle static files directly, without forwarding to the application alias /home/appuser/app/static/; - expires 1y; + expires 10y; } } \ No newline at end of file From d01c3cb671d6070f14bbc91a174906b4fdf77a3c Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Fri, 14 Jun 2024 18:32:31 +0100 Subject: [PATCH 46/62] increase bullet spacing --- app/templates/demos/component.html | 2 +- app/templates/demos/components.html | 2 +- app/templates/demos/forms.html | 2 +- app/templates/main/index.html | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/templates/demos/component.html b/app/templates/demos/component.html index 61ca9b7..6879a47 100644 --- a/app/templates/demos/component.html +++ b/app/templates/demos/component.html @@ -65,7 +65,7 @@

{{component | replace("-", " ") | capitalize}}

Examples:

-
    +
      {% for fixture in fixtures.examples if not fixture.hidden %}
    • {{fixture.name | capitalize}} diff --git a/app/templates/demos/components.html b/app/templates/demos/components.html index 9841936..8b8a8e0 100644 --- a/app/templates/demos/components.html +++ b/app/templates/demos/components.html @@ -23,7 +23,7 @@
      Demo

      Components

      -
        +
          {% for component in components %}
        • {{component | replace("-", " ") | capitalize}} diff --git a/app/templates/demos/forms.html b/app/templates/demos/forms.html index 29b1b98..62d4ec9 100644 --- a/app/templates/demos/forms.html +++ b/app/templates/demos/forms.html @@ -25,7 +25,7 @@ {{ super() }} Demo

          Forms

          -
            +
            • Autocomplete
            • Bank details
            • Conditional reveal
            • diff --git a/app/templates/main/index.html b/app/templates/main/index.html index 2fab0bf..dc6483c 100644 --- a/app/templates/main/index.html +++ b/app/templates/main/index.html @@ -14,7 +14,7 @@

              Hello, World!

              get a new project started quicker.

              It is also the reference implementation of two core packages:

              -
                +
                • GOV.UK Frontend Jinja which provides Jinja macros of GOV.UK components
                • GOV.UK Frontend WTForms @@ -26,7 +26,7 @@

                  Hello, World!

                  Features

                  A number of other packages are used to provide the features listed below with sensible and best-practice defaults:

                  -
                    +
                    • Asset management
                    • Cache busting
                    • Form generation and validation
                    • @@ -40,7 +40,7 @@

                      Features

                    Demos

                    -
                      + From 670a25ad18fb8314378f30769b42cc548c01a303 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Fri, 14 Jun 2024 18:33:43 +0100 Subject: [PATCH 47/62] update csp --- nginx/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 71f00e3..8f14d58 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -25,7 +25,7 @@ server { ssl_prefer_server_ciphers off; # add security headers - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'; base-uri 'self'" always; + add_header Content-Security-Policy "script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'; base-uri 'none'" always; add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; From 46d372d279f1a96aada52d74b175776414f79449 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 18 Jun 2024 17:06:23 +0100 Subject: [PATCH 48/62] gzip more mimetypes --- nginx/nginx.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 8f14d58..6f2a0c5 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -36,7 +36,7 @@ server { # enable gzip compression gzip on; gzip_proxied no-cache no-store private expired auth; - gzip_types text/css text/javascript image/png image/svg+xml font/woff2 application/json; + gzip_types application/javascript application/json font/otf font/ttf font/woff font/woff2 image/gif image/jpeg image/png image/svg+xml image/webp text/css text/javascript; location / { # forward application requests to the gunicorn server From f2e993eedee0d6f5ab3ed774d70a1b9598ba820c Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 18 Jun 2024 23:21:32 +0100 Subject: [PATCH 49/62] upgrade components --- requirements.txt | 4 ++-- requirements_dev.txt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements.txt b/requirements.txt index 06fde0e..fe541e2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ deprecated==1.2.14 # via limits dnspython==2.6.1 # via email-validator -email-validator==2.1.1 +email-validator==2.1.2 # via -r requirements.in flask==3.0.3 # via @@ -75,7 +75,7 @@ pygments==2.18.0 # via rich pyyaml==6.0.1 # via -r requirements.in -redis==5.0.5 +redis==5.0.6 # via limits rich==13.7.1 # via flask-limiter diff --git a/requirements_dev.txt b/requirements_dev.txt index cb943af..97daf79 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -37,7 +37,7 @@ dparse==0.6.4b0 # via # safety # safety-schemas -flake8==7.0.0 +flake8==7.1.0 # via # flake8-bugbear # pep8-naming @@ -86,7 +86,7 @@ pluggy==1.5.0 # via pytest pur==7.3.2 # via -r requirements_dev.in -pycodestyle==2.11.1 +pycodestyle==2.12.0 # via flake8 pycparser==2.22 # via cffi @@ -140,7 +140,7 @@ typing-extensions==4.12.2 # safety # safety-schemas # typer -urllib3==2.2.1 +urllib3==2.2.2 # via # requests # safety From 5177b4c48153a8ecd350e842bb22fbd137aede73 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 19 Jun 2024 10:11:06 +0100 Subject: [PATCH 50/62] more gzip tuning --- nginx/nginx.conf | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 6f2a0c5..d376399 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -35,8 +35,9 @@ server { # enable gzip compression gzip on; - gzip_proxied no-cache no-store private expired auth; - gzip_types application/javascript application/json font/otf font/ttf font/woff font/woff2 image/gif image/jpeg image/png image/svg+xml image/webp text/css text/javascript; + gzip_comp_level 6; + gzip_proxied any; + gzip_types application/javascript application/json application/xml font/otf font/ttf font/woff font/woff2 image/gif image/jpeg image/png image/svg+xml image/webp text/css text/csv text/javascript text/xml; location / { # forward application requests to the gunicorn server From 63a99fe9730e0fe5590de24e70862183070eb01e Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 24 Jun 2024 08:55:46 +0100 Subject: [PATCH 51/62] upgrade requirements --- requirements.txt | 4 ++-- requirements_dev.txt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index fe541e2..b688fcd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,7 +16,7 @@ deprecated==1.2.14 # via limits dnspython==2.6.1 # via email-validator -email-validator==2.1.2 +email-validator==2.2.0 # via -r requirements.in flask==3.0.3 # via @@ -54,7 +54,7 @@ jinja2==3.1.4 # govuk-frontend-wtf jsmin==3.0.1 # via -r requirements.in -limits[redis]==3.12.0 +limits[redis]==3.13.0 # via flask-limiter markdown-it-py==3.0.0 # via rich diff --git a/requirements_dev.txt b/requirements_dev.txt index 97daf79..ccb874c 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -29,7 +29,7 @@ click==8.1.7 # pur # safety # typer -coverage[toml]==7.5.3 +coverage[toml]==7.5.4 # via pytest-cov cryptography==42.0.8 # via authlib From 31097650f44cbb7f320efee411b9c1662c8c73d9 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Mon, 24 Jun 2024 08:56:15 +0100 Subject: [PATCH 52/62] add sec headers, tweak static serving --- nginx/nginx.conf | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index d376399..91eab39 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -25,7 +25,10 @@ server { ssl_prefer_server_ciphers off; # add security headers - add_header Content-Security-Policy "script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'; base-uri 'none'" always; + add_header Content-Security-Policy "script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'; base-uri 'none';" always; + add_header Cross-Origin-Embedder-Policy "require-corp" always; + add_header Cross-Origin-Opener-Policy "same-origin" always; + add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Permissions-Policy "accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always; @@ -50,8 +53,13 @@ server { } location /assets/ { - # handle static files directly, without forwarding to the application + # serve static files directly, without forwarding to the application alias /home/appuser/app/static/; + + sendfile on; + tcp_nopush on; + + # set far future expires header expires 10y; } } \ No newline at end of file From 2bc6aa43caa0710f0938aaecfc29d03d5fc619df Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 26 Jun 2024 10:09:10 +0100 Subject: [PATCH 53/62] dont need to map to host port --- compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.yml b/compose.yml index f443151..1a7323f 100644 --- a/compose.yml +++ b/compose.yml @@ -22,8 +22,8 @@ services: redis: image: redis:7-alpine restart: always - ports: - - 6379:6379 + expose: + - 6379 nginx: build: ./nginx volumes: From 8953c90b569a535af64435976bc9c837c0b89e79 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 31 Jul 2024 09:53:51 +0100 Subject: [PATCH 54/62] upgrade requirements --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index b688fcd..724d6b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ flask==3.0.3 # govuk-frontend-wtf flask-assets==2.1.0 # via -r requirements.in -flask-limiter[redis]==3.7.0 +flask-limiter[redis]==3.8.0 # via -r requirements.in flask-wtf==1.2.1 # via govuk-frontend-wtf @@ -75,7 +75,7 @@ pygments==2.18.0 # via rich pyyaml==6.0.1 # via -r requirements.in -redis==5.0.6 +redis==5.0.8 # via limits rich==13.7.1 # via flask-limiter From f4dbf3e3de85569c0b544e5c49d7472560323508 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 31 Jul 2024 09:54:07 +0100 Subject: [PATCH 55/62] upgrade dev reqs, remove safety --- .github/workflows/python-app.yml | 2 +- requirements_dev.in | 3 +- requirements_dev.txt | 116 ++++++++++++++----------------- 3 files changed, 57 insertions(+), 64 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index de80497..9ef8f51 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -27,7 +27,7 @@ jobs: pip install -r requirements_dev.txt pip install -r requirements.txt - name: Check dependencies for known security vulnerabilities - run: safety check -r requirements.txt + run: pip-audit -r requirements.txt - name: Check code for potential security vulnerabilities run: bandit -r . -x /tests - name: Check code formatting diff --git a/requirements_dev.in b/requirements_dev.in index a9fc1b4..70d88cb 100644 --- a/requirements_dev.in +++ b/requirements_dev.in @@ -2,8 +2,9 @@ bandit black flake8-bugbear isort +mypy pep8-naming +pip-audit pip-tools pur pytest-cov -safety diff --git a/requirements_dev.txt b/requirements_dev.txt index ccb874c..9a8a98e 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,22 +4,22 @@ # # pip-compile requirements_dev.in # -annotated-types==0.7.0 - # via pydantic attrs==23.2.0 # via flake8-bugbear -authlib==1.3.1 - # via safety bandit==1.7.9 # via -r requirements_dev.in black==24.4.2 # via -r requirements_dev.in +boolean-py==4.0 + # via license-expression build==1.2.1 # via pip-tools -certifi==2024.6.2 +cachecontrol[filecache]==0.14.0 + # via + # cachecontrol + # pip-audit +certifi==2024.7.4 # via requests -cffi==1.16.0 - # via cryptography charset-normalizer==3.3.2 # via requests click==8.1.7 @@ -27,57 +27,65 @@ click==8.1.7 # black # pip-tools # pur - # safety - # typer -coverage[toml]==7.5.4 +coverage[toml]==7.6.0 # via pytest-cov -cryptography==42.0.8 - # via authlib -dparse==0.6.4b0 - # via - # safety - # safety-schemas +cyclonedx-python-lib==7.5.1 + # via pip-audit +defusedxml==0.7.1 + # via py-serializable +filelock==3.15.4 + # via cachecontrol flake8==7.1.0 # via # flake8-bugbear # pep8-naming flake8-bugbear==24.4.26 # via -r requirements_dev.in +html5lib==1.1 + # via pip-audit idna==3.7 # via requests iniconfig==2.0.0 # via pytest isort==5.13.2 # via -r requirements_dev.in -jinja2==3.1.4 - # via safety +license-expression==30.3.0 + # via cyclonedx-python-lib markdown-it-py==3.0.0 # via rich -markupsafe==2.1.5 - # via jinja2 -marshmallow==3.21.3 - # via safety mccabe==0.7.0 # via flake8 mdurl==0.1.2 # via markdown-it-py +msgpack==1.0.8 + # via cachecontrol +mypy==1.11.1 + # via -r requirements_dev.in mypy-extensions==1.0.0 - # via black + # via + # black + # mypy +packageurl-python==0.15.6 + # via cyclonedx-python-lib packaging==24.1 # via # black # build - # dparse - # marshmallow + # pip-audit + # pip-requirements-parser # pytest - # safety - # safety-schemas pathspec==0.12.1 # via black pbr==6.0.0 # via stevedore pep8-naming==0.14.1 # via -r requirements_dev.in +pip-api==0.0.34 + # via pip-audit +pip-audit==2.7.3 + # via -r requirements_dev.in +pip-requirements-parser==32.0.1 + # via pip-audit pip-tools==7.4.1 # via -r requirements_dev.in platformdirs==4.2.2 @@ -86,64 +94,48 @@ pluggy==1.5.0 # via pytest pur==7.3.2 # via -r requirements_dev.in +py-serializable==1.1.0 + # via cyclonedx-python-lib pycodestyle==2.12.0 # via flake8 -pycparser==2.22 - # via cffi -pydantic==2.7.4 - # via - # safety - # safety-schemas -pydantic-core==2.18.4 - # via pydantic pyflakes==3.2.0 # via flake8 pygments==2.18.0 # via rich +pyparsing==3.1.2 + # via pip-requirements-parser pyproject-hooks==1.1.0 # via # build # pip-tools -pytest==8.2.2 +pytest==8.3.2 # via pytest-cov pytest-cov==5.0.0 # via -r requirements_dev.in pyyaml==6.0.1 # via bandit requests==2.32.3 - # via safety + # via + # cachecontrol + # pip-audit rich==13.7.1 # via # bandit - # safety - # typer -ruamel-yaml==0.18.6 - # via - # safety - # safety-schemas -ruamel-yaml-clib==0.2.8 - # via ruamel-yaml -safety==3.2.3 - # via -r requirements_dev.in -safety-schemas==0.0.2 - # via safety -shellingham==1.5.4 - # via typer + # pip-audit +six==1.16.0 + # via html5lib +sortedcontainers==2.4.0 + # via cyclonedx-python-lib stevedore==5.2.0 # via bandit -typer==0.12.3 - # via safety +toml==0.10.2 + # via pip-audit typing-extensions==4.12.2 - # via - # pydantic - # pydantic-core - # safety - # safety-schemas - # typer + # via mypy urllib3==2.2.2 - # via - # requests - # safety + # via requests +webencodings==0.5.1 + # via html5lib wheel==0.43.0 # via pip-tools From b06de8b26afc9fbbbea3ed33cbc108da9c7f1900 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Wed, 31 Jul 2024 09:54:13 +0100 Subject: [PATCH 56/62] formatting --- app/__init__.py | 17 +++++++++--- app/demos/custom_validators.py | 8 +++++- app/demos/forms.py | 48 +++++++++++++++++++++++++++------- app/demos/routes.py | 6 ++++- app/main/routes.py | 6 ++++- 5 files changed, 70 insertions(+), 15 deletions(-) diff --git a/app/__init__.py b/app/__init__.py index 96ccf98..7d75258 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -11,7 +11,10 @@ assets = Environment() csrf = CSRFProtect() -limiter = Limiter(get_remote_address, default_limits=["2 per second", "60 per minute"]) +limiter = Limiter( + get_remote_address, + default_limits=["2 per second", "60 per minute"], +) def create_app(config_class=Config): @@ -39,8 +42,16 @@ def create_app(config_class=Config): WTFormsHelpers(app) # Create static asset bundles - css = Bundle("src/css/*.css", filters="cssmin", output="dist/css/custom-%(version)s.min.css") - js = Bundle("src/js/*.js", filters="jsmin", output="dist/js/custom-%(version)s.min.js") + css = Bundle( + "src/css/*.css", + filters="cssmin", + output="dist/css/custom-%(version)s.min.css", + ) + js = Bundle( + "src/js/*.js", + filters="jsmin", + output="dist/js/custom-%(version)s.min.js", + ) if "css" not in assets: assets.register("css", css) if "js" not in assets: diff --git a/app/demos/custom_validators.py b/app/demos/custom_validators.py index c05667e..08390b1 100644 --- a/app/demos/custom_validators.py +++ b/app/demos/custom_validators.py @@ -2,7 +2,13 @@ class RequiredIf(InputRequired): - def __init__(self, other_field_name, other_field_value, *args, **kwargs): + def __init__( + self, + other_field_name, + other_field_value, + *args, + **kwargs, + ): self.other_field_name = other_field_name self.other_field_value = other_field_value diff --git a/app/demos/forms.py b/app/demos/forms.py index 0594fc7..9448ff5 100644 --- a/app/demos/forms.py +++ b/app/demos/forms.py @@ -45,7 +45,10 @@ class BankDetailsForm(FlaskForm): widget=GovTextInput(), validators=[ InputRequired(message="Enter a sort code"), - Regexp(regex=r"\d{6}", message="Enter a valid sort code like 309430"), + Regexp( + regex=r"\d{6}", + message="Enter a valid sort code like 309430", + ), ], description="Must be 6 digits long", ) @@ -54,8 +57,15 @@ class BankDetailsForm(FlaskForm): widget=GovTextInput(), validators=[ InputRequired(message="Enter an account number"), - Regexp(regex=r"\d{6,8}", message="Enter a valid account number like 00733445"), - Length(min=6, max=8, message="Account number must be between 6 and 8 digits"), + Regexp( + regex=r"\d{6,8}", + message="Enter a valid account number like 00733445", + ), + Length( + min=6, + max=8, + message="Account number must be between 6 and 8 digits", + ), ], description="Must be between 6 and 8 digits long", ) @@ -118,7 +128,10 @@ class CreateAccountForm(FlaskForm): widget=GovTextInput(), validators=[ InputRequired(message="Enter an email address"), - Length(max=256, message="Email address must be 256 characters or fewer"), + Length( + max=256, + message="Email address must be 256 characters or fewer", + ), Email(message="Enter an email address in the correct format, like name@example.com"), ], description="You'll need this email address to sign in to your account", @@ -139,7 +152,10 @@ class CreateAccountForm(FlaskForm): widget=GovPasswordInput(), validators=[ InputRequired(message="Enter a password"), - Length(min=8, message="Password must be at least 8 characters"), + Length( + min=8, + message="Password must be at least 8 characters", + ), ], description="Must be at least 8 characters", ) @@ -170,7 +186,10 @@ class KitchenSinkForm(FlaskForm): email_field = StringField( "EmailField", widget=GovTextInput(), - validators=[InputRequired(message="EmailField is required"), Email()], + validators=[ + InputRequired(message="EmailField is required"), + Email(), + ], description="StringField rendered using a GovTextInput widget.", ) @@ -207,7 +226,10 @@ class KitchenSinkForm(FlaskForm): widget=GovCharacterCount(), validators=[ InputRequired(message="CharacterCountField is required"), - Length(max=200, message="CharacterCountField must be 200 characters or fewer "), + Length( + max=200, + message="CharacterCountField must be 200 characters or fewer ", + ), ], description="TextAreaField rendered using a GovCharacterCount widget.", ) @@ -237,7 +259,11 @@ class KitchenSinkForm(FlaskForm): "SelectMultipleField", widget=GovCheckboxesInput(), validators=[InputRequired(message="Please select an option")], - choices=[("one", "One"), ("two", "Two"), ("three", "Three")], + choices=[ + ("one", "One"), + ("two", "Two"), + ("three", "Three"), + ], description="SelectMultipleField rendered using a GovCheckboxesInput widget.", ) @@ -245,7 +271,11 @@ class KitchenSinkForm(FlaskForm): "RadioField", widget=GovRadioInput(), validators=[InputRequired(message="Please select an option")], - choices=[("one", "One"), ("two", "Two"), ("three", "Three")], + choices=[ + ("one", "One"), + ("two", "Two"), + ("three", "Three"), + ], description="RadioField rendered using a GovRadioInput widget.", ) diff --git a/app/demos/routes.py b/app/demos/routes.py index 474658e..8ff30a6 100644 --- a/app/demos/routes.py +++ b/app/demos/routes.py @@ -24,7 +24,11 @@ def component(component): except FileNotFoundError: raise NotFound - return render_template("component.html", component=component, fixtures=fixtures) + return render_template( + "component.html", + component=component, + fixtures=fixtures, + ) @bp.route("/forms", methods=["GET"]) diff --git a/app/main/routes.py b/app/main/routes.py index 70bee6d..ccf9f40 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -34,7 +34,11 @@ def cookies(): response = make_response(render_template("cookies.html", form=form)) # Set cookies policy for one year - response.set_cookie("cookies_policy", json.dumps(cookies_policy), max_age=31557600) + response.set_cookie( + "cookies_policy", + json.dumps(cookies_policy), + max_age=31557600, + ) return response elif request.method == "GET": if request.cookies.get("cookies_policy"): From 5aa793aba5d1c3265b0771f03e1474d65675d44b Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Thu, 29 Aug 2024 15:59:41 +0100 Subject: [PATCH 57/62] govuk-frontend v5.6.0 --- README.md | 2 +- app/templates/base.html | 10 +++++----- app/templates/demos/component.html | 3 +++ build.sh | 6 +++--- requirements.txt | 14 ++++++------- requirements_dev.txt | 32 +++++++++++++++--------------- runtime.txt | 2 +- 7 files changed, 36 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e81ee11..b3f9a57 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # GOV.UK Frontend Flask -![govuk-frontend 5.4.0](https://img.shields.io/badge/govuk--frontend%20version-5.4.0-005EA5?logo=gov.uk&style=flat) +![govuk-frontend 5.6.0](https://img.shields.io/badge/govuk--frontend%20version-5.6.0-005EA5?logo=gov.uk&style=flat) **GOV.UK Frontend Flask is a [community tool](https://design-system.service.gov.uk/community/resources-and-tools/) of the [GOV.UK Design System](https://design-system.service.gov.uk/). The Design System team is not responsible for it and cannot support you with using it. Contact the [maintainers](#contributors) directly if you need [help](#support) or you want to request a feature.** diff --git a/app/templates/base.html b/app/templates/base.html index 2a94022..071e1d3 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -13,7 +13,7 @@ - + {% assets "css" %}{% endassets %} {% endblock %} @@ -33,7 +33,7 @@ {% endset %} {{ govukCookieBanner({ - 'ariaLabel': "Cookies on " + config['SERVICE_NAME'], + 'ariaLabel': "Cookies on " ~ config['SERVICE_NAME'], 'attributes': { 'id': "cookie-banner" }, @@ -42,7 +42,7 @@ 'attributes': { 'id': "default-message" }, - 'headingText': "Cookies on " + config['SERVICE_NAME'], + 'headingText': "Cookies on " ~ config['SERVICE_NAME'], 'html': html, 'actions': [ { @@ -161,9 +161,9 @@ {% endblock %} {% block bodyEnd %} - + {% assets "js" %}{% endassets %} diff --git a/app/templates/demos/component.html b/app/templates/demos/component.html index 6879a47..2e982b9 100644 --- a/app/templates/demos/component.html +++ b/app/templates/demos/component.html @@ -27,6 +27,7 @@ {%- from 'govuk_frontend_jinja/components/phase-banner/macro.html' import govukPhaseBanner -%} {%- from 'govuk_frontend_jinja/components/radios/macro.html' import govukRadios -%} {%- from 'govuk_frontend_jinja/components/select/macro.html' import govukSelect -%} +{%- from 'govuk_frontend_jinja/components/service-navigation/macro.html' import govukServiceNavigation -%} {%- from 'govuk_frontend_jinja/components/skip-link/macro.html' import govukSkipLink -%} {%- from 'govuk_frontend_jinja/components/summary-list/macro.html' import govukSummaryList -%} {%- from 'govuk_frontend_jinja/components/table/macro.html' import govukTable -%} @@ -133,6 +134,8 @@

                      {{fixture. {{ govukRadios(fixture.options)}} {% elif component == 'select' %} {{ govukSelect(fixture.options)}} + {% elif component == 'service-navigation' %} + {{ govukServiceNavigation(fixture.options)}} {% elif component == 'skip-link' %} {{ govukSkipLink(fixture.options)}} {% elif component == 'summary-list' %} diff --git a/build.sh b/build.sh index b97beac..447e178 100755 --- a/build.sh +++ b/build.sh @@ -4,7 +4,7 @@ rm -rf app/static/images rm -rf app/static/govuk-frontend* # Get new release distribution assets and move to static directory -curl -L https://github.com/alphagov/govuk-frontend/releases/download/v5.4.0/release-v5.4.0.zip > govuk_frontend.zip +curl -L https://github.com/alphagov/govuk-frontend/releases/download/v5.6.0/release-v5.6.0.zip > govuk_frontend.zip unzip -o govuk_frontend.zip -d app/static mv app/static/assets/* app/static @@ -21,10 +21,10 @@ rm -rf govuk_frontend.zip rm -rf app/demos/govuk_components # Get new release source code and move to a directory -curl -L https://github.com/alphagov/govuk-frontend/archive/refs/tags/v5.4.0.zip > govuk_frontend_source.zip +curl -L https://github.com/alphagov/govuk-frontend/archive/refs/tags/v5.6.0.zip > govuk_frontend_source.zip unzip -o govuk_frontend_source.zip -d govuk_frontend_source mkdir app/demos/govuk_components -mv govuk_frontend_source/govuk-frontend-5.4.0/packages/govuk-frontend/src/govuk/components/** app/demos/govuk_components +mv govuk_frontend_source/govuk-frontend-5.6.0/packages/govuk-frontend/src/govuk/components/** app/demos/govuk_components # Remove all files apart from test fixtures find app/demos/govuk_components -type f ! -name '*.yaml' -delete diff --git a/requirements.txt b/requirements.txt index 724d6b5..e7aaeed 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,17 +31,17 @@ flask-limiter[redis]==3.8.0 # via -r requirements.in flask-wtf==1.2.1 # via govuk-frontend-wtf -govuk-frontend-jinja==3.1.0 +govuk-frontend-jinja==3.3.0 # via # -r requirements.in # govuk-frontend-wtf govuk-frontend-wtf==3.1.0 # via -r requirements.in -gunicorn==22.0.0 +gunicorn==23.0.0 # via -r requirements.in -idna==3.7 +idna==3.8 # via email-validator -importlib-resources==6.4.0 +importlib-resources==6.4.4 # via limits itsdangerous==2.2.0 # via @@ -73,11 +73,11 @@ packaging==24.1 # limits pygments==2.18.0 # via rich -pyyaml==6.0.1 +pyyaml==6.0.2 # via -r requirements.in redis==5.0.8 # via limits -rich==13.7.1 +rich==13.8.0 # via flask-limiter typing-extensions==4.12.2 # via @@ -85,7 +85,7 @@ typing-extensions==4.12.2 # limits webassets==2.0 # via flask-assets -werkzeug==3.0.3 +werkzeug==3.0.4 # via flask wrapt==1.16.0 # via deprecated diff --git a/requirements_dev.txt b/requirements_dev.txt index 9a8a98e..3328925 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -4,11 +4,11 @@ # # pip-compile requirements_dev.in # -attrs==23.2.0 +attrs==24.2.0 # via flake8-bugbear bandit==1.7.9 # via -r requirements_dev.in -black==24.4.2 +black==24.8.0 # via -r requirements_dev.in boolean-py==4.0 # via license-expression @@ -27,29 +27,29 @@ click==8.1.7 # black # pip-tools # pur -coverage[toml]==7.6.0 +coverage[toml]==7.6.1 # via pytest-cov -cyclonedx-python-lib==7.5.1 +cyclonedx-python-lib==7.6.0 # via pip-audit defusedxml==0.7.1 # via py-serializable filelock==3.15.4 # via cachecontrol -flake8==7.1.0 +flake8==7.1.1 # via # flake8-bugbear # pep8-naming -flake8-bugbear==24.4.26 +flake8-bugbear==24.8.19 # via -r requirements_dev.in html5lib==1.1 # via pip-audit -idna==3.7 +idna==3.8 # via requests iniconfig==2.0.0 # via pytest isort==5.13.2 # via -r requirements_dev.in -license-expression==30.3.0 +license-expression==30.3.1 # via cyclonedx-python-lib markdown-it-py==3.0.0 # via rich @@ -59,7 +59,7 @@ mdurl==0.1.2 # via markdown-it-py msgpack==1.0.8 # via cachecontrol -mypy==1.11.1 +mypy==1.11.2 # via -r requirements_dev.in mypy-extensions==1.0.0 # via @@ -76,7 +76,7 @@ packaging==24.1 # pytest pathspec==0.12.1 # via black -pbr==6.0.0 +pbr==6.1.0 # via stevedore pep8-naming==0.14.1 # via -r requirements_dev.in @@ -96,13 +96,13 @@ pur==7.3.2 # via -r requirements_dev.in py-serializable==1.1.0 # via cyclonedx-python-lib -pycodestyle==2.12.0 +pycodestyle==2.12.1 # via flake8 pyflakes==3.2.0 # via flake8 pygments==2.18.0 # via rich -pyparsing==3.1.2 +pyparsing==3.1.4 # via pip-requirements-parser pyproject-hooks==1.1.0 # via @@ -112,13 +112,13 @@ pytest==8.3.2 # via pytest-cov pytest-cov==5.0.0 # via -r requirements_dev.in -pyyaml==6.0.1 +pyyaml==6.0.2 # via bandit requests==2.32.3 # via # cachecontrol # pip-audit -rich==13.7.1 +rich==13.8.0 # via # bandit # pip-audit @@ -126,7 +126,7 @@ six==1.16.0 # via html5lib sortedcontainers==2.4.0 # via cyclonedx-python-lib -stevedore==5.2.0 +stevedore==5.3.0 # via bandit toml==0.10.2 # via pip-audit @@ -136,7 +136,7 @@ urllib3==2.2.2 # via requests webencodings==0.5.1 # via html5lib -wheel==0.43.0 +wheel==0.44.0 # via pip-tools # The following packages are considered to be unsafe in a requirements file: diff --git a/runtime.txt b/runtime.txt index b884b0f..14a2d25 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -python-3.12.2 \ No newline at end of file +python-3.12.5 \ No newline at end of file From f5731a76536064e288c7afb109328b8c78e19ab1 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 3 Sep 2024 09:03:18 +0100 Subject: [PATCH 58/62] upgrade requirements --- requirements_dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_dev.txt b/requirements_dev.txt index 3328925..362865a 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -18,7 +18,7 @@ cachecontrol[filecache]==0.14.0 # via # cachecontrol # pip-audit -certifi==2024.7.4 +certifi==2024.8.30 # via requests charset-normalizer==3.3.2 # via requests From a18106bb90638c1eeffa5c05dddbccc8a277a690 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 3 Sep 2024 09:03:27 +0100 Subject: [PATCH 59/62] upgrade requirements --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e7aaeed..5d1cfee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ click==8.1.7 # via flask cssmin==0.2.0 # via -r requirements.in -deepmerge==1.1.1 +deepmerge==2.0 # via govuk-frontend-wtf deprecated==1.2.14 # via limits From 495deed7eb347305a2e86e72892185ca6d728618 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 3 Sep 2024 12:21:57 +0100 Subject: [PATCH 60/62] change OU --- nginx/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nginx/Dockerfile b/nginx/Dockerfile index ae604bf..b7c491a 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -2,6 +2,6 @@ FROM nginx:stable RUN rm /etc/nginx/conf.d/default.conf && \ mkdir /etc/nginx/ssl && \ - openssl req -x509 -noenc -newkey rsa:2048 -keyout /etc/nginx/ssl/key.pem -out /etc/nginx/ssl/req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=DDaT/CN=localhost" + openssl req -x509 -noenc -newkey rsa:2048 -keyout /etc/nginx/ssl/key.pem -out /etc/nginx/ssl/req.pem -days 90 -subj "/C=GB/ST=Devon/L=Plymouth/O=HM Land Registry/OU=Digital/CN=localhost" COPY nginx.conf /etc/nginx/conf.d \ No newline at end of file From 716c5b64b8aebe251531ff9b550f244342c6d763 Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 3 Sep 2024 12:22:14 +0100 Subject: [PATCH 61/62] update script hash --- nginx/nginx.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nginx/nginx.conf b/nginx/nginx.conf index 91eab39..cb07a61 100644 --- a/nginx/nginx.conf +++ b/nginx/nginx.conf @@ -1,5 +1,5 @@ -# generated 2024-06-13, Mozilla Guideline v5.7, nginx 1.26.1, OpenSSL 3.0.11, modern configuration, no OCSP -# https://ssl-config.mozilla.org/#server=nginx&version=1.26.1&config=modern&openssl=3.0.11&ocsp=false&guideline=5.7 +# generated 2024-09-03, Mozilla Guideline v5.7, nginx 1.26.2, OpenSSL 3.0.13, modern configuration, no OCSP +# https://ssl-config.mozilla.org/#server=nginx&version=1.26.2&config=modern&openssl=3.0.13&ocsp=false&guideline=5.7 server { listen 80 default_server; listen [::]:80 default_server; @@ -25,7 +25,7 @@ server { ssl_prefer_server_ciphers off; # add security headers - add_header Content-Security-Policy "script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-xvC5hOpINthj2xzP7qkRGmqR3SpU8ZVw1sEMKbsOS/4='; object-src 'none'; base-uri 'none';" always; + add_header Content-Security-Policy "script-src 'self' 'sha256-GUQ5ad8JK5KmEWmROf3LZd9ge94daqNvd8xy9YS1iDw=' 'sha256-3t81BEe/IfrPieOkVojxAPxOujfIBkzGt+HP2GeblR4='; object-src 'none'; base-uri 'none';" always; add_header Cross-Origin-Embedder-Policy "require-corp" always; add_header Cross-Origin-Opener-Policy "same-origin" always; add_header Cross-Origin-Resource-Policy "same-origin" always; From 8c2f0b050d7dc35facb81a670ef81fae6ec1571a Mon Sep 17 00:00:00 2001 From: Matt Shaw Date: Tue, 3 Sep 2024 13:10:39 +0100 Subject: [PATCH 62/62] set https only cookie --- app/main/routes.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/main/routes.py b/app/main/routes.py index ccf9f40..5aa173a 100644 --- a/app/main/routes.py +++ b/app/main/routes.py @@ -38,6 +38,7 @@ def cookies(): "cookies_policy", json.dumps(cookies_policy), max_age=31557600, + secure=True, ) return response elif request.method == "GET":