BounCA bundle

This commit is contained in:
Daniel Berteaud 2023-11-15 22:44:50 +01:00
parent 0194dc5de6
commit 0127749909
23 changed files with 608 additions and 0 deletions

151
bounca.nomad.hcl Normal file
View File

@ -0,0 +1,151 @@
job [[ .bounca.instance | toJSON ]] {
[[- $c := merge .bounca . ]]
[[ template "common/job_start.tpl" $c ]]
group "bounca" {
network {
mode = "bridge"
}
service {
name = "[[ .bounca.instance ]][[ .consul.suffix ]]"
port = 8749
[[ template "common/connect.tpl" $c ]]
tags = [
"[[ $c.traefik.instance ]].enable=[[ if $c.traefik.enabled ]]true[[ else ]]false[[ end ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ $c.consul.suffix ]].rule=Host(`[[ (urlParse .bounca.public_url).Hostname ]]`)
[[- if not (regexp.Match "^/?$" (urlParse .bounca.public_url).Path) ]] && PathPrefix(`[[ (urlParse .bounca.public_url).Path ]]`)[[ end ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ $c.consul.suffix ]].entrypoints=[[ join $c.traefik.entrypoints "," ]]",
[[- if not (regexp.Match "^/?$" (urlParse .bounca.public_url).Path) ]]
"[[ $c.traefik.instance ]].http.middlewares.[[ .bounca.instance ]][[ $c.consul.suffix ]]-prefix.stripprefix.prefixes=[[ (urlParse .bounca.public_url).Path ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ $c.consul.suffix ]].middlewares=[[ .bounca.instance ]][[ $c.consul.suffix ]]-prefix,[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
[[- else ]]
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ $c.consul.suffix ]].middlewares=[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
[[- end ]]
]
}
task "bounca" {
driver = [[ $c.nomad.driver | toJSON ]]
user = 8749
config {
image = [[ $c.image | toJSON ]]
pids_limit = 50
readonly_rootfs = true
command = "gunicorn"
args = [
"--bind=unix:/alloc/data/bounca.sock",
"--threads=4",
"--max-requests=10000",
"bounca.wsgi:application"
]
[[ template "common/tmpfs.tpl" dict "target" "/tmp" "size" 1000000 ]]
volumes = ["local/docker_settings.py:/opt/bounca/bounca/docker_settings.py:ro"]
}
vault {
policies = ["[[ .bounca.instance ]][[ .consul.suffix ]]"]
disable_file = true
env = false
}
env {
BOUNCA_UNIX_SOCKET = "/alloc/data/bounca.sock"
BOUNCA_HOST = "[[ (urlParse .bounca.public_url).Hostname ]]"
}
[[ template "common/file_env.tpl" $c.env ]]
template {
data =<<_EOT
[[ template "bounca/docker_settings.py.tpl" $c ]]
_EOT
destination = "local/docker_settings.py"
}
[[ template "common/resources.tpl" $c.resources ]]
}
task "public-exporter" {
driver = [[ $c.nomad.driver | toJSON ]]
user = 8749
lifecycle {
hook = "poststart"
sidecar = true
}
config {
image = [[ $c.image | toJSON ]]
pids_limit = 50
readonly_rootfs = true
command = "bounca-pub-export"
args = [
"300",
]
[[ template "common/tmpfs.tpl" dict "target" "/tmp" "size" 1000000 ]]
}
vault {
policies = ["[[ .bounca.instance ]][[ .consul.suffix ]]"]
disable_file = true
env = false
}
env {
BOUNCA_MIGRATE = "false"
BOUNCA_PUBLIC_DIR = "/alloc/data/public"
}
[[ template "common/file_env.tpl" $c.env ]]
resources {
cpu = 10
memory = 10
memory_max = 20
}
}
task "nginx" {
driver = [[ $c.nomad.driver | toJSON ]]
user = 8749
lifecycle {
hook = "poststart"
sidecar = true
}
config {
image = [[ $c.image | toJSON ]]
pids_limit = 30
readonly_rootfs = true
command = "nginx"
args = [
"-c",
"/tmp/nginx.conf"
]
[[ template "common/tmpfs.tpl" dict "target" "/tmp" "size" 1000000 ]]
}
env {
BOUNCA_BIND_ADDR = "127.0.0.1:8749"
BOUNCA_MIGRATE = "false"
BOUNCA_UNIX_SOCKET = "/alloc/data/bounca.sock"
BOUNCA_PUBLIC_DIR = "/alloc/data/public"
BOUNCA_HOST = "[[ (urlParse .bounca.public_url).Hostname ]]"
}
resources {
cpu = 20
memory = 20
}
}
}
}

