Cleanup, add missing wait_for, support pgbouncer

This commit is contained in:
Daniel Berteaud 2024-01-13 15:03:05 +01:00
parent 8053aacf4e
commit 40228e52a1
31 changed files with 805 additions and 41 deletions

View File

@ -1,4 +1,4 @@
job [[ .bounca.instance | toJSON ]] {
job [[ .instance | toJSON ]] {
[[- $c := merge .bounca . ]]
@ -10,7 +10,7 @@ job [[ .bounca.instance | toJSON ]] {
}
service {
name = "[[ .bounca.instance ]][[ .consul.suffix ]]"
name = "[[ .instance ]][[ .consul.suffix ]]"
port = 8749
[[ template "common/connect.tpl" $c ]]
@ -20,32 +20,35 @@ job [[ .bounca.instance | toJSON ]] {
[[- if $c.public.traefik.enabled ]]
[[ $p := merge .bounca.public . ]]
"[[ $p.traefik.instance ]].http.routers.[[ .bounca.instance ]]-public[[ .consul.suffix ]].rule=Host(`[[ (urlParse .bounca.public_url).Hostname ]]`) && PathPrefix(`[[ (urlParse .bounca.public_url).Path ]]/public/`)",
"[[ $p.traefik.instance ]].http.routers.[[ .bounca.instance ]]-public[[ .consul.suffix ]].priority=200",
"[[ $p.traefik.instance ]].http.routers.[[ .bounca.instance ]]-public[[ .consul.suffix ]].entrypoints=[[ join $p.traefik.entrypoints "," ]]",
"[[ $p.traefik.instance ]].http.routers.[[ .instance ]]-public[[ .consul.suffix ]].rule=Host(`[[ (urlParse .bounca.public_url).Hostname ]]`) && PathPrefix(`[[ (urlParse .bounca.public_url).Path ]]/public/`)",
"[[ $p.traefik.instance ]].http.routers.[[ .instance ]]-public[[ .consul.suffix ]].priority=200",
"[[ $p.traefik.instance ]].http.routers.[[ .instance ]]-public[[ .consul.suffix ]].entrypoints=[[ join $p.traefik.entrypoints "," ]]",
[[- if not (regexp.Match "^/?$" (urlParse .bounca.public_url).Path) ]]
"[[ $p.traefik.instance ]].http.middlewares.[[ .bounca.instance ]]-public[[ .consul.suffix ]]-prefix.stripprefix.prefixes=[[ (urlParse .bounca.public_url).Path ]]",
"[[ $p.traefik.instance ]].http.routers.[[ .bounca.instance ]]-public[[ .consul.suffix ]].middlewares=[[ .bounca.instance ]]-public[[ .consul.suffix ]]-prefix,[[ template "common/traefik_middlewares.tpl" $p.traefik ]]",
"[[ $p.traefik.instance ]].http.middlewares.[[ .instance ]]-public[[ .consul.suffix ]]-prefix.stripprefix.prefixes=[[ (urlParse .bounca.public_url).Path ]]",
"[[ $p.traefik.instance ]].http.routers.[[ .instance ]]-public[[ .consul.suffix ]].middlewares=[[ .instance ]]-public[[ .consul.suffix ]]-prefix,[[ template "common/traefik_middlewares.tpl" $p.traefik ]]",
[[- else ]]
"[[ $p.traefik.instance ]].http.routers.[[ .bounca.instance ]]-public[[ .consul.suffix ]].middlewares=[[ template "common/traefik_middlewares.tpl" $p.traefik ]]",
"[[ $p.traefik.instance ]].http.routers.[[ .instance ]]-public[[ .consul.suffix ]].middlewares=[[ template "common/traefik_middlewares.tpl" $p.traefik ]]",
[[- end ]]
[[- end ]]
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ .consul.suffix ]].rule=Host(`[[ (urlParse .bounca.public_url).Hostname ]]`)
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .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 ]][[ .consul.suffix ]].priority=100",
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ .consul.suffix ]].entrypoints=[[ join $c.traefik.entrypoints "," ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].priority=100",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].entrypoints=[[ join $c.traefik.entrypoints "," ]]",
[[- if not (regexp.Match "^/?$" (urlParse .bounca.public_url).Path) ]]
"[[ $c.traefik.instance ]].http.middlewares.[[ .bounca.instance ]][[ .consul.suffix ]]-prefix.stripprefix.prefixes=[[ (urlParse .bounca.public_url).Path ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ .consul.suffix ]].middlewares=[[ .bounca.instance ]][[ .consul.suffix ]]-prefix,[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
"[[ $c.traefik.instance ]].http.middlewares.[[ .instance ]][[ .consul.suffix ]]-prefix.stripprefix.prefixes=[[ (urlParse .bounca.public_url).Path ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].middlewares=[[ .instance ]][[ .consul.suffix ]]-prefix,[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
[[- else ]]
"[[ $c.traefik.instance ]].http.routers.[[ .bounca.instance ]][[ .consul.suffix ]].middlewares=[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
"[[ $c.traefik.instance ]].http.routers.[[ .instance ]][[ .consul.suffix ]].middlewares=[[ template "common/traefik_middlewares.tpl" $c.traefik ]]",
[[- end ]]
]
}
[[ template "common/task.wait_for" $c ]]
[[ template "common/postgres_pooler" $c ]]
task "bounca" {
driver = [[ $c.nomad.driver | toJSON ]]
user = 8749
@ -58,11 +61,7 @@ job [[ .bounca.instance | toJSON ]] {
volumes = ["local/docker_settings.py:/opt/bounca/bounca/docker_settings.py:ro"]
}
vault {
policies = ["[[ .bounca.instance ]][[ .consul.suffix ]]"]
disable_file = true
env = false
}
[[ template "common/vault.policies" $c ]]
env {
BOUNCA_MODE = "server"
@ -79,6 +78,21 @@ _EOT
destination = "local/docker_settings.py"
}
template {
data =<<_EOT
[[- if ne $c.postgres.pooler.engine "none" ]]
BOUNCA_DB_USER=[[ .instance ]]
BOUNCA_DB_PASSWORD={{ env "NOMAD_ALLOC_ID" }}
BOUNCA_DB_PORT=6432
[[- else ]]
BOUNCA_DB_USER={{ with secret "[[ .vault.prefix ]]/database/creds/[[ .instance ]]" }}{{ .Data.username }}{{ end }}
BOUNCA_DB_PASSWORD={{ with secret "[[ .vault.prefix ]]/database/creds/[[ .instance ]]" }}{{ .Data.password }}{{ end }}
BOUNCA_DB_PORT=[[ $c.postgres.port ]]
[[- end ]]
_EOT
destination = "secrets/.db.env"
}
[[ template "common/resources.tpl" $c.resources ]]
}
@ -98,11 +112,7 @@ _EOT
[[ template "common/tmpfs.tpl" dict "target" "/tmp" "size" 1000000 ]]
}
vault {
policies = ["[[ .bounca.instance ]][[ .consul.suffix ]]"]
disable_file = true
env = false
}
[[ template "common/vault.policies" $c ]]
env {
BOUNCA_MODE = "public-exporter"

View File

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

View File

@ -1,8 +1,8 @@
Kind = "service-intentions"
Name = "[[ .bounca.instance ]][[ .consul.suffix ]]"
Name = "[[ .instance ]][[ .consul.suffix ]]"
Sources = [
{
Name = "[[ .traefik.instance ]]"
Name = "[[ (merge .bounca.traefik .traefik).instance ]]"
Permissions = [
{
Action = "allow"

9
example/LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 dani
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

3
example/README.md Normal file
View File

@ -0,0 +1,3 @@
# bounca
Create a PKI, sign and revoke server and client X.509 v3 SSL certificates

292
example/bounca.nomad.hcl Normal file
View File

@ -0,0 +1,292 @@
job "bounca" {
datacenters = ["dc1"]
group "bounca" {
network {
mode = "bridge"
}
service {
name = "bounca"
port = 8749
connect {
sidecar_service {
proxy {
upstreams {
destination_name = "postgres"
local_bind_port = 5432
}
}
}
sidecar_task {
resources {
cpu = 50
memory = 64
}
}
}
tags = [
"traefik.enable=true",
"traefik.http.routers.bounca-public.rule=Host(`pki.example.org`) && PathPrefix(`/public/`)",
"traefik.http.routers.bounca-public.priority=200",
"traefik.http.routers.bounca-public.entrypoints=https",
"traefik.http.routers.bounca-public.middlewares=rate-limit-std@file,inflight-std@file,security-headers@file,hsts@file,compression@file,csp-relaxed@file",
"traefik.http.routers.bounca.rule=Host(`pki.example.org`)",
"traefik.http.routers.bounca.priority=100",
"traefik.http.routers.bounca.entrypoints=https",
"traefik.http.routers.bounca.middlewares=rate-limit-std@file,inflight-std@file,security-headers@file,hsts@file,compression@file,csp-relaxed@file",
]
}
# wait for required services tp be ready before starting the main task
task "wait-for" {
driver = "docker"
user = 1053
config {
image = "danielberteaud/wait-for:24.1-1"
readonly_rootfs = true
pids_limit = 20
}
lifecycle {
hook = "prestart"
}
env {
SERVICE_0 = "master.postgres.service.consul"
}
resources {
cpu = 10
memory = 10
memory_max = 30
}
}
task "bounca" {
driver = "docker"
user = 8749
config {
image = "danielberteaud/bounca:latest"
pids_limit = 50
readonly_rootfs = true
mount {
type = "tmpfs"
target = "/tmp"
tmpfs_options {
size = 1000000
}
}
volumes = ["local/docker_settings.py:/opt/bounca/bounca/docker_settings.py:ro"]
}
vault {
policies = ["bounca"]
env = false
disable_file = true
}
env {
BOUNCA_MODE = "server"
BOUNCA_UNIX_SOCKET = "/alloc/data/bounca.sock"
BOUNCA_HOST = "pki.example.org"
}
# Use a template block instead of env {} so we can fetch values from vault
template {
data = <<_EOT
BOUNCA_DB_NAME=bounca
BOUNCA_DJANGO_SECRET={{ with secret "/kv/service/bounca" }}{{ .Data.data.django_secret }}{{ end }}
LANG=fr_FR.utf8
TZ=Europe/Paris
_EOT
destination = "secrets/.env"
perms = 400
env = true
}
template {
data = <<_EOT
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 = [
"https://pki.example.org"
]
_EOT
destination = "local/docker_settings.py"
}
template {
data = <<_EOT
BOUNCA_DB_USER={{ with secret "/database/creds/bounca" }}{{ .Data.username }}{{ end }}
BOUNCA_DB_PASSWORD={{ with secret "/database/creds/bounca" }}{{ .Data.password }}{{ end }}
BOUNCA_DB_PORT=5432
_EOT
destination = "secrets/.db.env"
}
resources {
cpu = 200
memory = 192
}
}
task "public-exporter" {
driver = "docker"
user = 8749
lifecycle {
hook = "poststart"
sidecar = true
}
config {
image = "danielberteaud/bounca:latest"
pids_limit = 50
readonly_rootfs = true
mount {
type = "tmpfs"
target = "/tmp"
tmpfs_options {
size = 1000000
}
}
}
vault {
policies = ["bounca"]
env = false
disable_file = true
}
env {
BOUNCA_MODE = "public-exporter"
BOUNCA_PUBLIC_DIR = "/alloc/data/public"
}
# Use a template block instead of env {} so we can fetch values from vault
template {
data = <<_EOT
BOUNCA_DB_NAME=bounca
BOUNCA_DJANGO_SECRET={{ with secret "/kv/service/bounca" }}{{ .Data.data.django_secret }}{{ end }}
LANG=fr_FR.utf8
TZ=Europe/Paris
_EOT
destination = "secrets/.env"
perms = 400
env = true
}
resources {
cpu = 10
memory = 10
memory_max = 20
}
}
task "nginx" {
driver = "docker"
user = 8749
lifecycle {
hook = "poststart"
sidecar = true
}
config {
image = "danielberteaud/bounca:latest"
pids_limit = 30
readonly_rootfs = true
mount {
type = "tmpfs"
target = "/tmp"
tmpfs_options {
size = 1000000
}
}
}
env {
BOUNCA_MODE = "front"
BOUNCA_BIND_ADDR = "127.0.0.1:8749"
BOUNCA_UNIX_SOCKET = "/alloc/data/bounca.sock"
BOUNCA_PUBLIC_DIR = "/alloc/data/public"
BOUNCA_HOST = "pki.example.org"
}
resources {
cpu = 20
memory = 20
}
}
}
}

View File

@ -0,0 +1,3 @@
Kind = "service-defaults"
Name = "bounca"
Protocol = "http"

View File

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

View File

@ -0,0 +1,83 @@
# 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 \
git \
nodejs \
npm \
make \
&&\
git clone --depth=1 --branch=release/${BOUNCA_VERSION} https://gitlab.com/bounca/bounca.git &&\
cd bounca &&\
rm -rf .git &&\
python3 -m venv venv &&\
source /opt/bounca/venv/bin/activate &&\
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 --link --noinput &&\
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 &&\
rm -rf docs &&\
find /opt/bounca -type d -name '__pycache__' -exec rm -rf "{}" + &&\
cd /opt/bounca/front &&\
npm install &&\
export NODE_OPTIONS=--openssl-legacy-provider &&\
make production &&\
find ./ -maxdepth 1 \! -name dist -exec rm -rf "{}" \; &&\
rm -f results.txt &&\
rdfind /opt
FROM python:3.11-alpine
MAINTAINER Daniel Berteaud <dbd@ehtrace.com>
ENV PATH=/opt/bounca/venv/bin:${PATH} \
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 \
BOUNCA_MODE=all-in-one
ADD https://git.lapiole.org/nomad/base_tools.git#master /
COPY --from=builder /opt /opt
RUN set -euxo pipefail &&\
apk --no-cache add \
tini \
curl \
gettext \
openssl \
nginx \
supervisor \
postgresql15-client
COPY root/ /
WORKDIR /opt/bounca
EXPOSE 8749
ENTRYPOINT ["tini", "--", "/entrypoint.sh"]
CMD ["bounca"]

View File

@ -0,0 +1,14 @@
#!/bin/sh
set -euo pipefail
if [ "${BOUNCA_MODE}" = "all-in-one" -o "${BOUNCA_MODE}" = "server" ]; then
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
fi
if [ "${BOUNCA_MODE}" = "all-in-one" -o "${BOUNCA_MODE}" = "front" ]; then
echo "Populating /tmp/nginx.conf from /opt/bounca/etc/nginx/nginx.conf.template"
envsubst < /opt/bounca/etc/nginx/nginx.conf.template > /tmp/nginx.conf
fi

View File

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

View File

@ -0,0 +1,24 @@
#!/bin/sh
set -euo pipefail
if [ "${BOUNCA_MODE}" != "all-in-one" -a "${BOUNCA_MODE}" != "server" ]; 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/bounca/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,35 @@
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,67 @@
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};
autoindex on;
autoindex_localtime on;
autoindex_exact_size off;
}
location ~ ^/(api|admin) {
proxy_pass http://bounca;
}
location / {
root /opt/bounca/front/dist;
try_files $uri $uri/ /index.html =404;
}
}
}

View File

@ -0,0 +1,22 @@
#!/bin/sh
set -euo pipefail
source /opt/bounca/venv/bin/activate
if [ "${BOUNCA_MODE}" = "all-in-one" ]; then
exec supervisor -c /etc/supervisord.conf -n
elif [ "${BOUNCA_MODE}" = "server" ]; then
exec gunicorn \
--bind=unix:/alloc/data/bounca.sock \
--threads=4 \
--max-requests=10000 \
bounca.wsgi:application
elif [ "${BOUNCA_MODE}" = "front" ]; then
exec nginx -c /tmp/nginx.conf
elif [ "${BOUNCA_MODE}" = "public-exporter" ]; then
exec bounca-pub-export 300
else
echo "unknown mode (${BOUNCA_MODE})"
exit 1
fi

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

12
example/init/vault-bounca Executable file
View File

@ -0,0 +1,12 @@
#!/bin/sh
set -euo pipefail
vault write database/roles/bounca \
db_name="postgres" \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT \"bounca\" TO \"{{name}}\"; \
ALTER ROLE \"{{name}}\" SET role = \"bounca\"" \
default_ttl="12h" \
max_ttl="720h"

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

@ -0,0 +1,19 @@
#!/bin/sh
set -eu
if [ "bounca" != "bounca" ]; then
for DIR in vault consul nomad; do
if [ -d output/${DIR} ]; then
for FILE in $(find output/${DIR} -name "*bounca*.hcl" -type f); do
NEW_FILE=$(echo "${FILE}" | sed -E "s/bounca/bounca/g")
mv "${FILE}" "${NEW_FILE}"
done
fi
done
fi

17
example/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 kv/service 2>/dev/null | grep -q -E '^bounca$'; then
vault kv put kv/service/bounca \
django_secret=$(pwgen -s -n 50 1)
fi
for PWD in django_secret; do
if ! vault kv get -field ${PWD} kv/service/bounca >/dev/null 2>&1; then
vault kv patch kv/service/bounca \
${PWD}=$(pwgen -s -n 50 1)
fi
done

View File

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

View File

@ -4,5 +4,5 @@ set -euo pipefail
[[- template "common/vault.mkpgrole.sh.tpl"
dict "ctx" .
"config" (dict "role" .bounca.instance "database" "postgres")
"config" (dict "role" .instance "database" "postgres")
]]

View File

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

View File

@ -4,14 +4,14 @@ 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 ]] \
if ! vault kv list [[ .vault.prefix ]]kv/service 2>/dev/null | grep -q -E '^[[ .instance ]]$'; then
vault kv put [[ .vault.prefix ]]kv/service/[[ .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 ]] \
if ! vault kv get -field ${PWD} [[ .vault.prefix ]]kv/service/[[ .instance ]] >/dev/null 2>&1; then
vault kv patch [[ .vault.prefix ]]kv/service/[[ .instance ]] \
${PWD}=$(pwgen -s -n 50 1)
fi
done

View File

@ -1,18 +1,27 @@
---
bounca:
# Name of this instance (controls job and service name)
instance: bounca
# Name of this instance (controls job and service name)
instance: bounca
bounca:
# The image to use
image: danielberteaud/bounca:latest
# Vault policies to use
vault:
policies:
- '[[ .instance ]][[ .consul.suffix ]]'
postgres:
database: '[[ .instance ]]'
user: '{{ with secret "[[ .vault.prefix ]]/database/creds/[[ .instance ]]" }}{{ .Data.username }}{{ end }}'
password: '{{ with secret "[[ .vault.prefix ]]/database/creds/[[ .instance ]]" }}{{ .Data.password }}{{ end }}'
# 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 }}'
BOUNCA_DB_NAME: '[[ .bounca.postgres.database ]]'
BOUNCA_DJANGO_SECRET: '{{ with secret "[[ .vault.prefix ]]/kv/service/[[ .instance ]]" }}{{ .Data.data.django_secret }}{{ end }}'
# Public URL where user can reach the app
public_url: https://pki.example.org

View File

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