Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,31 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

release:
# after image, so the release can tag the image this commit already pushed
needs: image
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
packages: write
steps:
- id: release
uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
- uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
if: steps.release.outputs.release_created
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1
if: steps.release.outputs.release_created
- name: Tag the release image
if: steps.release.outputs.release_created
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
image=ghcr.io/spongepowered/spongeauth
docker buildx imagetools create -t "$image:$VERSION" "$image:sha-${GITHUB_SHA::7}"
3 changes: 3 additions & 0 deletions .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
13 changes: 9 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,19 @@ services:
- "host.docker.internal:host-gateway"
links:
- "redis"
# nginx shares this container's network, so its port is published here
ports:
- "8080:80"
proxy:
image: nginx:mainline-alpine
restart: always
# same network namespace as app, like a k8s pod: nginx.conf proxies to 127.0.0.1:8080
network_mode: "service:app"
environment:
# IPs/CIDRs allowed to set X-Forwarded-For; empty trusts none
NGINX_TRUSTED_PROXIES: ${NGINX_TRUSTED_PROXIES:-}
volumes:
- static:/usr/share/nginx/public_html/static
- ../media:/usr/share/nginx/public_html/media
- ./nginx.conf:/etc/nginx/nginx.conf
links:
- "app"
ports:
- "8080:80"
- ./nginx-trusted-proxies.sh:/docker-entrypoint.d/40-trusted-proxies.sh
7 changes: 5 additions & 2 deletions entrypoint/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ set -euxo pipefail

cd $APP_HOME

$HOME/env/bin/python spongeauth/manage.py migrate
# set RUN_MIGRATIONS=false when migrations run elsewhere (init container / Job)
if [ "${RUN_MIGRATIONS:-true}" = "true" ]; then
$HOME/env/bin/python spongeauth/manage.py migrate --noinput
fi
$HOME/env/bin/python spongeauth/manage.py collectstatic --noinput

set +euxo pipefail

# run worker - necessary for background sso syncs
./entrypoint/run-worker.sh &

$HOME/env/bin/gunicorn -b :8080 -w 4 --chdir spongeauth spongeauth.wsgi
exec $HOME/env/bin/gunicorn -b :8080 -w 4 --chdir spongeauth spongeauth.wsgi
21 changes: 21 additions & 0 deletions nginx-trusted-proxies.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/sh
# Mount at /docker-entrypoint.d/40-trusted-proxies.sh in the official nginx image; its entrypoint runs it before nginx starts.
# Turns NGINX_TRUSTED_PROXIES (comma- or space-separated IPs/CIDRs) into set_real_ip_from directives for nginx.conf.
# Unset or empty trusts no proxy, so $remote_addr stays the proxy's own address.
set -eu

out=/run/nginx/trusted-proxies.conf
mkdir -p "${out%/*}"
: > "$out"

for cidr in $(echo "${NGINX_TRUSTED_PROXIES:-}" | tr ',' ' '); do
case "$cidr" in
*[!0-9a-fA-F.:/]*)
echo "$0: invalid entry in NGINX_TRUSTED_PROXIES: $cidr" >&2
exit 1
;;
esac
echo "set_real_ip_from $cidr;" >> "$out"
done

[ -s "$out" ] || echo "$0: NGINX_TRUSTED_PROXIES is empty, client IPs will be the proxy's" >&2
33 changes: 19 additions & 14 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
error_log /dev/stderr;
pid /run/nginx.pid;

