diff options
30 files changed, 1166 insertions, 0 deletions
@@ -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 }}; + } + } +} |