4
bundles.yml Normal file
View File

@ -0,0 +1,4 @@
---
dependencies:
- url: ../common.git

View File

@ -0,0 +1,3 @@
Kind = "service-defaults"
Name = "[[ .bounca.instance ]][[ .consul.suffix ]]"
Protocol = "http"

View File

@ -0,0 +1,16 @@
Kind = "service-intentions"
Name = "[[ .bounca.instance ]][[ .consul.suffix ]]"
Sources = [
{
Name = "[[ .traefik.instance ]]"
Permissions = [
{
Action = "allow"
HTTP {
PathPrefix = "/"
Methods = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "DELETE", "PATCH"]
}
}
]
}
]

71
images/bounca/Dockerfile Normal file
View File

@ -0,0 +1,71 @@
# syntax=docker/dockerfile:labs
FROM python:3.11-alpine AS builder
ARG BOUNCA_VERSION=0.4.4
workdir /opt
RUN set -euxo pipefail &&\
apk --no-cache add \
curl \
ca-certificates \
rdfind \
&&\
curl -sSLo bounca.tar.gz https://github.com/repleo/bounca/releases/download/v${BOUNCA_VERSION}/bounca.tar.gz &&\
python3 -m venv venv &&\
source /opt/venv/bin/activate &&\
tar xvzf bounca.tar.gz &&\
rm -f bounca.tar.gz &&\
cd bounca &&\
pip --no-cache-dir install -r requirements.txt &&\
pip --no-cache-dir install \
gunicorn \
&&\
mkdir -p /var/log/bounca/ &&\
cp /opt/bounca/etc/bounca/services.yaml.example /opt/bounca/etc/bounca/services.yaml &&\
./manage.py collectstatic &&\
rm -f /opt/bounca/etc/bounca/services.yaml &&\
sed -i -E 's|/etc/bounca/services.yaml|/tmp/services.yaml|g' /opt/bounca/bounca/settings.py &&\
rdfind /opt
FROM python:3.11-alpine
MAINTAINER [[ .docker.maintainer ]]
ENV PATH=/opt/venv/bin:${PATH} \
BOUNCA_MIGRATE=true \
BOUNCA_UNIX_SOCKET=/tmp/bounca.sock \
BOUNCA_HOST='*' \
DJANGO_SETTINGS_MODULE=bounca.docker_settings \
BOUNCA_BIND_ADDR=0.0.0.0:8749 \
BOUNCA_DB_NAME=bounca \
BOUNCA_DB_USER=bounca \
BOUNCA_DB_PASSWORD=bounca \
BOUNCA_DB_HOST=localhost \
BOUNCA_DB_PORT=5432 \
BOUNCA_DJANGO_SECRET=changeme \
BOUNCA_SMTP_SERVER=127.0.0.1 \
BOUNCA_SMTP_PORT=25 \
BOUNCA_ADMIN_EMAIL=admin@localhost \
BOUNCA_FROM_EMAIL=no-reply@localhost \
BOUNCA_ADMIN_USER=admin \
BOUNCA_ADMIN_PASSWORD=password \
BOUNCA_PUBLIC_DIR=/tmp/public
ADD https://git.lapiole.org/nomad/base_tools.git#master /
COPY --from=builder /opt /opt
RUN set -euxo pipefail &&\
apk --no-cache add \
tini \
gettext \
openssl \
nginx \
supervisor \
postgresql15-client
COPY root/ /
WORKDIR /opt/bounca
EXPOSE 8749
ENTRYPOINT ["tini", "--", "/entrypoint.sh"]
CMD ["supervisord", "-n", "-c", "/etc/supervisord.conf"]