# Load dynamic modules. See /usr/share/nginx/README.dynamic.
include /usr/share/nginx/modules/*.conf;

events {
worker_connections 1024;
}
Expand All @@ -15,30 +12,38 @@ http {
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;
access_log /dev/stdout main;

sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

client_max_body_size 300M;
client_max_body_size 10M;

include /etc/nginx/mime.types;
default_type application/octet-stream;

proxy_cache_path /srv/cache levels=1:2 keys_zone=STATIC:10m inactive=6h max_size=2g;
# TLS ends at Traefik: take the client address from its X-Forwarded-For.
# The trusted proxies come from NGINX_TRUSTED_PROXIES, rendered by nginx-trusted-proxies.sh at startup.
include /run/nginx/trusted-proxies.conf;
real_ip_header X-Forwarded-For;
real_ip_recursive on;

proxy_cache_path /var/cache/nginx/avatar levels=1:2 keys_zone=STATIC:10m inactive=6h max_size=2g;

upstream spongeauth {
server app:8080;
# app container in the same pod
server 127.0.0.1:8080;
}

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;

# static/ and media/ are mounted under here, at the same paths the app container uses under public_html
root /usr/share/nginx/public_html;

rewrite ^/avatars(.*)$ /avatar$1;
Expand All @@ -53,10 +58,10 @@ http {
}

location /avatar/ {
proxy_cache_valid 5m;
proxy_cache STATIC;
proxy_cache_key "$request_method $request_uri $http_accept";
proxy_cache_lock on;
proxy_cache_valid 5m;
proxy_cache STATIC;
proxy_cache_key "$request_method $request_uri $http_accept";
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status;
expires 5m;
proxy_hide_header Vary;
Expand All @@ -65,7 +70,7 @@ http {
add_header Cache-Control "public, max-age=300, must-revalidate";

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
Expand All @@ -75,7 +80,7 @@ http {

location @proxy_to_app {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
Expand Down
8 changes: 8 additions & 0 deletions release-please-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "simple",
"bootstrap-sha": "09729331ec77d6c141e1211925bf53bf46ca0da9",
"packages": {
".": {}
}
}
7 changes: 7 additions & 0 deletions spongeauth/core/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ def test_admin_login_redirect_not_staff():
client.login(username=user.username, password="secret")
resp = client.get("/admin/", follow=True)
assert resp.redirect_chain == [("/admin/login/?next=/admin/", 302), ("/", 302)]


def test_healthz():
# no django_db mark: fails if the probe ever starts touching the database
resp = django.test.Client().get("/healthz")
assert resp.status_code == 200
assert resp.content == b"ok"
5 changes: 5 additions & 0 deletions spongeauth/core/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.urls import reverse
from django.contrib.auth.decorators import login_required
Expand All @@ -14,3 +15,7 @@ def admin_login_redirect(request):
return redirect("index")

return redirect("{}?{}".format(reverse("accounts:login"), urlencode({"next": request.GET.get("next", "/")})))


def healthz(request):
return HttpResponse("ok", content_type="text/plain")
9 changes: 9 additions & 0 deletions spongeauth/spongeauth/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
CSRF_COOKIE_SECURE = os.getenv("CSRF_COOKIE_SECURE", "true") != 'false'
CSRF_COOKIE_HTTPONLY = True
CSRF_TRUSTED_ORIGINS = os.environ["CSRF_TRUSTED_ORIGINS"].split(',')
# TLS terminates at the ingress; nginx forwards its X-Forwarded-Proto
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"root": {"handlers": ["console"], "level": "INFO"},
}

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS = os.environ["EMAIL_TLS"] == 'true'
Expand Down
3 changes: 2 additions & 1 deletion spongeauth/spongeauth/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
import twofa.urls
import sso.urls

from core.views import index, admin_login_redirect
from core.views import index, admin_login_redirect, healthz

from accounts.views import avatar_for_user

Expand All @@ -39,6 +39,7 @@
re_path(r"^avatar/(?P<username>[^/]+)/?$", avatar_for_user, name="avatar-for-user"),
re_path(r"^sso/", include(sso.urls, "sso")),
re_path(r"^$", index, name="index"),
re_path(r"^healthz$", healthz, name="healthz"),
re_path(r"^api/", include(api.urls, "api")),
re_path("django-rq/", include(django_rq.urls)),
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
Expand Down
Loading