Skip to content

Commit

Permalink
Merge pull request #135 from LandRegistry/nginx
Browse files Browse the repository at this point in the history
Use NGINX reverse proxy server
matthew-shaw authored Sep 3, 2024
2 parents 1fa4820 + 8c2f0b0 commit 34e2df9
Showing 24 changed files with 325 additions and 240 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -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
18 changes: 7 additions & 11 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
FROM python:3.12-slim

RUN useradd containeruser
RUN useradd appuser

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 ./
WORKDIR /home/appuser

# 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 appuser:appuser ./

EXPOSE 9876
ENTRYPOINT ["./docker-entrypoint.sh"]
USER appuser
52 changes: 43 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.**

@@ -53,7 +53,7 @@ python -c 'import secrets; print(secrets.token_hex())'
docker compose up --build
```

You should now have the app running on <https://localhost:9876/>. Accept the browsers security warning due to the self-signed HTTPS certificate to continue.
You should now have the app running on <https://localhost/>. Accept the browsers security warning due to the self-signed HTTPS certificate to continue.

## Demos

@@ -67,19 +67,52 @@ To run the tests:
python -m pytest --cov=app --cov-report=term-missing --cov-branch
```

## Environment

```mermaid
flowchart TB
cache1(Redis):::CACHE
Client
prox1(NGINX):::PROXY
web1(Flask app):::WEB
web2[/Static files/]:::WEB
Client <-- https:443 --> prox1 <-- http:5000 --> web1
prox1 -- Read only --> web2
web1 -- Write --> web2
web1 <-- redis:6379 --> cache1
subgraph Proxy container
prox1
end
subgraph Web container
web1
web2
end
subgraph Cache container
cache1
end
classDef CACHE fill:#F8CECC,stroke:#B85450,stroke-width:2px
classDef PROXY fill:#D5E8D4,stroke:#82B366,stroke-width:2px
classDef WEB fill:#FFF2CC,stroke:#D6B656,stroke-width:2px
```

## Features

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.

### 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

@@ -101,20 +134,21 @@ 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.
- 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 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 [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

81 changes: 16 additions & 65 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
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_talisman import Talisman
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

assets = Environment()
compress = Compress()
csrf = CSRFProtect()
limiter = Limiter(get_remote_address, default_limits=["2 per second", "60 per minute"])
talisman = Talisman()
limiter = Limiter(
get_remote_address,
default_limits=["2 per second", "60 per minute"],
)


def create_app(config_class=Config):
@@ -33,74 +33,25 @@ def create_app(config_class=Config):
),
]
)

# 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": "()",
}
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1)

# Initialise app extensions
assets.init_app(app)
compress.init_app(app)
csrf.init_app(app)
limiter.init_app(app)
talisman.init_app(app, content_security_policy=csp, permissions_policy=permissions_policy)
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:
8 changes: 7 additions & 1 deletion app/demos/custom_validators.py
Original file line number Diff line number Diff line change
@@ -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

52 changes: 39 additions & 13 deletions app/demos/forms.py
Original file line number Diff line number Diff line change
@@ -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,15 +259,23 @@ 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.",
)

radio_field = RadioField(
"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.",
)

