diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/pythonie/pythonie/settings/base.py b/pythonie/pythonie/settings/base.py index 27c66d2..4f19bed 100644 --- a/pythonie/pythonie/settings/base.py +++ b/pythonie/pythonie/settings/base.py @@ -74,13 +74,16 @@ ) MIDDLEWARE = ( + "django.middleware.security.SecurityMiddleware", + # WhiteNoise should sit directly after SecurityMiddleware so static + # files are served as early as possible. + "whitenoise.middleware.WhiteNoiseMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", - "whitenoise.middleware.WhiteNoiseMiddleware", "wagtail.contrib.redirects.middleware.RedirectMiddleware", ) diff --git a/pythonie/pythonie/settings/production.py b/pythonie/pythonie/settings/production.py index 4abc442..cd89d38 100644 --- a/pythonie/pythonie/settings/production.py +++ b/pythonie/pythonie/settings/production.py @@ -1,5 +1,8 @@ # ruff: noqa import os + +import dj_database_url + from pythonie.settings.configure import configure_redis from .base import * @@ -7,6 +10,28 @@ # Disable debug mode DEBUG = False +# Security / HTTPS hardening +# Heroku terminates TLS at the router and forwards plain HTTP to the dyno, +# so Django must trust the X-Forwarded-Proto header to detect HTTPS. +# Without this, SECURE_SSL_REDIRECT would cause an infinite redirect loop. +SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True +SECURE_HSTS_SECONDS = 31536000 # 1 year +SECURE_HSTS_INCLUDE_SUBDOMAINS = True +SECURE_HSTS_PRELOAD = True + +# Persistent database connections with TLS for Heroku Postgres. +# Avoids opening a new TCP+TLS connection on every request. +DATABASES = { + "default": dj_database_url.config( + conn_max_age=600, + conn_health_checks=True, + ssl_require=True, + ) +} + # AWS S3 Storage Configuration AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") @@ -14,6 +39,9 @@ AWS_S3_CUSTOM_DOMAIN = "s3.python.ie" AWS_HOST = "s3-eu-west-1.amazonaws.com" AWS_DEFAULT_ACL = "public-read" +# Don't silently overwrite/delete user-uploaded files that share a name +# (Wagtail recommends False; see wagtailadmin.W004). +AWS_S3_FILE_OVERWRITE = False # Use S3 for media files (Django 5.1+ STORAGES API) STORAGES = { diff --git a/runtime.txt b/runtime.txt deleted file mode 100644 index e912aa6..0000000 --- a/runtime.txt +++ /dev/null @@ -1 +0,0 @@ -python-3.13