View File

@ -0,0 +1,9 @@
#!/bin/sh
set -euo pipefail
echo "Populating /tmpservices.yaml from /opt/bounca/etc/bounca/services.yaml.template"
envsubst < /opt/bounca/etc/bounca/services.yaml.template > /tmp/services.yaml
chmod 600 /tmp/services.yaml
echo "Populating /tmp/nginx.conf from /opt/bounca/etc/nginx/nginx.conf.template"
envsubst < /opt/bounca/etc/nginx/nginx.conf.template > /tmp/nginx.conf

View File

@ -0,0 +1,4 @@
#!/bin/sh
set -euo pipefail
source /opt/venv/bin/activate

View File

@ -0,0 +1,24 @@
#!/bin/sh
set -euo pipefail
if [ "${BOUNCA_MIGRATE}" = "false" ]; then
echo "Not running migration"
exit 0
fi
echo "Migrating database"
cd /opt/bounca
./manage.py migrate
if [ "${BOUNCA_HOST}" != "*" ]; then
echo "Configure site URL"
./manage.py site ${BOUNCA_HOST}
fi
if [ -n "${BOUNCA_ADMIN_USER}" -a -n "${BOUNCA_ADMIN_PASSWORD}" ]; then
echo "Creating admin user ${BOUNCA_ADMIN_USER}"
export DJANGO_SUPERUSER_PASSWORD="${BOUNCA_ADMIN_PASSWORD}"
./manage.py createsuperuser --noinput --username ${BOUNCA_ADMIN_USER} --email ${BOUNCA_ADMIN_EMAIL} ||\
echo "Failed to create user ${BOUNCA_ADMIN_USER} (maybe it already exists ?)"
fi

View File