@@ -268,10 +298,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.",
)
10 changes: 7 additions & 3 deletions app/demos/routes.py
Original file line number Diff line number Diff line change
@@ -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,12 +19,16 @@ def components():
@bp.route("/components/<string:component>", 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

return render_template("component.html", component=component, fixtures=fixtures)
return render_template(
"component.html",
component=component,
fixtures=fixtures,
)


@bp.route("/forms", methods=["GET"])
7 changes: 6 additions & 1 deletion app/main/routes.py
Original file line number Diff line number Diff line change
@@ -34,7 +34,12 @@ 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,
secure=True,
)
return response
elif request.method == "GET":
if request.cookies.get("cookies_policy"):
10 changes: 5 additions & 5 deletions app/templates/base.html
Original file line number Diff line number Diff line change
@@ -13,7 +13,7 @@
<meta name="description" content="{{config['SERVICE_NAME']}}">
<meta name="keywords" content="GOV.UK, govuk, gov, government, uk, frontend, ui, user interface, jinja, python, flask, port, template, templating, macro, component, design system, html, forms, wtf, wtforms, widget, widgets, demo, example">
<meta name="author" content="{{config['DEPARTMENT_NAME']}}">
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='govuk-frontend-5.4.0.min.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='govuk-frontend-5.6.0.min.css') }}" />
{% assets "css" %}<link href="{{ ASSET_URL }}" rel="stylesheet">{% 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 %}
<script type="module" src="{{ url_for('static', filename='govuk-frontend-5.4.0.min.js') }}"></script>
<script type="module" src="{{ url_for('static', filename='govuk-frontend-5.6.0.min.js') }}"></script>
<script type="module">
import { initAll } from "{{ url_for('static', filename='govuk-frontend-5.4.0.min.js') }}"
import { initAll } from "{{ url_for('static', filename='govuk-frontend-5.6.0.min.js') }}"
initAll()
</script>
{% assets "js" %}<script type="text/javascript" src="{{ ASSET_URL }}"></script>{% endassets %}
5 changes: 4 additions & 1 deletion app/templates/demos/component.html
Original file line number Diff line number Diff line change
@@ -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 -%}
@@ -65,7 +66,7 @@ <h1 class="govuk-heading-xl">{{component | replace("-", " ") | capitalize}}</h1>
<div class="govuk-grid-row">
<div class="govuk-grid-column-two-thirds">
<p class="govuk-body">Examples:</p>
<ul class="govuk-list govuk-list--bullet govuk-list--spaced">
<ul class="govuk-list govuk-list--bullet govuk-list--spaced govuk-list--spaced">
{% for fixture in fixtures.examples if not fixture.hidden %}
<li>
<a class="govuk-link" href="#{{fixture.name | replace(' ', '-')}}">{{fixture.name | capitalize}}</a>
@@ -133,6 +134,8 @@ <h2 id="{{fixture.name | replace(' ', '-')}}" class="govuk-heading-l">{{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' %}
2 changes: 1 addition & 1 deletion app/templates/demos/components.html
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@
<div class="govuk-grid-column-two-thirds">
<span class="govuk-caption-xl">Demo</span>
<h1 class="govuk-heading-l">Components</h1>
<ul class="govuk-list govuk-list--bullet govuk-list--spaced">
<ul class="govuk-list govuk-list--bullet govuk-list--spaced govuk-list--spaced">
{% for component in components %}
<li>
<a class="govuk-link" href="{{ url_for('demos.component', component=component) }}">{{component | replace("-", " ") | capitalize}}</a>
2 changes: 1 addition & 1 deletion app/templates/demos/forms.html
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@
{{ super() }}
<span class="govuk-caption-xl">Demo</span>
<h1 class="govuk-heading-l">Forms</h1>
<ul class="govuk-list govuk-list--bullet">
<ul class="govuk-list govuk-list--bullet govuk-list--spaced">
<li><a class="govuk-link" href="{{ url_for('demos.autocomplete') }}">Autocomplete</a></li>
<li><a class="govuk-link" href="{{ url_for('demos.bank_details') }}">Bank details</a></li>
<li><a class="govuk-link" href="{{ url_for('demos.conditional_reveal') }}">Conditional reveal</a></li>
6 changes: 3 additions & 3 deletions app/templates/main/index.html
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ <h1 class="govuk-heading-l">Hello, World!</h1>
get a new project started quicker.
</p>
<p class="govuk-body">It is also the reference implementation of two core packages:</p>
<ul class="govuk-list govuk-list--bullet">
<ul class="govuk-list govuk-list--bullet govuk-list--spaced">
<li><a class="govuk-link" href="https://github.com/LandRegistry/govuk-frontend-jinja">GOV.UK Frontend Jinja</a>
which provides Jinja macros of GOV.UK components</li>
<li><a class="govuk-link" href="https://github.com/LandRegistry/govuk-frontend-wtf">GOV.UK Frontend WTForms</a>
@@ -26,7 +26,7 @@ <h1 class="govuk-heading-l">Hello, World!</h1>
<h2 class="govuk-heading-m">Features</h2>
<p class="govuk-body">A number of other packages are used to provide the features listed below with sensible and
best-practice defaults:</p>
<ul class="govuk-list govuk-list--bullet">
<ul class="govuk-list govuk-list--bullet govuk-list--spaced">
<li>Asset management</li>
<li>Cache busting</li>
<li>Form generation and validation</li>
@@ -40,7 +40,7 @@ <h2 class="govuk-heading-m">Features</h2>
</ul>

<h2 class="govuk-heading-m">Demos</h2>
<ul class="govuk-list govuk-list--bullet">
<ul class="govuk-list govuk-list--bullet govuk-list--spaced">
<li><a class="govuk-link" href="{{ url_for('demos.components')}}">Components</a></li>
<li><a class="govuk-link" href="{{ url_for('demos.forms')}}">Forms</a></li>
</ul>
12 changes: 6 additions & 6 deletions build.sh
Original file line number Diff line number Diff line change
@@ -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

@@ -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
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 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.6.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
30 changes: 19 additions & 11 deletions compose.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
services:
web:
container_name: govuk-frontend-flask
build: .
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]
- 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
- static_volume:/home/appuser/app/static:rw
expose:
- 5000
depends_on:
- cache
cache:
container_name: redis
image: redis:7.0-alpine
- redis
redis:
image: redis:7-alpine
restart: always
expose:
- 6379
nginx:
build: ./nginx
volumes:
- static_volume:/home/appuser/app/static:ro
ports:
- 6379:6379

- 443:443
depends_on:
- web
volumes:
static_volume:
1 change: 1 addition & 0 deletions config.py
Original file line number Diff line number Diff line change
@@ -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
3 changes: 0 additions & 3 deletions docker-entrypoint.sh

This file was deleted.

7 changes: 7 additions & 0 deletions nginx/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
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=Digital/CN=localhost"

COPY nginx.conf /etc/nginx/conf.d
65 changes: 65 additions & 0 deletions nginx/nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# 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;

location / {
return 301 https://$host$request_uri;
}
}

