summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.envrc1
-rw-r--r--.gitignore4
-rw-r--r--10-renewcerts22
-rw-r--r--ansible.cfg6
-rw-r--r--flake.lock42
-rw-r--r--flake.nix21
-rw-r--r--haproxy.conf.j2107
-rw-r--r--inventory.yaml4
-rw-r--r--jails/datagubbe/nginx.conf.j251
-rw-r--r--jails/datagubbe/tasks.yaml52
-rw-r--r--jails/gubbhub/cgit.nginx.conf.j241
-rw-r--r--jails/gubbhub/cgitrc.j220
-rw-r--r--jails/gubbhub/gubbshell/create-repo59
-rw-r--r--jails/gubbhub/gubbshell/delete-repo33
-rw-r--r--jails/gubbhub/gubbshell/edit-repo16
-rw-r--r--jails/gubbhub/gubbshell/help21
-rw-r--r--jails/gubbhub/gubbshell/list-repos12
-rw-r--r--jails/gubbhub/tasks.yaml163
-rw-r--r--jails/gubbhub/variables.yaml9
-rwxr-xr-xlibrary/jexec.sh17
-rw-r--r--playbook.yaml199
-rw-r--r--roles/jail/meta/main.yml2
-rw-r--r--roles/jail/tasks/main.yml34
-rw-r--r--roles/jail/templates/jail.conf.j218
-rw-r--r--roles/jailhost/handlers/main.yml9
-rw-r--r--roles/jailhost/tasks/main.yml64
-rw-r--r--roles/pf/handlers/main.yml15
-rw-r--r--roles/pf/tasks/main.yml19
-rw-r--r--roles/pf/templates/pf.conf.j256
-rw-r--r--templates/nginx-certbot.conf.j249
30 files changed, 1166 insertions, 0 deletions
diff --git a/.envrc b/.envrc
new file mode 100644
index 0000000..3550a30
--- /dev/null
+++ b/.envrc
@@ -0,0 +1 @@
+use flake
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d11ab27
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+/.cache/
+/.direnv/
+
+/result*
diff --git a/10-renewcerts b/10-renewcerts
new file mode 100644
index 0000000..f08ed9e
--- /dev/null
+++ b/10-renewcerts
@@ -0,0 +1,22 @@
+#!/bin/sh
+#
+# $FreeBSD$
+#
+# Renew certbot certificates
+#
+
+# If there is a global system configuration file, suck it in.
+#
+if [ -r /etc/defaults/periodic.conf ]
+then
+ . /etc/defaults/periodic.conf
+ source_periodic_confs
+fi
+
+case "$daily_renewcerts_enable" in
+ [Yy][Ee][Ss])
+ certbot renew && rc=0 || rc=3;;
+ *) rc=0;;
+esac
+
+exit $rc
diff --git a/ansible.cfg b/ansible.cfg
new file mode 100644
index 0000000..76e278b
--- /dev/null
+++ b/ansible.cfg
@@ -0,0 +1,6 @@
+[defaults]
+remote_port = 2223
+remote_user = ansible
+
+fact_caching = jsonfile
+fact_caching_connection = .cache/facts
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 0000000..b0c3157
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,42 @@
+{
+ "nodes": {
+ "flake-utils": {
+ "locked": {
+ "lastModified": 1659877975,
+ "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1664594436,
+ "narHash": "sha256-YHowMADGzdi7fKnGlg47qe0PIljq+11VqLarmXDuKxQ=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "9cac45850280978a21a3eb67b15a18f34cbffa2d",
+ "type": "github"
+ },
+ "original": {
+ "id": "nixpkgs",
+ "ref": "nixos-22.05",
+ "type": "indirect"
+ }
+ },
+ "root": {
+ "inputs": {
+ "flake-utils": "flake-utils",
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 0000000..dc097a4
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,21 @@
+{
+ description = "Configuration for datagubbar";
+
+ inputs.nixpkgs.url = "nixpkgs/nixos-22.05";
+ inputs.flake-utils.url = "github:numtide/flake-utils";
+
+ outputs = { self, nixpkgs, flake-utils }:
+ flake-utils.lib.eachDefaultSystem (system:
+ {
+ packages = with nixpkgs.legacyPackages."${system}"; {
+ default = stdenv.mkDerivation {
+ name = "datagubbe-setup";
+ src = ./.;
+ nativeBuildInputs = [
+ ansible
+ ];
+ };
+ };
+ }
+ );
+}
diff --git a/haproxy.conf.j2 b/haproxy.conf.j2
new file mode 100644
index 0000000..afe65f6
--- /dev/null
+++ b/haproxy.conf.j2
@@ -0,0 +1,107 @@
+global
+ # all file names are relative to the directory containing this config
+ # file by default
+ default-path config
+
+ # refuse to start if any warning is emitted at boot (keep configs clean)
+ zero-warning
+
+ # Security hardening: isolate and drop privileges
+ chroot /var/empty
+ user haproxy
+ group haproxy
+
+ # daemonize
+ daemon
+ pidfile /var/run/haproxy-svc1.pid
+
+ # do not keep old processes longer than that after a reload
+ hard-stop-after 5m
+
+ # The command-line-interface (CLI) used by the admin, by provisionning
+ # tools, and to transfer sockets during reloads
+ stats socket /var/run/haproxy-svc1.sock level admin mode 600 user haproxy expose-fd listeners
+ stats timeout 1h
+
+ # send logs to stderr for logging via the service manager
+ log stderr local0 info
+
+ # intermediate security for SSL, from https://ssl-config.mozilla.org/
+ ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384
+ ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
+ ssl-default-bind-options prefer-client-ciphers no-sslv3 no-tlsv10 no-tlsv11 no-tls-tickets
+
+# default settings common to all HTTP proxies below
+defaults http
+ mode http
+ option httplog
+ log global
+ timeout client 1m
+ timeout server 1m
+ timeout connect 10s
+ timeout http-keep-alive 2m
+ timeout queue 15s
+ timeout tunnel 4h # for websocket
+
+defaults tcp
+ mode tcp
+ option tcplog
+ timeout client 1h
+ timeout server 1h
+ timeout connect 10s
+ log global
+
+# provide a stats page on port 8181
+frontend stats from http
+ bind localhost:8181
+ # provide advanced stats (ssl, h2, ...)
+ stats uri /
+ stats show-modules
+ stats admin if { src 127.0.0.0/8 }
+
+frontend http from http
+ bind :80
+ option socket-stats # provide per-bind line stats
+
+ acl is_certbot path_beg -i /.well-known/acme-challenge
+ http-request redirect scheme https code 301 if !is_certbot
+
+ # silently ignore connect probes and pre-connect without request
+ option http-ignore-probes
+
+ # pass client's IP address to the server and prevent against attempts
+ # to inject bad contents
+ http-request del-header x-forwarded-for
+ option forwardfor
+
+ default_backend certbot
+
+{% if certificates is defined and certificates|length > 0 %}
+frontend https from http
+ bind :443 name secure ssl {%- for cert in certificates %} crt /usr/local/etc/haproxy/certs/{{ cert }}.pem {%- endfor %} alpn h2,http/1.1
+
+ # set HSTS for one year after all responses
+ http-after-response set-header Strict-Transport-Security "max-age=31536000"
+ http-request redirect scheme https code 301 if !{ ssl_fc }
+
+ option http-ignore-probes
+
+ # pass client's IP address to the server and prevent against attempts
+ # to inject bad contents
+ http-request del-header x-forwarded-for
+ option forwardfor
+
+ # enable HTTP compression of text contents
+ compression algo deflate gzip
+ compression type text/ application/javascript application/xhtml+xml image/x-icon
+
+ use_backend %[req.hdr(Host),lower]
+ default_backend datagubbe
+{% endif %}
+
+{% for jailname, jail in jails.items() %}
+ {{- jail.haproxy_conf | default('') }}
+{% endfor %}
+
+backend certbot from http
+ server srv localhost:7878
diff --git a/inventory.yaml b/inventory.yaml
new file mode 100644
index 0000000..f363b91
--- /dev/null
+++ b/inventory.yaml
@@ -0,0 +1,4 @@
+all:
+ hosts:
+ datagubbe.dev:
+ ansible_port: 2223
diff --git a/jails/datagubbe/nginx.conf.j2 b/jails/datagubbe/nginx.conf.j2
new file mode 100644
index 0000000..d772466
--- /dev/null
+++ b/jails/datagubbe/nginx.conf.j2
@@ -0,0 +1,51 @@
+worker_processes 1;
+
+# This default error log path is compiled-in to make sure configuration parsing
+# errors are logged somewhere, especially during unattended boot when stderr
+# isn't normally logged anywhere. This path will be touched on every nginx
+# start regardless of error log location configured here. See
+# https://trac.nginx.org/nginx/ticket/147 for more info.
+#
+#error_log /var/log/nginx/error.log;
+#
+
+#pid logs/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include 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 logs/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ #keepalive_timeout 0;
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ server {
+ listen [::]:80;
+ server_name {{ server_name }};
+
+ #charset koi8-r;
+
+ #access_log logs/host.access.log main;
+
+ location / {
+ root {{ root }};
+ index index.html index.htm;
+ }
+ }
+}
diff --git a/jails/datagubbe/tasks.yaml b/jails/datagubbe/tasks.yaml
new file mode 100644
index 0000000..b10889c
--- /dev/null
+++ b/jails/datagubbe/tasks.yaml
@@ -0,0 +1,52 @@
+- name: create webroot
+ jexec:
+ cmd: mkdir -p /var/www/html
+ jail: "{{ jail.name }}"
+
+- name: create index file
+ ansible.builtin.copy:
+ content: |
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Welcome to datagubbe.dev</title>
+ <style type="text/css">
+ body {
+ background-color: #222;
+ color: #ccc;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>Datagubbe</h1>
+ </body>
+ </html>
+ dest: "{{ jailbase }}/{{ jail.name }}/var/www/html/index.html"
+
+- name: install nginx
+ community.general.pkgng:
+ name:
+ - nginx
+ state: latest
+ jail: "{{ jail.name }}"
+
+- name: create nginx config
+ ansible.builtin.template:
+ src: nginx.conf.j2
+ dest: "{{ jailbase }}/{{ jail.name }}/usr/local/etc/nginx/nginx.conf"
+ vars:
+ root: "/var/www/html"
+ server_name: "datagubbe.dev"
+
+- name: enable nginx
+ community.general.sysrc:
+ name: nginx_enable
+ value: "YES"
+ jail: "{{ jail.name }}"
+
+- name: start nginx
+ jexec:
+ cmd: service nginx status || service nginx start
+ jail: "{{ jail.name }}"
+
diff --git a/jails/gubbhub/cgit.nginx.conf.j2 b/jails/gubbhub/cgit.nginx.conf.j2
new file mode 100644
index 0000000..3593e5b
--- /dev/null
+++ b/jails/gubbhub/cgit.nginx.conf.j2
@@ -0,0 +1,41 @@
+worker_processes 1;
+
+events {
+ worker_connections 1024;
+}
+
+http {
+ include mime.types;
+ default_type application/octet-stream;
+ sendfile on;
+ keepalive_timeout 65;
+ gzip on;
+
+ # Cgit
+ server {
+ listen [::]:80;
+ server_name git.datagubbe.dev;
+ root /usr/local/www/cgit;
+ try_files $uri @cgit;
+
+ # Configure HTTP transport
+ location ~ /.+/(info/refs|git-upload-pack) {
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME /usr/local/libexec/git-core/git-http-backend;
+ fastcgi_param PATH_INFO $uri;
+ fastcgi_param GIT_HTTP_EXPORT_ALL 1;
+ fastcgi_param GIT_PROJECT_ROOT /git/repos;
+ fastcgi_param HOME /git/repos;
+ fastcgi_pass unix:/var/run/fcgiwrap/fcgiwrap.sock;
+ }
+
+ location @cgit {
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root/cgit.cgi;
+ fastcgi_param PATH_INFO $uri;
+ fastcgi_param QUERY_STRING $args;
+ fastcgi_param HTTP_HOST $server_name;
+ fastcgi_pass unix:/var/run/fcgiwrap/fcgiwrap.sock;
+ }
+ }
+}
diff --git a/jails/gubbhub/cgitrc.j2 b/jails/gubbhub/cgitrc.j2
new file mode 100644
index 0000000..ef1f50b
--- /dev/null
+++ b/jails/gubbhub/cgitrc.j2
@@ -0,0 +1,20 @@
+css=/cgit.css
+logo=/cgit.png
+
+root-title=git.datagubbe.dev
+root-desc=Git hosting for datagubbar
+
+# disallow http transport git clone, this is handled with git
+enable-http-clone=0
+clone-url=https://$HTTP_HOST/$CGIT_REPO_URL
+
+# if you do not want that webcrawler (like google) index your site
+robots=noindex, nofollow
+
+# if cgit messes up links, use a virtual-root. For example, cgit.example.org/ has this value:
+virtual-root=/
+
+snapshots=tar.xz tar.gz zip
+
+section-from-path=1
+scan-path=/git/repos/
diff --git a/jails/gubbhub/gubbshell/create-repo b/jails/gubbhub/gubbshell/create-repo
new file mode 100644
index 0000000..07c7aa4
--- /dev/null
+++ b/jails/gubbhub/gubbshell/create-repo
@@ -0,0 +1,59 @@
+#! /usr/bin/env sh
+
+set -euo pipefail
+
+if ! id -nG | grep -qwF "gitadm"; then
+ echo $'\e[31myou are not an admin\e[0m'
+ exit 13
+fi
+
+if [ $# -lt 1 ]; then
+ echo $'\e[31mrepository name is required\e[0m'
+ exit 1
+fi
+
+read_with_default() {
+ local var="$1"
+ local title="$2"
+ local default="$3"
+
+ read -p $'\e[36m'"$title"$'\e[0m \e[1m['"$default"$']\e[0m: ' $var
+ eval vval="\$$var"
+ eval $var="\"${vval:-$default}\""
+}
+
+name="$1"
+path="$(echo "$1" | sed 's/ /-/' | tr '[:upper:]' '[:lower:]').git"
+
+if [ -e "$path" ]; then
+ echo $'\e[31m'"repository at \"$path\" already exists"$'\e[0m'
+ exit 1
+fi
+
+read_with_default defbranch "default branch" "main"
+read_with_default dispname "display name" "$name"
+read_with_default owner "owner" "$(id -un)"
+read_with_default shared "shared with 'gitdev' group" "yes"
+
+echo $'\e[36mdescription\e[0m (terminate with C-d): '
+description=""
+while read -r descline; do
+ description="$description$descline\n"
+done
+
+echo "creating repo at: $path..."
+sharerepo="false"
+case "$(echo "$shared" | tr '[:upper:]' '[:lower:]')" in
+ y|yes|ye|true|t) sharerepo="group" ;;
+esac
+git init --bare --shared="$sharerepo" --initial-branch="$defbranch" "$path"
+
+echo "generating web config..."
+cat <<EOF > "$path/cgitrc"
+name=$dispname
+owner=$owner
+desc=${description%\\n}
+EOF
+
+echo "browse new repo at: https://git.datagubbe.dev/$path"
+
diff --git a/jails/gubbhub/gubbshell/delete-repo b/jails/gubbhub/gubbshell/delete-repo
new file mode 100644
index 0000000..ee8b2c6
--- /dev/null
+++ b/jails/gubbhub/gubbshell/delete-repo
@@ -0,0 +1,33 @@
+#! /usr/bin/env sh
+
+set -euo pipefail
+
+if ! id -nG | grep -qwF "gitadm"; then
+ echo $'\e[31myou are not an admin\e[0m'
+ exit 13
+fi
+
+if [ $# -lt 1 ]; then
+ echo $'\e[31mrepository name is required\e[0m'
+ exit 1
+fi
+
+name="$1"
+path="$(echo "$1" | sed 's/ /-/' | tr '[:upper:]' '[:lower:]').git"
+
+read -p $'\e[33mare you sure you want to delete the repo '"\"$name\" at \"$path\""$'\e[0m \e[1m[no]\e[0m: ' confirm
+
+delete="false"
+case "$(echo "$confirm" | tr '[:upper:]' '[:lower:]')" in
+ y|yes|ye) delete="true" ;;
+esac
+
+if [ "$delete" = "true" ]; then
+ echo -n "deleting repository \"$name\"..."
+ rm -rf "$path"
+ echo "done!"
+fi
+
+# delete the dir if empty
+dir=$(dirname "$path")
+find "$dir" -maxdepth 0 -type d -delete
diff --git a/jails/gubbhub/gubbshell/edit-repo b/jails/gubbhub/gubbshell/edit-repo
new file mode 100644
index 0000000..6ceb6f3
--- /dev/null
+++ b/jails/gubbhub/gubbshell/edit-repo
@@ -0,0 +1,16 @@
+#! /usr/bin/env sh
+set -euo pipefail
+
+if ! id -nG | grep -qwF "gitadm"; then
+ echo $'\e[31myou are not an admin\e[0m'
+ exit 13
+fi
+
+if [ $# -lt 1 ]; then
+ echo $'\e[31mrepository name is required\e[0m'
+ exit 1
+fi
+
+path="$(echo "$1" | sed 's/ /-/' | tr '[:upper:]' '[:lower:]').git"
+
+nano -R "$path/cgitrc"
diff --git a/jails/gubbhub/gubbshell/help b/jails/gubbhub/gubbshell/help
new file mode 100644
index 0000000..5ea3e62
--- /dev/null
+++ b/jails/gubbhub/gubbshell/help
@@ -0,0 +1,21 @@
+#! /usr/bin/env sh
+
+printf "Hi $(id -un) and welcome to the gubbshell 👴🐚!
+
+If you have administrative rights this shell can be used to manage repos.
+
+Commands:
+ - \033[1mcreate-repo\033[0m NAME
+ create a new repository called NAME. The command will
+ prompt for needed information. To create a repo in a group use <group>/name.
+
+ - \033[1mdelete-repo\033[0m NAME
+ delete the repository NAME. Note that this is an irreversible operation.
+
+ - \033[1mlist-repos\033[0m
+ list current repos
+
+ - \033[1medit-repo\033[0m NAME
+ edit repository information for NAME. This will open a restricted editor (nano)
+ for editing the repo-specific cgitrc file.
+"
diff --git a/jails/gubbhub/gubbshell/list-repos b/jails/gubbhub/gubbshell/list-repos
new file mode 100644
index 0000000..a33a84b
--- /dev/null
+++ b/jails/gubbhub/gubbshell/list-repos
@@ -0,0 +1,12 @@
+#! /usr/bin/env sh
+
+set -euo pipefail
+
+printf '%-32s %-32s %s\n' 'DISPLAY NAME' 'OWNER' 'PATH'
+echo '-------------------------------------------------------------------------------------------'
+cd /git/repos && find . -name '*.git' -type d |
+while read repo; do
+ name="$(grep "name=" "$repo/cgitrc" | sed 's/^.*=//')"
+ owner="$(grep "owner=" "$repo/cgitrc" | sed 's/^.*=//')"
+ printf '%-32s %-32s %s\n' "$name" "$owner" "$repo"
+done
diff --git a/jails/gubbhub/tasks.yaml b/jails/gubbhub/tasks.yaml
new file mode 100644
index 0000000..4f8c2f2
--- /dev/null
+++ b/jails/gubbhub/tasks.yaml
@@ -0,0 +1,163 @@
+- name: load task vars
+ ansible.builtin.include_vars: ./variables.yaml
+
+- name: install git and cgit
+ community.general.pkgng:
+ name:
+ - git
+ - cgit
+ jail: "{{ jail.name }}"
+
+- name: install nginx and fcgiwrap
+ community.general.pkgng:
+ name:
+ - nginx
+ - fcgiwrap
+ jail: "{{ jail.name }}"
+
+- name: enable fcgiwrap
+ community.general.sysrc:
+ name: fcgiwrap_enable
+ value: "YES"
+ jail: "{{ jail.name }}"
+
+- name: set nginx permissions on fcgiwrap
+ community.general.sysrc:
+ name: fcgiwrap_socket_group
+ value: "www"
+ jail: "{{ jail.name }}"
+
+- name: start fcgiwrap
+ jexec:
+ cmd: service fcgiwrap restart
+ jail: "{{ jail.name }}"
+
+- name: enable nginx
+ community.general.sysrc:
+ name: nginx_enable
+ value: "YES"
+ jail: "{{ jail.name }}"
+
+- name: create groups
+ jexec:
+ cmd: |
+ getent group gitdev || pw groupadd gitdev
+ getent group gitadm || pw groupadd gitadm
+ jail: "{{ jail.name }}"
+
+- name: mount and set up git in zfs
+ jexec:
+ cmd: |
+ [ $(zfs get -H -o value mountpoint {{ gitdataset }}) == "/git" ] && \
+ [ $(zfs get -H -o value mounted {{ gitdataset }}) == "yes" ] || \
+ zfs set mountpoint=/git {{ gitdataset }}
+ jail: "{{ jail.name }}"
+
+- name: enable zfs compression on git data
+ jexec:
+ cmd: |
+ zfs set compression=on {{ gitdataset }}
+ jail: "{{ jail.name }}"
+
+- name: create folders and set permissions
+ jexec:
+ cmd: |
+ mkdir -p /git/repos
+ chown root:gitdev /git/repos
+ chmod g+rws /git/repos
+ mkdir -p /git/sshkeys
+ jail: "{{ jail.name }}"
+
+- name: enable sshd
+ community.general.sysrc:
+ name: sshd_enable
+ value: "YES"
+ jail: "{{ jail.name }}"
+
+- name: create git shell command dir
+ ansible.builtin.file:
+ path: "{{ jailroot }}/git/repos/git-shell-commands"
+ state: directory
+
+- name: setup gubbshell
+ ansible.builtin.copy:
+ src: ./gubbshell/
+ dest: "{{ jailroot }}/git/repos/git-shell-commands/"
+ mode: "g=rx"
+
+- name: install nano
+ community.general.pkgng:
+ name:
+ - nano
+ jail: "{{ jail.name }}"
+
+- name: set correct permissions for gubbshell scripts
+ jexec:
+ cmd: chgrp gitadm /git/repos/git-shell-commands/create-repo /git/repos/git-shell-commands/delete-repo /git/repos/git-shell-commands/edit-repo
+ jail: "{{ jail.name }}"
+
+- name: disable motd
+ jexec:
+ cmd: touch /git/repos/.hushlogin
+ jail: "{{ jail.name }}"
+
+- name: create users
+ jexec:
+ cmd: |
+ pw useradd {{ item.key }} -g gitdev -s /usr/local/libexec/git-core/git-shell -d /git/repos
+ {% if item.value.admin %}
+ pw usermod {{ item.key }} -G gitadm
+ {% endif %}
+ jail: "{{ jail.name }}"
+ loop: "{{ users | dict2items }}"
+ loop_control:
+ label: "{{ item.key }}"
+
+- name: upload pubkeys
+ ansible.builtin.copy:
+ content: "{{ item.value.ssh_keys | join('\n') }}"
+ dest: "{{ jailroot }}/git/sshkeys/{{ item.key }}"
+ owner: "{{ item.key }}"
+ mode: 0400
+ when: item.value.ssh_keys is defined
+ loop: "{{ users | dict2items }}"
+ loop_control:
+ label: "{{ item.key }}"
+
+- name: configure sshd
+ ansible.builtin.lineinfile:
+ path: "{{ jailroot }}/etc/ssh/sshd_config"
+ regex: "^(#)?{{item.key}}"
+ line: "{{item.key}} {{item.value}}"
+ state: present
+ loop:
+ - { key: "PermitRootLogin", value: "no" }
+ - { key: "PasswordAuthentication", value: "no" }
+ - { key: "ChallengeResponseAuthentication", value: "no" }
+ - { key: "UsePAM", value: "no" }
+ - { key: "AuthorizedKeysFile", value: "/git/sshkeys/%u" }
+ register: sshd_conf
+
+- name: configure nginx for cgit
+ ansible.builtin.template:
+ src: cgit.nginx.conf.j2
+ dest: "{{ jailroot }}/usr/local/etc/nginx/nginx.conf"
+ register: nginx_conf
+
+- name: cgit conf
+ ansible.builtin.template:
+ src: cgitrc.j2
+ dest: "{{ jailroot }}/usr/local/etc/cgitrc"
+ register: cgit_conf
+
+- name: restart nginx
+ when: nginx_conf.changed
+ jexec:
+ cmd: service nginx restart
+ jail: "{{ jail.name }}"
+
+- name: restart sshd
+ when: sshd_conf.changed
+ jexec:
+ cmd: service sshd restart
+ jail: "{{ jail.name }}"
diff --git a/jails/gubbhub/variables.yaml b/jails/gubbhub/variables.yaml
new file mode 100644
index 0000000..20c4bbb
--- /dev/null
+++ b/jails/gubbhub/variables.yaml
@@ -0,0 +1,9 @@
+jailroot: "{{ jailbase }}/{{ jail.name }}"
+gitdataset: "{{ jailset }}/tank/gitdata"
+users:
+ abbe:
+ admin: true
+ ssh_keys:
+ - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDBNLyPSAklU0Uz4BarqyUTvkQN9UiIC+dEO7f+lZ1F0K7/S4aEl3lUy8bE5jOiZaOGJNlsJe0zKEAZ3rhjFE70XoWEY5tojaCn9EycKAclbIH+Tvc6ssirfjhjWYIdLsARMndKzcKszNfI7+bYq76miJyzkb8umzxNvecqF9AMK0ZvZaaLwmgIOUy7cSn4ZvU/8U6aBuPsSsOx3i7F1nm0hcy3TCI4OOUN454Ajqub4529Q68iqIZZ9GhppbogCuHuJEMI1OJMPiSUklgtOq4fPnzM6J4uIBsnQ1deSgw+T0QsRWQcME/zKEw6MhOcwVO0z+n08irZehgNtxDxjBDy5L3THLbztG3V+zND+DpfyNsHIYlKYDzE4XW74v+fjU4V3lBuR9rFm5KTRAU/Vok7/I6H1WrvXyvbOq/YvqEzW8JCsq53LIk04obR57iG371wdLoSi8RCEaLzPuDlzRK10KzjlVEfpRHgWyaHFbxtuaqkU6BGQqGeOEiLS639Bm2XYVbybezQBNZ2ht1gDTDMVxVsgsmT18eJiS5SEy49chTC5Jpv9qIlb6cSDUUiTABs8S4iN91dLvSWqu3fWzHJqTc14dujTJ9M+BdF2bwlNPSnsH98iXBLJVGgg/jICSi1LBMSymTzCkf/AMhS7YS2ZI1Ma2AxoysXsjpfDYNyRw== cardno:10 138 182
+ sakarias:
+ admin: true
diff --git a/library/jexec.sh b/library/jexec.sh
new file mode 100755
index 0000000..2c4e19d
--- /dev/null
+++ b/library/jexec.sh
@@ -0,0 +1,17 @@
+#! /usr/bin/env sh
+set -e
+jsonargs='<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>'
+
+jailname=$(echo "$jsonargs" | jq -r '.jail' -)
+cmd=$(echo "$jsonargs" | jq -r '.cmd' -)
+
+jexec -l -u root "$jailname" /bin/sh -c "$cmd"
+
+rc=$?
+if [ $rc != 0 ]; then
+ failed="false"
+else
+ failed="true"
+fi
+
+echo '{ "msg": "'"$jailname"'", "rc": '"$rc"', "failed": false }'
diff --git a/playbook.yaml b/playbook.yaml
new file mode 100644
index 0000000..decbf6b
--- /dev/null
+++ b/playbook.yaml
@@ -0,0 +1,199 @@
+- name: Setup the main server
+ hosts: datagubbe.dev
+ become: true
+
+ vars:
+ jailbase: "/usr/local/jails"
+ jailset: "poolen/jails"
+ jails:
+ gubbhub:
+ name: gubbhub
+ ip: "2a01:4f9:2b:f05::2/64"
+ tags: gubbhub
+ additional_data_sets:
+ - gitdata
+ certificates:
+ - git.datagubbe.dev
+ haproxy_conf: |
+ frontend git-ssh from tcp
+ mode tcp
+ bind :2224
+ default_backend gubbhub
+
+ backend gubbhub from tcp
+ server srv 2a01:4f9:2b:f05::2:22
+
+ backend git.datagubbe.dev from http
+ server srv 2a01:4f9:2b:f05::2:80
+
+ hallosbacken:
+ name: hallosbacken
+ ip: "2a01:4f9:2b:f05::3/64"
+ tags: hallosbacken
+ additional_data_sets:
+ - wp_data
+
+ datagubbe:
+ name: datagubbe
+ ip: "2a01:4f9:2b:f05::4/64"
+ tags: datagubbe
+ certificates:
+ - datagubbe.dev
+ haproxy_conf: |
+ backend datagubbe from http
+ server srv 2a01:4f9:2b:f05::4:80
+
+ roles:
+ - pf
+ - jailhost
+
+ - role: jail
+ jail: "{{ jails.gubbhub }}"
+ tags: "{{ jails.gubbhub.tags }}"
+
+ - role: jail
+ jail: "{{ jails.hallosbacken }}"
+
+ - role: jail
+ jail: "{{ jails.datagubbe }}"
+
+ tasks:
+ - name: configure periodic to be less chatty
+ ansible.builtin.copy:
+ content: |
+ # i do not need to know this
+ daily_show_success=”NO”
+ weekly_show_success="NO"
+ monthly_show_success="NO"
+
+ # enable our certbot renew script
+ daily_renewcerts_enable="YES"
+ dest: /etc/periodic.conf
+
+ - name: install jq
+ community.general.pkgng:
+ name: "jq"
+ state: latest
+
+ - name: install haproxy
+ community.general.pkgng:
+ name: "haproxy"
+ state: latest
+
+ - name: create haproxy user
+ ansible.builtin.user:
+ name: haproxy
+ system: true
+
+ - name: config for haproxy
+ ansible.builtin.template:
+ src: haproxy.conf.j2
+ dest: /usr/local/etc/haproxy.conf
+ notify: reload haproxy
+
+ - name: enable haproxy service
+ community.general.sysrc:
+ name: haproxy_enable
+ value: "YES"
+ notify: start haproxy
+
+ - name: install certbot and nginx
+ community.general.pkgng:
+ name:
+ - security/py-certbot
+ - nginx
+ state: latest
+
+ - name: nginx config for certbot
+ ansible.builtin.template:
+ src: templates/nginx-certbot.conf.j2
+ dest: /usr/local/etc/nginx/nginx.conf
+ vars:
+ root: /var/www/html
+
+ - name: enable nginx
+ community.general.sysrc:
+ name: nginx_enable
+ value: "YES"
+
+ - name: start nginx
+ ansible.builtin.service:
+ name: nginx
+ state: reloaded
+
+ - name: create cert hook for haproxy
+ ansible.builtin.copy:
+ content: |
+ #! /usr/bin/env sh
+ mkdir -p /usr/local/etc/haproxy/certs
+ dir="$RENEWED_LINEAGE"
+ domain=`basename "$RENEWED_LINEAGE"`
+ cat $dir/fullchain.pem $dir/privkey.pem > /usr/local/etc/haproxy/certs/$domain.pem
+ chown -R haproxy:haproxy /usr/local/etc/haproxy
+ dest: /usr/local/etc/letsencrypt/renewal-hooks/deploy/create-haproxy-cert
+ mode: 755
+
+ - name: set needed certs
+ ansible.builtin.set_fact:
+ certificates: "{{ jails | dict2items | selectattr('value.certificates', 'defined') | map(attribute='value.certificates') | list | flatten }}"
+
+ - name: "obtain cert for {{ item }}"
+ shell:
+ cmd: |
+ certbot \
+ --non-interactive \
+ --email albert@acervin.com \
+ --agree-tos \
+ certonly \
+ --webroot \
+ --webroot-path /var/www/html \
+ -d '{{ item }}'
+
+ RENEWED_LINEAGE=/usr/local/etc/letsencrypt/live/{{item}} /usr/local/etc/letsencrypt/renewal-hooks/deploy/create-haproxy-cert
+ creates: /usr/local/etc/letsencrypt/live/{{ item }}
+ loop: "{{ certificates }}"
+
+ - name: create daily job for updating certs
+ ansible.builtin.copy:
+ src: ./10-renewcerts
+ dest: /usr/local/etc/periodic/daily/10-renewcerts
+ mode: 'a=rx,u=rwx'
+
+ - name: config for haproxy (with certs)
+ ansible.builtin.template:
+ src: haproxy.conf.j2
+ dest: /usr/local/etc/haproxy.conf
+ notify: reload haproxy
+
+ - name: Setup datagubbe
+ import_tasks: jails/datagubbe/tasks.yaml
+ vars:
+ jail: "{{ jails.datagubbe }}"
+ certs:
+ - /usr/local/etc/certs/datagubbe.dev.pem
+ tags: datagubbe-setup
+
+ - name: Setup the gubbhub
+ import_tasks: jails/gubbhub/tasks.yaml
+ vars:
+ jail: "{{ jails.gubbhub }}"
+ tags: gubbhub-setup
+
+ - name: Make sure packages are up to date
+ community.general.pkgng:
+ name: "*"
+ state: latest
+
+ handlers:
+ - name: reload haproxy
+ ansible.builtin.shell: /usr/local/etc/rc.d/haproxy configtest && /usr/local/etc/rc.d/haproxy reload
+
+ - name: start haproxy
+ ansible.builtin.service:
+ name: haproxy
+ state: started
+
+ - name: restart sshd in jail
+ jexec:
+ jail:
+
diff --git a/roles/jail/meta/main.yml b/roles/jail/meta/main.yml
new file mode 100644
index 0000000..574a987
--- /dev/null
+++ b/roles/jail/meta/main.yml
@@ -0,0 +1,2 @@
+dependencies:
+# - role: jailhost
diff --git a/roles/jail/tasks/main.yml b/roles/jail/tasks/main.yml
new file mode 100644
index 0000000..670a886
--- /dev/null
+++ b/roles/jail/tasks/main.yml
@@ -0,0 +1,34 @@
+- name: create dataset for jail
+ community.general.zfs:
+ name: "{{ jailset }}/{{ jail.name }}"
+ state: present
+ origin: "{{ jailset }}/base@{{ base_jail_patch_level }}"
+
+- name: make sure jail conf dir exists
+ file:
+ path: "{{ jailbase }}/conf"
+ state: directory
+ mode: '0755'
+
+- name: create jail conf {{ jail.name }}
+ ansible.builtin.template:
+ src: jail.conf.j2
+ dest: "{{ jailbase }}/conf/{{ jail.name }}.conf"
+
+- name: create additional datasets
+ community.general.zfs:
+ name: "{{ jailset }}/tank/{{ item }}"
+ state: present
+ extra_zfs_properties:
+ jailed: on
+ loop: "{{ jail.additional_data_sets | default([]) }}"
+
+- name: start jail
+ shell:
+ cmd: jls -j {{ jail.name }} || jail -f {{ jailbase }}/conf/{{ jail.name }}.conf -c {{ jail.name }}
+
+- name: Make sure packages are up to date in {{ jail.name }}
+ community.general.pkgng:
+ name: "*"
+ state: latest
+ jail: "{{ jail.name }}"
diff --git a/roles/jail/templates/jail.conf.j2 b/roles/jail/templates/jail.conf.j2
new file mode 100644
index 0000000..0b55078
--- /dev/null
+++ b/roles/jail/templates/jail.conf.j2
@@ -0,0 +1,18 @@
+{{ jail.name }} {
+ exec.start = '/bin/sh /etc/rc';
+ exec.stop = '/bin/sh /etc/rc.shutdown jail';
+ mount.devfs;
+ host.hostname={{ jail.name }}.in.prison;
+ allow.raw_sockets;
+ allow.mount;
+ allow.mount.devfs;
+ allow.mount.zfs;
+ devfs_ruleset = 4;
+ enforce_statfs = 1;
+ ip6.addr='em0|{{ jail.ip }}';
+ path=/usr/local/jails/{{ jail.name }};
+
+ {% for dataset in jail.additional_data_sets | default([]) -%}
+ exec.poststart+="zfs jail ${name} poolen/jails/tank/{{ dataset }}";
+ {% endfor -%}
+}
diff --git a/roles/jailhost/handlers/main.yml b/roles/jailhost/handlers/main.yml
new file mode 100644
index 0000000..4177d4c
--- /dev/null
+++ b/roles/jailhost/handlers/main.yml
@@ -0,0 +1,9 @@
+- name: restart netif
+ service:
+ name: netif
+ state: restarted
+
+- name: restart routing
+ service:
+ name: routing
+ state: restarted
diff --git a/roles/jailhost/tasks/main.yml b/roles/jailhost/tasks/main.yml
new file mode 100644
index 0000000..a26351b
--- /dev/null
+++ b/roles/jailhost/tasks/main.yml
@@ -0,0 +1,64 @@
+- name: create jails dataset
+ community.general.zfs:
+ name: poolen/jails
+ state: present
+ extra_zfs_properties:
+ mountpoint: /usr/local/jails
+
+- name: create jails tank dataset
+ community.general.zfs:
+ name: poolen/jails/tank
+ state: present
+
+- name: create base jail dataset
+ community.general.zfs:
+ name: poolen/jails/base
+ state: present
+
+- name: install base jail
+ shell: |
+ set -e
+ bsdinstall checksum || echo 'checksums failed'
+ bsdinstall distextract || echo 'distextract failed'
+ bsdinstall config || error 'failed to save config'
+
+ bsdinstall entropy
+ environment:
+ BSDINSTALL_CHROOT: "/usr/local/jails/base"
+ DISTRIBUTIONS: "base.txz"
+ nonInteractive: "YES"
+ args:
+ creates: "/usr/local/jails/base/bin"
+
+- name: configure base jail
+ shell: |
+ cp /etc/resolv.conf /usr/local/jails/base/etc/
+ cp /etc/localtime /usr/local/jails/base/etc/
+ cp /var/db/zoneinfo /usr/local/jails/base/var/db/
+ args:
+ creates: "/usr/local/jails/base/etc/resolv.conf"
+
+- name: apply updates for base jail
+ ansible.builtin.shell: |
+ freebsd-update -b /usr/local/jails/base fetch
+ freebsd-update -b /usr/local/jails/base install
+ register: result_update
+ failed_when: result_update.rc != 0 and result_update.rc != 2
+ changed_when: result_update.rc != 2
+
+- name: determine patch level of base jail
+ shell: /usr/local/jails/base/bin/freebsd-version -u
+ register: patch_level
+ environment:
+ ROOT: /usr/local/jails/base
+
+- name: snapshot the base jail
+ community.general.zfs:
+ name: "poolen/jails/base@{{ patch_level.stdout }}"
+ state: present
+
+- name: set patch level as fact
+ ansible.builtin.set_fact:
+ base_jail_patch_level: "{{ patch_level.stdout }}"
+ cacheable: yes
+
diff --git a/roles/pf/handlers/main.yml b/roles/pf/handlers/main.yml
new file mode 100644
index 0000000..4baf234
--- /dev/null
+++ b/roles/pf/handlers/main.yml
@@ -0,0 +1,15 @@
+---
+- name: start pflog
+ service:
+ name: pflog
+ state: started
+
+- name: start pf
+ service:
+ name: pf
+ state: started
+ async: 45
+ poll: 5
+
+- name: reload pf
+ shell: pfctl -nf /etc/pf.conf && pfctl -f /etc/pf.conf
diff --git a/roles/pf/tasks/main.yml b/roles/pf/tasks/main.yml
new file mode 100644
index 0000000..b7c405a
--- /dev/null
+++ b/roles/pf/tasks/main.yml
@@ -0,0 +1,19 @@
+- name: enable pf
+ community.general.sysrc:
+ name: pf_enable
+ value: "YES"
+ notify: start pf
+
+- name: enable pflog
+ community.general.sysrc:
+ name: pflog_enable
+ value: "YES"
+ notify: start pflog
+
+- name: template pf.conf
+ template:
+ src: pf.conf.j2
+ dest: /etc/pf.conf
+ notify: reload pf
+
+- meta: flush_handlers
diff --git a/roles/pf/templates/pf.conf.j2 b/roles/pf/templates/pf.conf.j2
new file mode 100644
index 0000000..8819ee0
--- /dev/null
+++ b/roles/pf/templates/pf.conf.j2
@@ -0,0 +1,56 @@
+# our interface
+ext_if = "em0"
+
+# IPv6 link local prefix.
+PFX_LNKLOC = "fe80::/10"
+
+# IPv6 Solicited Node Multicast Prefix.
+MC_SOLNOD = "ff02::1:ff00:0/104"
+
+# IPv6 All Nodes Link Local Multicast Address.
+MC_NODLNK = "ff02::1"
+
+# skip loopback
+set skip on lo0
+set loginterface $ext_if
+set block-policy drop
+
+scrub in on $ext_if
+
+## RULES ##
+
+# block and log all traffic not matching below rules
+block in
+
+# allow ssh traffic to the host on the custom port
+pass in quick proto tcp to port 2223
+
+# allow http/https
+pass in quick proto {tcp, udp} to port { http, https }
+
+# allow ssh for gubbhub
+pass in quick proto tcp to port 2224
+
+# ping
+pass in quick inet6 proto icmp6 icmp6-type echoreq
+pass in quick inet proto icmp icmp-type echoreq
+
+# ipv6 stuff
+
+# Allow NS from unspecified to solicited node multicast address (DAD).
+pass quick inet6 proto icmp6 from :: to $MC_SOLNOD icmp6-type neighbrsol no state
+
+# Allow IPv6 Router Discovery.
+pass in quick inet6 proto icmp6 from $PFX_LNKLOC to $MC_NODLNK icmp6-type routeradv no state
+
+# Allow IPv6 Neighbor Discovery (ND/NUD/DAD).
+pass in quick inet6 proto icmp6 to { ($ext_if), $MC_SOLNOD } icmp6-type { neighbrsol, neighbradv } no state
+
+# Allow any outgoing traffic
+pass out
+
+# TODO: We seem to not only get neighbor advertisements from the local subnet? How does
+# this work?
+#pass in quick inet6 proto icmp6 from { $PFX_LNKLOC, ($ext_if:network) } to { ($ext_if), $MC_SOLNOD } icmp6-type neighbrsol no state
+#pass in quick inet6 proto icmp6 from { $PFX_LNKLOC, ($ext_if:network) } to { ($ext_if), $MC_NODLNK } icmp6-type neighbradv no state
+
diff --git a/templates/nginx-certbot.conf.j2 b/templates/nginx-certbot.conf.j2
new file mode 100644
index 0000000..51eca20
--- /dev/null
+++ b/templates/nginx-certbot.conf.j2
@@ -0,0 +1,49 @@
+worker_processes 1;
+
+# This default error log path is compiled-in to make sure configuration parsing
+# errors are logged somewhere, especially during unattended boot when stderr
+# isn't normally logged anywhere. This path will be touched on every nginx
+# start regardless of error log location configured here. See
+# https://trac.nginx.org/nginx/ticket/147 for more info.
+#
+#error_log /var/log/nginx/error.log;
+#
+
+#pid logs/nginx.pid;
+
+
+events {
+ worker_connections 1024;
+}
+
+
+http {
+ include 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 logs/access.log main;
+
+ sendfile on;
+ #tcp_nopush on;
+
+ #keepalive_timeout 0;
+ keepalive_timeout 65;
+
+ #gzip on;
+
+ server {
+ listen [::]:7878;
+ server_name localhost;
+
+ #charset koi8-r;
+
+ #access_log logs/host.access.log main;
+ location ^~ /.well-known/acme-challenge/ {
+ root {{ root }};
+ }
+ }
+}