@ -0,0 +1,8 @@
[supervisord]
pidfile=/tmp/supervisord.pi
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
[include]
files = supervisord.d/*.ini

View File

@ -0,0 +1,10 @@
[program:gunicorn]
command=sh -c "source /opt/venv/bin/activate && gunicorn --bind=unix:${BOUNCA_UNIX_SOCKET} --threads=4 --max-requests=10000 bounca.wsgi:application"
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,10 @@
[program:nginx]
command=/usr/sbin/nginx -c /tmp/nginx.conf
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,10 @@
[program:pub-export]
command=/usr/local/bin/bounca-pub-export 300
stdout_logfile=/proc/self/fd/1
stdout_logfile_backups=0
stdout_logfile_maxbytes=0
stderr_logfile=/proc/self/fd/2
stderr_logfile_backups=0
stderr_logfile_maxbytes=0
autostart=true
autorestart=true

View File

@ -0,0 +1,2 @@
[group:bounca]
programs=gunicorn,nginx,pub-export

View File

@ -0,0 +1,34 @@
from bounca.settings import *
LOGGING: dict = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "%(levelname)s [%(asctime)s] %(name)s %(message)s",
},
"simple": {"format": "[%(asctime)s] %(message)s"},
},
"handlers": {
"null": {
"class": "logging.NullHandler",
},
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"mail_admins": {"level": "ERROR", "class": "django.utils.log.AdminEmailHandler"},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
"loggers": {},
}
TIME_ZONE = os.environ.get('TZ')
CSRF_TRUSTED_ORIGINS = [
"http://localhost:%d" % (os.environ.get('BOUNCA_BIND_ADDR').split(':'))[1],
"https://%s" % os.environ.get('BOUNCA_HOST')
]

View File

@ -0,0 +1,41 @@
psql:
dbname: ${BOUNCA_DB_NAME}
username: ${BOUNCA_DB_USER}
password: ${BOUNCA_DB_PASSWORD}
host: ${BOUNCA_DB_HOST}
port: ${BOUNCA_DB_PORT}
admin:
enabled: True
superuser_signup: False
django:
debug: False
secret_key: '${BOUNCA_DJANGO_SECRET}'
hosts:
# add your hosts here
- localhost
- 127.0.0.1
- bounca
- ${BOUNCA_HOST}
mail:
host: ${BOUNCA_SMTP_SERVER}
port: ${BOUNCA_SMTP_PORT}
# port: 587 optionally, only for tls and ssl
# username: <user>
# password: <password>
# connection: none # allowed values: none, tls, ssl
admin: ${BOUNCA_ADMIN_EMAIL}
from: ${BOUNCA_FROM_EMAIL}
certificate-engine:
# allowed values: ed25519, rsa
# Ed25519 is a a modern, fast and safe key algorithm, however not supported by all operating systems, like MacOS.
# Keep the 'rsa' option if unsure. Root and intermediate keys are 4096 bits, client and server certificates
# use 2048 bits keys.
key_algorithm: rsa
registration:
# allowed values: mandatory, optional, off
email_verification: off

View File

@ -0,0 +1,64 @@
worker_processes auto;
error_log /dev/stderr warn;
pid /tmp/nginx.pid;
daemon off;
events {
worker_connections 1024;
}
http {
proxy_temp_path /tmp/proxy_temp;
client_body_temp_path /tmp/client_temp;
fastcgi_temp_path /tmp/fastcgi_temp;
uwsgi_temp_path /tmp/uwsgi_temp;
scgi_temp_path /tmp/scgi_temp;
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /dev/stdout main;
sendfile on;
keepalive_timeout 65;
set_real_ip_from 127.0.0.1;
real_ip_header X-Forwarded-For;
real_ip_recursive on;
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
upstream bounca {
server unix:${BOUNCA_UNIX_SOCKET};
}
server {
listen ${BOUNCA_BIND_ADDR} default_server;
server_tokens off;
location /static {
root /opt/bounca/media;
}
location /public {
alias ${BOUNCA_PUBLIC_DIR};
}
location ~ ^/(api|admin) {
proxy_pass http://bounca;
}
location / {
root /opt/bounca/front/dist;
try_files $uri $uri/ /index.html;
}
}
}

View File

@ -0,0 +1,37 @@
#!/bin/sh
set -euo pipefail
export PGHOST=${BOUNCA_DB_HOST}
export PGPORT=${BOUNCA_DB_PORT}
export PGUSER=${BOUNCA_DB_USER}
export PGPASSWORD=${BOUNCA_DB_PASSWORD}
export PGDATABASE=${BOUNCA_DB_NAME}
mkdir -p ${BOUNCA_PUBLIC_DIR}
extract_pub(){
echo "Exporting public keys and CRL"
for CERT_ID in $(psql -A -q -t -c "SELECT id FROM x509_pki_certificate WHERE type IN ('R', 'I') AND revoked_at IS NULL;"); do
CERT_NAME=$(psql -A -q -t -c "SELECT name FROM x509_pki_certificate WHERE id='${CERT_ID}'")
echo "Exporting for certificate ${CERT_ID} (${CERT_NAME})"
psql -A -q -t -c "SELECT crt FROM x509_pki_keystore WHERE id='${CERT_ID}';" > ${BOUNCA_PUBLIC_DIR}/${CERT_ID}.crt
ln -sf ${BOUNCA_PUBLIC_DIR}/${CERT_ID}.crt "${BOUNCA_PUBLIC_DIR}/${CERT_NAME}.crt"
if [ "$(psql -A -q -t -c "SELECT COUNT(crl) from x509_pki_crlstore WHERE id='${CERT_ID}'")" != "0" ]; then
psql -A -q -t -c "SELECT crl FROM x509_pki_crlstore WHERE id='${CERT_ID}';" > ${BOUNCA_PUBLIC_DIR}/${CERT_ID}.crl
ln -sf ${BOUNCA_PUBLIC_DIR}/${CERT_ID}.crl ${BOUNCA_PUBLIC_DIR}/${CERT_NAME}.crl
fi
done
}
# Extract once when we start
extract_pub
# First arg of the script is an optional delay between exports.
# If set, the script keeps running and export certs and crl every X seconds
if [ ${1:-0} -gt 0 ]; then
while true; do
sleep ${1}
extract_pub;
done
fi

8
init/vault-bounca Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -euo pipefail
[[- template "common/vault.mkpgrole.sh.tpl"
dict "ctx" .
"config" (dict "role" .bounca.instance "database" "postgres")
]]

1
prep.d/10-mv-conf.sh Executable file
View File

@ -0,0 +1 @@
[[ template "common/mv_conf.sh.tpl" dict "ctx" . "services" (dict "bounca" .bounca.instance) ]]

17
prep.d/10-rand-pwd.sh Executable file
View File

@ -0,0 +1,17 @@
#!/bin/sh
set -euo pipefail
# Initialize random passwords if needed
if ! vault kv list [[ .vault.prefix ]]kv/service 2>/dev/null | grep -q -E '^[[ .bounca.instance ]]$'; then
vault kv put [[ .vault.prefix ]]kv/service/[[ .bounca.instance ]] \
django_secret=$(pwgen -s -n 50 1)
fi
for PWD in django_secret; do
if ! vault kv get -field ${PWD} [[ .vault.prefix ]]kv/service/[[ .bounca.instance ]] >/dev/null 2>&1; then
vault kv patch [[ .vault.prefix ]]kv/service/[[ .bounca.instance ]] \
${PWD}=$(pwgen -s -n 50 1)
fi
done

View File

@ -0,0 +1,36 @@
from bounca.settings import *
LOGGING: dict = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"verbose": {
"format": "%(levelname)s [%(asctime)s] %(name)s %(message)s",
},
"simple": {"format": "[%(asctime)s] %(message)s"},
},
"handlers": {
"null": {
"class": "logging.NullHandler",
},
"console": {
"class": "logging.StreamHandler",
"formatter": "simple",
},
"mail_admins": {"level": "ERROR", "class": "django.utils.log.AdminEmailHandler"},
},
"root": {
"level": "DEBUG",
"handlers": ["console"],
},
"loggers": {},
}
TIME_ZONE = os.environ.get('TZ')
CSRF_TRUSTED_ORIGINS = [
"[[ .bounca.public_url ]]"
]
[[ .bounca.django_custom_settings ]]

41
variables.yml Normal file
View File

@ -0,0 +1,41 @@
---
bounca:
# Name of this instance (controls job and service name)
instance: bounca
# The image to use
image: danielberteaud/bounca:0.4.4-1
# Env variable to pass to the container
env:
BOUNCA_DB_USER: '{{ with secret "[[ .vault.prefix ]]/database/creds/[[ .bounca.instance ]]" }}{{ .Data.username }}{{ end }}'
BOUNCA_DB_PASSWORD: '{{ with secret "[[ .vault.prefix ]]/database/creds/[[ .bounca.instance ]]" }}{{ .Data.password }}{{ end }}'
BOUNCA_DJANGO_SECRET: '{{ with secret "[[ .vault.prefix ]]/kv/service/[[ .bounca.instance ]]" }}{{ .Data.data.django_secret }}{{ end }}'
# Public URL where user can reach the app
public_url: https://pki.example.org
# Custom django settings
django_custom_settings: ""
# Wait for postgres to be ready before starting
wait_for:
- service: master.postgres[[ .consul.suffix ]]
# Connect to the postgres service through the service mesh]
consul:
connect:
upstreams:
- destination_name: postgres[[ .consul.suffix ]]
local_bind_port: 5432
# Traefik settings
traefik:
enabled: true
# Resource allocation for the main bounca task
resources:
cpu: 200
memory: 192

View File

@ -0,0 +1,7 @@
path "[[ .vault.prefix ]]kv/data/service/[[ .bounca.instance ]]" {
capabilities = ["read"]
}
path "[[ .vault.prefix ]]database/creds/[[ .bounca.instance ]]" {
capabilities = ["read"]
}