server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;

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;

# modern configuration
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;

# add security headers
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;
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 "DENY" always;
add_header X-Xss-Protection "1; mode=block" always;

# enable gzip compression
gzip on;
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
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-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Real-IP $remote_addr;
}

location /assets/ {
# 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;
}
}
2 changes: 0 additions & 2 deletions requirements.in
Original file line number Diff line number Diff line change
@@ -2,9 +2,7 @@ cssmin
email_validator
flask
flask-assets
flask-compress
flask-limiter[redis]
flask-talisman
govuk-frontend-jinja
govuk-frontend-wtf
gunicorn
37 changes: 14 additions & 23 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -6,49 +6,42 @@
#
blinker==1.8.2
# via flask
brotli==1.1.0
# via flask-compress
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
dnspython==2.6.1
# via email-validator
email-validator==2.1.1
email-validator==2.2.0
# via -r requirements.in
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-talisman==1.1.0
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
@@ -61,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
@@ -74,31 +67,29 @@ 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
pygments==2.18.0
# via rich
pyyaml==6.0.1
pyyaml==6.0.2
# via -r requirements.in
redis==5.0.4
redis==5.0.8
# via limits
rich==13.7.1
rich==13.8.0
# via flask-limiter
typing-extensions==4.12.0
typing-extensions==4.12.2
# via
# flask-limiter
# limits
webassets==2.0
# via flask-assets
werkzeug==3.0.3
werkzeug==3.0.4
# via flask
wrapt==1.16.0
# via deprecated
wtforms==3.1.2
# via
# flask-wtf
# govuk-frontend-wtf
zstandard==0.22.0
# via flask-compress
3 changes: 2 additions & 1 deletion requirements_dev.in
Original file line number Diff line number Diff line change
@@ -2,8 +2,9 @@ bandit
black
flake8-bugbear
isort
mypy
pep8-naming
pip-audit
pip-tools
pur
pytest-cov
safety
148 changes: 70 additions & 78 deletions requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -4,147 +4,139 @@
#
# pip-compile requirements_dev.in
#
annotated-types==0.7.0
# via pydantic
attrs==23.2.0
attrs==24.2.0
# via flake8-bugbear
authlib==1.3.0
# via safety
bandit==1.7.8
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
build==1.2.1
# via pip-tools
certifi==2024.2.2
cachecontrol[filecache]==0.14.0
# via
# cachecontrol
# pip-audit
certifi==2024.8.30
# via requests
cffi==1.16.0
# via cryptography
charset-normalizer==3.3.2
# via requests
click==8.1.7
# via
# black
# pip-tools
# pur
# safety
# typer
coverage[toml]==7.5.3
coverage[toml]==7.6.1
# via pytest-cov
cryptography==42.0.7
# via authlib
dparse==0.6.4b0
# via
# safety
# safety-schemas
flake8==7.0.0
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.1
# via
# flake8-bugbear
# pep8-naming
flake8-bugbear==24.4.26
flake8-bugbear==24.8.19
# via -r requirements_dev.in
idna==3.7
html5lib==1.1
# via pip-audit
idna==3.8
# 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.1
# via cyclonedx-python-lib
markdown-it-py==3.0.0
# via rich
markupsafe==2.1.5
# via jinja2
marshmallow==3.21.2
# 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.2
# via -r requirements_dev.in
mypy-extensions==1.0.0
# via black
packaging==24.0
# 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
pbr==6.1.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
# 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
py-serializable==1.1.0
# via cyclonedx-python-lib
pycodestyle==2.12.1
# via flake8
pycparser==2.22
# via cffi
pydantic==2.7.2
# via
# safety
# safety-schemas
pydantic-core==2.18.3
# via pydantic
pyflakes==3.2.0
# via flake8
pygments==2.18.0
# via rich
pyparsing==3.1.4
# via pip-requirements-parser
pyproject-hooks==1.1.0
# via
# build
# pip-tools
pytest==8.2.1
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 safety
rich==13.7.1
# via
# bandit
# safety
# typer
ruamel-yaml==0.18.6
# cachecontrol
# pip-audit
rich==13.8.0
# via
# safety
# safety-schemas
ruamel-yaml-clib==0.2.8
# via ruamel-yaml
safety==3.2.0
# via -r requirements_dev.in
safety-schemas==0.0.2
# via safety
shellingham==1.5.4
# via typer
stevedore==5.2.0
# bandit
# pip-audit
six==1.16.0
# via html5lib
sortedcontainers==2.4.0
# via cyclonedx-python-lib
stevedore==5.3.0
# via bandit
typer==0.12.3
# via safety
typing-extensions==4.12.0
# via
# pydantic
# pydantic-core
# safety
# safety-schemas
# typer
urllib3==2.2.1
# via
# requests
# safety
wheel==0.43.0
toml==0.10.2
# via pip-audit
typing-extensions==4.12.2
# via mypy
urllib3==2.2.2
# via requests
webencodings==0.5.1
# via html5lib
wheel==0.44.0
# via pip-tools

# The following packages are considered to be unsafe in a requirements file:
2 changes: 1 addition & 1 deletion runtime.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
python-3.12.2
python-3.12.5

0 comments on commit 34e2df9

Please sign in to comment.