summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/ci/git-post-receive.sh178
-rwxr-xr-xsrc/ci/git-pre-receive.sh14
-rw-r--r--src/config/conf.env.in16
-rw-r--r--src/config/gitconfig11
-rw-r--r--src/config/init.scm6
-rw-r--r--src/config/known_hosts.txt5
-rw-r--r--src/config/profile.sh5
-rw-r--r--src/config/rc.sh80
-rw-r--r--src/config/ssh.conf6
-rw-r--r--src/guix/channels.scm12
-rw-r--r--src/guix/system.scm474
-rw-r--r--src/keys/GPG/andre.asc86
-rw-r--r--src/keys/SSH/andre.pub.txt1
-rw-r--r--src/keys/SSH/laisse.pub.txt1
-rw-r--r--src/keys/SSH/root@papo.im.id_rsa.pub.stripped1
-rw-r--r--src/keys/SSH/root@papo.im.id_rsa.pub.txt1
-rwxr-xr-xsrc/keys/gpg-import.sh71
-rwxr-xr-xsrc/keys/gpg-recipients.sh71
-rwxr-xr-xsrc/scripts/backup.sh149
-rwxr-xr-xsrc/scripts/check.sh92
-rwxr-xr-xsrc/scripts/cicd.sh168
-rwxr-xr-xsrc/scripts/cronjob.sh169
-rwxr-xr-xsrc/scripts/deploy.sh72
-rwxr-xr-xsrc/scripts/gc.sh146
-rwxr-xr-xsrc/scripts/reconfigure.sh146
-rwxr-xr-xsrc/scripts/report.sh260
-rw-r--r--src/secrets/VPS-root.txt.gpg18
-rw-r--r--src/secrets/borg-passphrase.txt.gpg18
-rw-r--r--src/secrets/root@papo.im.id_rsa.txt.gpg18
-rw-r--r--src/secrets/rsync.net.txt.gpg18
30 files changed, 2313 insertions, 0 deletions
diff --git a/src/ci/git-post-receive.sh b/src/ci/git-post-receive.sh
new file mode 100755
index 0000000..46bf5a5
--- /dev/null
+++ b/src/ci/git-post-receive.sh
@@ -0,0 +1,178 @@
+#!/bin/sh
+# shellcheck source=/dev/null disable=2317
+. /etc/rc
+set -eu
+
+
+# shellcheck disable=2034
+read -r _oldrev SHA REFNAME
+
+if [ "$SHA" = '0000000000000000000000000000000000000000' ]; then
+ exit
+fi
+
+
+SKIP_DEPLOY=false
+for n in $(seq 0 $((GIT_PUSH_OPTION_COUNT - 1))); do
+ opt="$(eval "printf '%s' \"\$GIT_PUSH_OPTION_$n\"")"
+ case "$opt" in
+ ci.skip)
+ cat <<-EOF
+
+ "$opt" option detected, not running CI.
+
+ EOF
+ exit
+ ;;
+ deploy.skip)
+ SKIP_DEPLOY=true
+ ;;
+ *)
+ ;;
+ esac
+done
+
+
+epoch() {
+ awk 'BEGIN { srand(); print(srand()); }'
+}
+
+now() {
+ date '+%Y-%m-%dT%H:%M:%S%:z'
+}
+
+NAME="$(basename "$PWD" .git)"
+LOGS_DIR=/var/log/ci/"$NAME"/
+TIMESTAMP="$(now)"
+FILENAME="$TIMESTAMP-$SHA.log"
+LOGFILE="$LOGS_DIR/$FILENAME"
+mkdir -p "$LOGS_DIR"
+
+
+END_MARKER='\033[0m'
+LIGHT_BLUE_B='\033[1;36m'
+YELLOW='\033[1;33m'
+
+blue() {
+ printf "${LIGHT_BLUE_B}%s${END_MARKER}" "$1"
+}
+
+yellow() {
+ printf "${YELLOW}%s${END_MARKER}" "$1"
+}
+
+info() {
+ sed "s|^\(.\)|$(blue 'CI'): \1|"
+}
+
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+tmpname() {
+ printf '%s/uuid-tmpname with spaces.%s' "${TMPDIR:-/tmp}" "$(uuid)"
+}
+
+mkdtemp() {
+ name="$(tmpname)"
+ mkdir -- "$name"
+ printf '%s' "$name"
+}
+
+
+{
+ cat <<-EOF | info
+ Starting CI job at: $(now)
+ EOF
+ START="$(epoch)"
+
+ duration() {
+ if [ "$RUN_DURATION" -gt 60 ]; then
+ cat <<-EOF
+ $(yellow 'WARNING'): run took more than 1 minute! ($RUN_DURATION seconds)
+ EOF
+ else
+ cat <<-EOF
+ Run took $RUN_DURATION seconds.
+ EOF
+ fi
+ }
+
+ finish() {
+ STATUS="$?"
+ END="$(epoch)"
+ RUN_DURATION=$((END - START))
+ cat <<-EOF | info
+ Finishing CI job at: $(now)
+ Exit status was $STATUS
+ Re-run with:
+ \$ $CMD
+ $(duration)
+ EOF
+
+ NOTE="$(
+ cat <<-EOF
+ See CI logs with:
+ git notes --ref=refs/notes/ci-logs show $SHA
+ git notes --ref=refs/notes/ci-data show $SHA
+
+ Exit status: $STATUS
+ Duration: $RUN_DURATION
+ EOF
+ )"
+ git notes --ref=refs/notes/ci-data add -f -m "$(
+ cat <<-EOF
+ status $STATUS
+ sha $SHA
+ filename $FILENAME
+ duration $RUN_DURATION
+ timestamp $TIMESTAMP
+ to-prod $TO_PROD
+ refname $REFNAME
+ EOF
+ )" "$SHA"
+ git notes --ref=refs/notes/ci-logs add -f -F "$LOGFILE" "$SHA"
+ git notes add -f -m "$NOTE" "$SHA"
+
+ {
+ printf 'Git CI HTML report for %s (%s) started.\n' "$NAME" "$SHA" >&2
+ DIR="$(mkdtemp)"
+ report -o "$DIR"
+ sudo -u deployer rsync \
+ --chmod=D775,F664 \
+ --chown=deployer:deployer \
+ --delete \
+ -a \
+ "$DIR"/ "$HTML_OUTDIR_CI"/
+ rm -rf "$DIR"
+ printf 'Git CI HTML report for %s (%s) finished.\n' "$NAME" "$SHA" >&2
+ } 2>&1 | logger -i -p local0.warn -t git-ci 1>/dev/null 2>&1 &
+ }
+ trap finish EXIT
+
+ unset GIT_DIR
+
+ if [ "$REFNAME" = 'refs/heads/main' ] && [ "$SKIP_DEPLOY" = false ]; then
+ cat <<-EOF | info
+ In branch "main", running deploy for $SHA.
+ EOF
+ TO_PROD=true
+ CMD="sudo reconfigure $SHA"
+ else
+ if [ "$SKIP_DEPLOY" = true ]; then
+ cat <<-EOF | info
+ "deploy.skip" option detected, skipping deploy for $SHA.
+ EOF
+ else
+ cat <<-EOF | info
+ Not on branch "main", skipping deploy for $SHA.
+ EOF
+ fi
+ TO_PROD=false
+ CMD="sudo reconfigure -n $SHA"
+ fi
+ $CMD
+} 2>&1 | ts -s '%.s' | tee "$LOGFILE"
diff --git a/src/ci/git-pre-receive.sh b/src/ci/git-pre-receive.sh
new file mode 100755
index 0000000..8cd83ee
--- /dev/null
+++ b/src/ci/git-pre-receive.sh
@@ -0,0 +1,14 @@
+#!/bin/sh
+set -eu
+
+read -r _oldrev SHA _refname
+unset GIT_DIR
+
+if [ "$SHA" = '0000000000000000000000000000000000000000' ]; then
+ exit
+fi
+
+printf 'Upgrading post-receive hook...' >&2
+git show "$SHA":src/infrastructure/ci/git-post-receive.sh > hooks/post-receive
+chmod +x hooks/post-receive
+printf 'done.\n' >&2
diff --git a/src/config/conf.env.in b/src/config/conf.env.in
new file mode 100644
index 0000000..e54daef
--- /dev/null
+++ b/src/config/conf.env.in
@@ -0,0 +1,16 @@
+#!/bin/sh
+
+NAME='@NAME@'
+TLD='@TLD@'
+URL='@URL@'
+OFFSITE_SSH='@OFFSITE_SSH@'
+OUT_SUFFIX=''
+PRIV_SUFFIX=''
+CI_SUFFIX="ci"
+
+HTML_OUTDIR_TOP="/srv/www/$OUT_SUFFIX"
+HTML_OUTDIR_PRIV="$HTML_OUTDIR_TOP$PRIV_SUFFIX"
+HTML_OUTDIR_CI="$HTML_OUTDIR_TOP/$CI_SUFFIX"
+HOMEPAGE="https://$TLD/$OUT_SUFFIX/"
+CGIT_URL="https://$TLD/git/$NAME/commit/?id="
+REPO_NAME="$NAME.git"
diff --git a/src/config/gitconfig b/src/config/gitconfig
new file mode 100644
index 0000000..f1f1eb3
--- /dev/null
+++ b/src/config/gitconfig
@@ -0,0 +1,11 @@
+[init]
+ defaultBranch = main
+[user]
+ email = ci@$TLD
+ name = "Git CI"
+[advice]
+ detachedHead = false
+[receive]
+ advertisePushOptions = true
+[uploadpack]
+ allowAnySHA1InWant = true
diff --git a/src/config/init.scm b/src/config/init.scm
new file mode 100644
index 0000000..9e962e8
--- /dev/null
+++ b/src/config/init.scm
@@ -0,0 +1,6 @@
+(use-modules
+ (ice-9 colorized)
+ (ice-9 readline))
+
+(activate-colorized)
+(activate-readline)
diff --git a/src/config/known_hosts.txt b/src/config/known_hosts.txt
new file mode 100644
index 0000000..74ba219
--- /dev/null
+++ b/src/config/known_hosts.txt
@@ -0,0 +1,5 @@
+# rsync.net public keys
+# Verified in 2023-03-08 at:
+# https://www.rsync.net/resources/fingerprints.txt
+
+hk-s020.rsync.net ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAILcPl9x9JfRFwsn09NnDw/xBZbAN80ZQck+h6AqlVqPH
diff --git a/src/config/profile.sh b/src/config/profile.sh
new file mode 100644
index 0000000..1dca8b2
--- /dev/null
+++ b/src/config/profile.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+# shellcheck source=/dev/null
+. /etc/rc
+ln -fs .profile .bashrc
diff --git a/src/config/rc.sh b/src/config/rc.sh
new file mode 100644
index 0000000..b44d3d1
--- /dev/null
+++ b/src/config/rc.sh
@@ -0,0 +1,80 @@
+#!/bin/sh
+
+# shellcheck source=/dev/null
+. /etc/profile
+. /etc/conf.env
+
+export XDG_PREFIX=~/.usr
+export XDG_CACHE_HOME="$XDG_PREFIX"/var/cache
+export XDG_CONFIG_HOME="$XDG_PREFIX"/etc
+export XDG_DATA_HOME="$XDG_PREFIX"/share
+export XDG_STATE_HOME="$XDG_PREFIX"/state
+export XDG_LOG_HOME="$XDG_PREFIX"/var/log
+
+
+HOME_PARENT="$(dirname "$HOME")"
+if [ "$HOME_PARENT" = '/home' ] || [ "$HOME_PARENT" = '/' ]; then
+ mkdir -p \
+ "$XDG_CONFIG_HOME" \
+ "$XDG_CACHE_HOME" \
+ "$XDG_DATA_HOME" \
+ "$XDG_LOG_HOME" \
+ "$XDG_STATE_HOME"/ssh/conn
+fi
+
+
+GUIX_PROFILE="$XDG_CONFIG_HOME"/guix/current
+if [ -r "$GUIX_PROFILE"/etc/profile ]; then
+ # shellcheck source=/dev/null
+ . "$GUIX_PROFILE"/etc/profile
+fi
+
+export ENV=~/.profile
+export HISTSIZE=-1
+export HISTCONTROL=ignorespace:ignoredups
+export EDITOR=vi
+export VISUAL="$EDITOR"
+export PAGER='less -R'
+
+export EXINIT='
+ " set number
+ " set autoindent
+ set ruler
+ set showmode
+ set showmatch
+'
+
+export HISTFILE="$XDG_STATE_HOME"/bash-history
+export LESSHISTFILE="$XDG_STATE_HOME"/lesshst
+export RLWRAP_HOME="$XDG_CACHE_HOME"/rlwrap
+export GUILE_HISTORY="$XDG_STATE_HOME"/guile-history
+
+HOSTNAME="$(hostname)"
+export BORG_REPO="$OFFSITE_SSH:borg/$HOSTNAME"
+export BORG_REMOTE_PATH='borg1'
+export BORG_PASSCOMMAND='cat /opt/secrets/borg-passphrase.txt'
+
+export GIT_CONFIG_GLOBAL=/etc/gitconfig
+
+unalias -a
+alias l='ls -lahF --color'
+alias grep='grep --color=auto'
+alias diff='diff --color=auto'
+alias watch='watch --color '
+alias man='MANWIDTH=$((COLUMNS > 80 ? 80 : COLUMNS)) man'
+alias less='less -R'
+alias tree='tree -aC'
+alias mv='mv -i'
+alias e='vi'
+
+alias sqlite='rlwrap sqlite3'
+alias guile='guile -l /etc/init.scm'
+
+error_marker() {
+ STATUS=$?
+ if [ "$STATUS" != 0 ]; then
+ printf ' (!! %s !!) ' "$STATUS"
+ fi
+}
+export PS1='`error_marker`\T \w/
+\u@\H\$ '
diff --git a/src/config/ssh.conf b/src/config/ssh.conf
new file mode 100644
index 0000000..ca41df0
--- /dev/null
+++ b/src/config/ssh.conf
@@ -0,0 +1,6 @@
+Host *
+ ServerAliveInterval 30
+ ServerAliveCountMax 20
+ ControlMaster auto
+ ControlPath ${XDG_STATE_HOME}/ssh/conn/%r@%h:%p
+ ControlPersist 1h
diff --git a/src/guix/channels.scm b/src/guix/channels.scm
new file mode 100644
index 0000000..16a9c7d
--- /dev/null
+++ b/src/guix/channels.scm
@@ -0,0 +1,12 @@
+(append
+ (list
+ (channel
+ (name 'org-euandre)
+ (url "git://euandre.org/package-repository")
+ (branch "main")
+ (introduction
+ (make-channel-introduction
+ "d749e053e6db365069cb9b2ef47a78b06f9e7361"
+ (openpgp-fingerprint
+ "5BDA E9B8 B2F6 C6BC BB0D 6CE5 81F9 0EC3 CD35 6060")))))
+ %default-channels)
diff --git a/src/guix/system.scm b/src/guix/system.scm
new file mode 100644
index 0000000..5e16737
--- /dev/null
+++ b/src/guix/system.scm
@@ -0,0 +1,474 @@
+(use-modules
+ ((ice-9 textual-ports) #:prefix textual-ports:)
+ ((srfi srfi-1) #:prefix s1:)
+ ((xyz euandreh heredoc) #:prefix heredoc:)
+ ((org euandre queue) #:prefix queue:)
+ (gnu)
+ (guix build-system trivial)
+ (guix packages))
+(use-package-modules
+ admin
+ ssh
+ version-control)
+(use-service-modules
+ admin
+ certbot
+ cgit
+ dns
+ mail
+ mcron
+ networking
+ security
+ ssh
+ web)
+(heredoc:enable-syntax)
+
+
+(define +working-dir+
+ (canonicalize-path "."))
+
+(define (str . rest)
+ (apply string-append rest))
+
+(define (fmt . rest)
+ (apply format #f rest))
+
+(define (path s)
+ (str +working-dir+ "/" s))
+
+(define (slurp s)
+ (call-with-input-file
+ s
+ textual-ports:get-string-all))
+
+(define file
+ (compose slurp path))
+
+(define +tld+
+ (string-trim-right
+ (slurp (path "tld.txt"))))
+
+(define +ipv4+ "216.238.73.1")
+(define +ipv6+ "2001:19f0:b400:1582:5400:04ff:fea9:370e")
+
+(define +users+
+ '(("andre" "EuAndreh" ("wheel" "become-deployer" "become-secrets-keeper"))
+ ("laisse" "Laísses" ())))
+
+(define +user-accounts+
+ (map (lambda (user)
+ (let ((name (s1:first user))
+ (comment (s1:second user))
+ (groups (s1:third user)))
+ (user-account
+ (name name)
+ (comment comment)
+ (group "users")
+ (supplementary-groups groups))))
+ +users+))
+
+(define (ssh-file-for user)
+ (let ((name (s1:first user)))
+ (path (fmt "src/keys/SSH/~a.pub.txt" name))))
+
+(define +authorized-keys+
+ (let ((users-with-keys
+ (map (lambda (user)
+ `(,@user ,(slurp (ssh-file-for user))))
+ (filter (lambda (user)
+ (file-exists? (ssh-file-for user)))
+ +users+))))
+ (append
+ (map (lambda (user)
+ (let ((name (s1:first user))
+ (key (s1:fourth user)))
+ `(,name ,(plain-file (str name "-id_rsa.pub")
+ key))))
+ users-with-keys)
+ `(("git" ,@(map (lambda (user)
+ (let ((name (s1:first user))
+ (key (s1:fourth user)))
+ (plain-file (str name "-git-id_rsa.pub")
+ key)))
+ users-with-keys))))))
+
+(define (script name content)
+ (package
+ (name name)
+ (version "latest")
+ (source #f)
+ (build-system trivial-build-system)
+ (arguments
+ (list
+ #:modules '((guix build utils))
+ #:builder
+ #~(begin
+ (use-modules (guix build utils))
+ (let* ((bin (string-append %output "/bin"))
+ (prog (string-append bin "/" #$name)))
+ (mkdir-p bin)
+ (call-with-output-file prog
+ (lambda (port)
+ (display #$content port)))
+ (chmod prog #o755)))))
+ (home-page #f)
+ (synopsis #f)
+ (description #f)
+ (license #f)))
+
+
+(define ns1 (fmt "ns1.~a." +tld+))
+(define ns2 (fmt "ns2.~a." +tld+))
+(define ns ns1)
+(define mail (fmt "hostmaster.~a." +tld+))
+(define dkim-selector "dkimproxyout")
+(define dkim-public-key-path "/var/lib/dkimproxyout/public.key")
+
+(define dkim-name (str dkim-selector "._domainkey"))
+(define dkim-public-key
+ (if (file-exists? dkim-public-key-path)
+ (string-join
+ (reverse
+ (cdr
+ (reverse
+ (cdr
+ (string-split (slurp dkim-public-key-path)
+ #\newline)))))
+ "")
+ "stub-public-key-for-building"))
+
+(define ipv4-reverse-domain
+ (str
+ (string-join (reverse
+ (string-split +ipv4+
+ #\.))
+ ".")
+ ".in-addr.arpa"))
+
+(define ipv6-reverse-domain
+ (str
+ (string-join (reverse
+ (map (lambda (s) (fmt "~a" s))
+ (string->list
+ (string-delete #\: +ipv6+))))
+ ".")
+ ".ip6.arpa"))
+
+(define-zone-entries tld-zone
+ ("@" "" "IN" "NS" ns1)
+ ("@" "" "IN" "NS" ns2)
+ ("ns1" "" "IN" "A" +ipv4+)
+ ("ns1" "" "IN" "AAAA" +ipv6+)
+ ("ns2" "" "IN" "A" +ipv4+)
+ ("ns2" "" "IN" "AAAA" +ipv6+)
+
+ ("@" "" "IN" "A" +ipv4+)
+ ("@" "" "IN" "AAAA" +ipv6+)
+
+ ("@" "" "IN" "CAA" "0 issue \"letsencrypt.org\"")
+ ("@" "" "IN" "CAA" "0 issuewild \";\"")
+ ("@" "" "IN" "CAA" (fmt "0 iodef \"mailto:root@~a\"" +tld+))
+
+ ("mta-sts" "" "IN" "A" +ipv4+)
+ ("mta-sts" "" "IN" "AAAA" +ipv6+)
+ ("_mta-sts" "" "IN" "TXT" "\"v=STSv1; id=20230314\"")
+ ("@" "" "IN" "MX" (fmt "10 ~a." +tld+))
+ ("_dmarc" "" "IN" "TXT" "\"v=DMARC1; p=quarantine\"")
+ ("@" "" "IN" "TXT" (fmt "\"v=spf1 a:~a -all\"" +tld+))
+ (dkim-name "" "IN" "TXT" (fmt "\"v=DKIM1; k=rsa; t=s; p=~a\"" dkim-public-key)))
+
+(define-zone-entries ipv4-reverse-domain-zone
+ ("@" "" "IN" "PTR" (str +tld+ "."))
+ ("@" "" "IN" "NS" ns1)
+ ("@" "" "IN" "NS" ns2))
+
+(define-zone-entries ipv6-reverse-domain-zone
+ ("@" "" "IN" "PTR" (str +tld+ "."))
+ ("@" "" "IN" "NS" ns1)
+ ("@" "" "IN" "NS" ns2))
+
+(define zones
+ (list
+ (knot-zone-configuration
+ (domain +tld+)
+ (semantic-checks? #t)
+ (zone
+ (zone-file
+ (origin +tld+)
+ (ns ns)
+ (mail mail)
+ (entries tld-zone))))
+ (knot-zone-configuration
+ (domain ipv4-reverse-domain)
+ (semantic-checks? #t)
+ (zone
+ (zone-file
+ (origin ipv4-reverse-domain)
+ (ns ns)
+ (mail mail)
+ (entries ipv4-reverse-domain-zone))))
+ (knot-zone-configuration
+ (domain ipv6-reverse-domain)
+ (semantic-checks? #t)
+ (zone
+ (zone-file
+ (origin ipv6-reverse-domain)
+ (ns ns)
+ (mail mail)
+ (entries ipv6-reverse-domain-zone))))))
+
+
+(operating-system
+ (locale "en_GB.UTF-8")
+ (timezone "America/Sao_Paulo")
+ (host-name +tld+)
+ (skeletons
+ `((".profile"
+ ,(plain-file
+ "user-profile"
+ (file "src/config/profile.sh")))))
+ (users
+ (append
+ (list
+ (user-account
+ (name "git")
+ (group "git")
+ (system? #t)
+ (comment "External SSH Git user")
+ (home-directory "/srv/git")
+ (create-home-directory? #f)
+ (shell
+ (file-append git "/bin/git-shell")))
+ (user-account
+ (name "deployer")
+ (group "deployer")
+ (system? #t)
+ (comment "The account used to run deployment commands")
+ (home-directory "/var/empty")
+ (create-home-directory? #f)
+ (shell
+ (file-append shadow "/sbin/nologin")))
+ (user-account
+ (name "secrets-keeper")
+ (group "secrets-keeper")
+ (system? #t)
+ (comment "The account used to manage production secrets")
+ (home-directory "/var/empty")
+ (create-home-directory? #f)
+ (shell
+ (file-append shadow "/sbin/nologin"))))
+ +user-accounts+
+ %base-user-accounts))
+ (groups
+ (append
+ (list
+ (user-group
+ (name "git")
+ (system? #t))
+ (user-group
+ (name "deployer")
+ (system? #t))
+ (user-group
+ (name "become-deployer")
+ (system? #t))
+ (user-group
+ (name "secrets-keeper")
+ (system? #t))
+ (user-group
+ (name "become-secrets-keeper")
+ (system? #t)))
+ %base-groups))
+ (sudoers-file
+ (plain-file "sudoers" #"-
+ root ALL=(ALL) ALL
+ %wheel ALL= ALL
+ %become-deployer ALL=(deployer) NOPASSWD: ALL
+ %become-secrets-keeper ALL=(secrets-keeper) NOPASSWD: /run/current-system/profile/bin/rsync, /run/current-system/profile/bin/setfacl, /run/current-system/profile/bin/rm
+ git ALL= NOPASSWD: /run/current-system/profile/bin/reconfigure, /run/current-system/profile/bin/cicd
+ git ALL=(deployer) NOPASSWD: /run/current-system/profile/bin/rsync, /run/current-system/profile/bin/mkdir
+ "#))
+ (packages
+ (append
+ (map
+ (compose list specification->package+output symbol->string)
+ '(nss-certs
+ guile-heredoc
+ parted
+ acl
+ bind:utils
+ knot:tools
+ file
+ git
+ lsof
+ mailutils-sendmail
+ curl
+ make
+ borg
+ rsync
+ sqlite
+ strace
+ rlwrap
+ trash-cli
+ tree))
+ (list
+ (script "gc" (file "src/scripts/gc.sh"))
+ (script "cicd" (file "src/scripts/cicd.sh"))
+ (script "check" (file "src/scripts/check.sh"))
+ (script "backup" (file "src/scripts/backup.sh"))
+ (script "deploy" (file "src/scripts/deploy.sh"))
+ (script "report" (file "src/scripts/report.sh"))
+ (script "cronjob" (file "src/scripts/cronjob.sh"))
+ (script "reconfigure" (file "src/scripts/reconfigure.sh")))
+ %base-packages))
+ (services
+ (append
+ (list
+ (service ntp-service-type)
+ (service dhcp-client-service-type)
+ (service knot-service-type
+ (knot-configuration
+ (zones zones)))
+ (service openssh-service-type
+ (openssh-configuration
+ (openssh openssh-sans-x)
+ (password-authentication? #f)
+ (authorized-keys +authorized-keys+)
+ (extra-content #"-
+ ClientAliveInterval 30
+ ClientAliveCountMax 20
+ MaxSessions 20
+ SetEnv GIT_CONFIG_GLOBAL=/etc/gitconfig
+ "#)))
+ (simple-service 'extra-rottlog-rotations rottlog-service-type
+ (list
+ (log-rotation
+ (frequency 'weekly)
+ (files '("/var/log/cronjobs.log"))
+ (options '("rotate 52")))))
+ (service fail2ban-service-type)
+ (service mcron-service-type
+ (mcron-configuration
+ (jobs
+ (list
+ #~(job "0 0 * * *" "cronjob check")
+ #~(job "0 1 * * *" "cronjob env BORG_REPO=/mnt/backup/borg backup -q cron")
+ #~(job "0 2 * * *" "cronjob backup -q cron")
+ #~(job "0 3 * * 0" "cronjob gc")
+ #~(job "0 4 * * *" "cronjob reconfigure -U")))))
+ (service certbot-service-type
+ (certbot-configuration
+ (email (str "root@" +tld+))
+ (certificates
+ (list
+ (certificate-configuration
+ (domains (list +tld+))
+ (deploy-hook
+ (program-file
+ "nginx-deploy-hook"
+ #~(let ((pid (call-with-input-file "/var/run/nginx/pid" read)))
+ (kill pid SIGHUP)))))))))
+ (service nginx-service-type
+ (nginx-configuration
+ (server-blocks
+ (list
+ (nginx-server-configuration
+ (server-name (list +tld+))
+ (listen '("[::]:443 ssl http2" "443 ssl http2"))
+ (root "/srv/www")
+ (ssl-certificate (fmt "/etc/letsencrypt/live/~a/fullchain.pem" +tld+))
+ (ssl-certificate-key (fmt "/etc/letsencrypt/live/~a/privkey.pem" +tld+))
+ (locations
+ (list
+ (nginx-location-configuration
+ (uri "/api/")
+ (body
+ (list
+#;
+ (fmt "include /var/run/~a/curr.conf;~%" +tld+))))
+ (nginx-location-configuration
+ (uri "/git/static/")
+ (body
+ (list
+ (list "alias " cgit "/share/cgit/;"))))
+ (nginx-location-configuration
+ (uri "/git/")
+ (body
+ (list
+ (list "fastcgi_param SCRIPT_FILENAME " cgit "/lib/cgit/cgit.cgi;")
+ #"-
+ fastcgi_param PATH_INFO $uri;
+ fastcgi_param QUERY_STRING $args;
+ fastcgi_param HTTP_HOST $server_name;
+ fastcgi_pass localhost:9000;
+ rewrite /git(.*) $1 break;
+ "#)))))
+ (raw-content
+ '(#"-
+ # BearSSL still doesn't do TLSv1.3, so we deem TLSv1.2 as
+ # acceptable
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH;
+ ssl_prefer_server_ciphers on;
+ gzip off; # Disable catch-all compression due to BREACH
+ charset UTF-8;
+ autoindex on;
+ add_header Strict-Transport-Security 'max-age=31536000; includeSubdomains' always;
+ "#)))))))
+ (service cgit-service-type queue:cgit-pre-configuration)
+ (simple-service 'extra-etc-file etc-service-type
+ `(("rc" ,(plain-file "rc.sh" (file "src/config/rc.sh")))
+ ("known_hosts" ,(plain-file "known_hosts" (file "src/config/known_hosts.txt")))
+ ("id_rsa.pub" ,(plain-file "id_rsa.pub" (file (fmt "src/keys/SSH/root@~a.id_rsa.pub.stripped" +tld+))))
+ ("ssh.conf" ,(plain-file "ssh.conf" (file "src/config/ssh.conf")))
+ ("init.scm" ,(plain-file "init.scm" (file "src/config/init.scm")))
+ ("conf.env" ,(plain-file "conf.env" (file "src/config/conf.env")))
+ ("gitconfig" ,(plain-file "gitconfig" (file "src/config/gitconfig")))))
+ (service queue:shadow-group-service-type)
+ (service queue:dkimproxyout-service-type)
+ (service queue:cyrus-sasl-service-type)
+ (service queue:dovecot-service-type)
+ (service queue:internet-postfix-service-type
+ (queue:postfix-configuration
+ (enable-submission? #t)
+ (main.cf-extra #"-
+ message_size_limit = 102400000
+ mailbox_size_limit = 5120000000
+ "#)))
+ (service mail-aliases-service-type
+ `(("root" "andre")
+ ("support" ,@(map s1:first +users+)))))
+ (modify-services %base-services
+ (rottlog-service-type config =>
+ (rottlog-configuration
+ (inherit config)
+ (rc-file
+ (file-append queue:rottlog-mailutils-sendmail "/etc/rc")))))))
+ (bootloader
+ (bootloader-configuration
+ (bootloader grub-bootloader)
+ (targets '("/dev/vda"))))
+ (swap-devices
+ (list
+ (swap-space
+ (target
+ (uuid "fde5e4a8-acc2-4c9a-9712-5494724c2c04")))))
+ (file-systems
+ (append
+ (list
+ (file-system
+ (mount-point "/")
+ (device
+ (uuid "da72be6a-0c6b-4874-a57f-2046fcba13af" 'btrfs))
+ (type "btrfs"))
+ (file-system
+ (mount-point "/mnt/production")
+ (needed-for-boot? #t)
+ (device
+ (uuid "c50ad9fa-c7a1-49a1-93d2-6633f3cf929f" 'btrfs))
+ (type "btrfs"))
+ (file-system
+ (mount-point "/mnt/backup")
+ (device
+ (uuid "d675e98c-3f48-44d1-b085-36c476d9313f" 'btrfs))
+ (type "btrfs")))
+ %base-file-systems)))
diff --git a/src/keys/GPG/andre.asc b/src/keys/GPG/andre.asc
new file mode 100644
index 0000000..9164cbd
--- /dev/null
+++ b/src/keys/GPG/andre.asc
@@ -0,0 +1,86 @@
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+s5K/1f77Aqr6hdYRfw5KqGs=
+=kMg0
+-----END PGP PUBLIC KEY BLOCK-----
diff --git a/src/keys/SSH/andre.pub.txt b/src/keys/SSH/andre.pub.txt
new file mode 100644
index 0000000..bfd5e6f
--- /dev/null
+++ b/src/keys/SSH/andre.pub.txt
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDnUv7iWOejQNa3fZ6v4lkHT6qFRp2+NuzIpFJ2Vy7eP58XZoiz6HJPcCU8Hf95JXwaXEwS4S7mXdw1x60hd8JIe058Ek6MZSSVQmlLfocGsAYj1wTrLmnQ8+PV0IeQlNj1aytBI1fL+v3IPt+JdLt6b+g3vwcEUU9efzxx2E0KZ5GIpb2meiCQ6ha+tcd7XqegB53eQj/h/coE2zLJodpaJ3xbj894pE/OJCNC0+4d0Sv7oHhY7QoLYldTQbSgPyhyfl4iZpJf6OEPZxK2cJaB+cbeoBB6aGNyU+CIJToM+uAJJ7H7EpvxfcnfJQ1PuY5szTdvFbW820euiUEKEW69mW4uaFNPSc6D4Z8tZ5hXQIqBD40irULhF0CYNkIILmyNV/KJIZ5HkbQ1q+UrCFHJyvuH/3aCTjj9OSfE7xHPQ3xd3Xw8vvj0Mjie09xFbbcklBTw5WRzH7cw8c+Q0O69kZZ8b+ykcdzWTeZeWNdnzptNqnMjfheig90rUIJ7DN0c+53jCUcGpWJxJhcYF9Uk1RNHmSE5+VzK1y+20t0grVFX90nApm4Tl35QPrX7Qxp9C81cWiUB8xCAE6jYrmd4x+P/3wSQfc1Xg0Eg3QjJB+6JD7cbyDJpzDR3ja+CLZCAr9I0B4rDKD2d6et/z67iXPnZUWMyZ8RVVZPFbBMOTw== openpgp:0xF727046D
diff --git a/src/keys/SSH/laisse.pub.txt b/src/keys/SSH/laisse.pub.txt
new file mode 100644
index 0000000..9dc9d85
--- /dev/null
+++ b/src/keys/SSH/laisse.pub.txt
@@ -0,0 +1 @@
+ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+cecVzuCf1GfURtCa8MDEPZ4bcJSnakcunlWw4FTCl9XTyO46Wx5wRfEHPMMhu6tE55OzbqxbdZLIoni4PvUA1KxAddJuz6vuAmAxGA2a46Xg5Pi+efYGue2194cYAyCW6dn7RcU+aTNoHTGVdypjUcTZwkQ7hBSlz0ICbVWYUa6qDbsKK68bWuhSoOFzOkERHQEBhXcIkg0uKZmTsDzEJZ+2H0kUgdvsaUKDTPpujPU7AOV9sDEFyNDh77aX1RSx14J3gCiJuWAYk1iLuoxbvg5VOieePRRvaDbtJt3RvSFHgNiVHWnBtK1FAg2EbMHl72dWSKhdjpPD37AQE0GX
diff --git a/src/keys/SSH/root@papo.im.id_rsa.pub.stripped b/src/keys/SSH/root@papo.im.id_rsa.pub.stripped
new file mode 100644
index 0000000..9d6cf4b
--- /dev/null
+++ b/src/keys/SSH/root@papo.im.id_rsa.pub.stripped
@@ -0,0 +1 @@
+FIXME
diff --git a/src/keys/SSH/root@papo.im.id_rsa.pub.txt b/src/keys/SSH/root@papo.im.id_rsa.pub.txt
new file mode 100644
index 0000000..9d6cf4b
--- /dev/null
+++ b/src/keys/SSH/root@papo.im.id_rsa.pub.txt
@@ -0,0 +1 @@
+FIXME
diff --git a/src/keys/gpg-import.sh b/src/keys/gpg-import.sh
new file mode 100755
index 0000000..63d2347
--- /dev/null
+++ b/src/keys/gpg-import.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ gpg-import.sh
+ gpg-import.sh -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Import GPG keys under src/infrastructure/keys/GPG/ and mark them as
+ trusted, so that they can be used as recipients for encryption.
+
+
+ Examples:
+
+ Just run it:
+
+ $ gpg-import.sh
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+gpg --import src/infrastructure/keys/GPG/*
+
+gpg --with-colons --show-key src/infrastructure/keys/GPG/* |
+ awk -F: '$1 == "fpr" { print $10 }' |
+ while read -r fpr; do
+ printf '5\ny\n' |
+ gpg --command-fd 0 --expert --edit-key "$fpr" trust
+ done
diff --git a/src/keys/gpg-recipients.sh b/src/keys/gpg-recipients.sh
new file mode 100755
index 0000000..ad6e522
--- /dev/null
+++ b/src/keys/gpg-recipients.sh
@@ -0,0 +1,71 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ gpg-recipients.sh
+ gpg-recipients.sh -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Process GPG keys under src/infrastructure/keys/GPG/, and emit
+ the command-line flags to be given to the `gpg` command, as in:
+
+ $ gpg -r KEY1 -r KEY2 ...
+
+ gpg-recipients.sh emits the `-r KEY1 -r KEY2` part, getting
+ those values from the fingerprints of the GPG keys in the
+ directory.
+
+
+ Examples:
+
+ Just run it:
+
+ $ gpg-recipients.sh
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+gpg --with-colons --show-key src/infrastructure/keys/GPG/* |
+ awk -F: '$1 == "fpr" { printf " -r %s", $10 }'
diff --git a/src/scripts/backup.sh b/src/scripts/backup.sh
new file mode 100755
index 0000000..6a2a4ff
--- /dev/null
+++ b/src/scripts/backup.sh
@@ -0,0 +1,149 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ backup [-q] [-C COMMENT] [-x] [ARCHIVE_TAG]
+ backup -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -q disable verbose mode, useful for batch sessions
+ -C COMMENT the comment text to be attached to the archive
+ -x disable checking the repository after creating the backup
+ -h, --help show this message
+
+ ARCHIVE_TAG the tag used to create the new
+ backup (default: "manual")
+
+
+ The repository is expected to have been create with:
+
+ $ borg init -e repokey-blake2
+
+ The following environment variables are expected to be exported:
+
+ $BORG_PASSCOMMAND
+ $BORG_REPO
+ $BORG_REMOTE_PATH
+
+ Password-less SSH access is required, usually done via adding
+ /root/.ssh/id_rsa.pub to the ssh remote's
+ $THE_REMOTE:.ssh/authorized_keys
+
+ Root permission is also required.
+
+
+ Examples:
+
+ Run backup from cronjob:
+
+ $ backup
+
+
+ Run backup from cronjob:
+
+ $ backup -q cronjob
+
+
+ Create backup with a comment, a tag, and verbose mode active, and do not
+ verify the repository afterwards:
+
+ $ backup -xC 'The backup has a comment'
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+VERBOSE_FLAGS='--verbose --progress'
+COMMENT=' '
+CHECK=true
+while getopts 'qC:xh' flag; do
+ case "$flag" in
+ q)
+ VERBOSE_FLAGS=''
+ ;;
+ C)
+ COMMENT="$OPTARG"
+ ;;
+ x)
+ CHECK=false
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+ARCHIVE_TAG="${1:-manual}"
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+run() {
+ STATUS=0
+ set -x
+ # shellcheck disable=2086
+ sudo -i borg create \
+ $VERBOSE_FLAGS \
+ --comment "$COMMENT" \
+ --stats \
+ --compression lzma,9 \
+ "$BORG_REPO::$(hostname)-{now}-$ARCHIVE_TAG" \
+ /mnt/production/ \
+ /root/ \
+ /home/ \
+ /etc/ \
+ /var/ \
+ /opt/ \
+ /srv/ || STATUS=$?
+ set +x
+
+ if [ "$STATUS" = 0 ]; then
+ return 0
+ elif [ "$STATUS" = 1 ]; then
+ printf 'WARNING, but no ERROR.\n' >&2
+ return 0
+ else
+ return "$STATUS"
+ fi
+}
+
+run
+
+if [ "$CHECK" = true ]; then
+ # shellcheck disable=2086
+ sudo -i borg check $VERBOSE_FLAGS --verify-data "$BORG_REPO"
+fi
diff --git a/src/scripts/check.sh b/src/scripts/check.sh
new file mode 100755
index 0000000..5c63816
--- /dev/null
+++ b/src/scripts/check.sh
@@ -0,0 +1,92 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ check
+ check -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Run system sanity checks, such as email reachability, alarms
+ reachability, filesystem checks, etc.
+
+
+ Examples:
+
+ Just run it
+
+ $ check
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+for alias in abuse admin postmaster hostmaster; do
+ uuid | mail -s "\"$alias\" alias test from $(id -un)@$(hostname)" "$alias@$(hostname)"
+done
+
+
+PARTITIONS='
+/dev/vda3
+/dev/vdb1
+/dev/vdc1
+'
+set -x
+
+for part in $PARTITIONS; do
+ btrfs scrub start -B "$part"
+ btrfs check --force -p "$part"
+done
diff --git a/src/scripts/cicd.sh b/src/scripts/cicd.sh
new file mode 100755
index 0000000..58f4fce
--- /dev/null
+++ b/src/scripts/cicd.sh
@@ -0,0 +1,168 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ cicd [-n] NAME [SHA]
+ cicd -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -n build the system, but don't switch to it (dry-run)
+ -h, --help show this message
+
+ NAME the name of the project
+ SHA the repository SHA to checkout (default: main)
+
+
+ Do a CI/CD run of the project called NAME, located at
+ /srv/git/$NAME.git. If -n is given, only build, otherwise
+ also do the deploy when the build is successfull.
+
+ A "build" consists of:
+ - doing a fresh clone of the project on a temporary directory;
+ - checkout the project to version $SHA;
+ - when a "manifest.scm" file exists in the root of the project,
+ use it to launch a containerized Guix shell; otherwise use a
+ fallback template for a containerized Guix shell;
+ - build the "dev" target of the Makefile via "make dev".
+
+ A "deploy" consists of:
+ - copying the "description" metadata file to
+ /srv/git/$NAME.git/description, in order to update the project
+ repository's description;
+ - upgrading the "pre-receive" Git hook, so that future runs are
+ affected by it;
+ - copying the "public/" directory that the "dev" target built to
+ the /srv/www/s/$NAME/ directory, so that the projects "public/"
+ directory is accessible via the web address
+ "https://euandre.org/s/$NAME/".
+
+ This command must be ran as root.
+
+
+ Examples:
+
+ Build and deploy the "remembering" project on the default branch:
+
+ $ sudo cicd remembering
+
+
+ Build the "urubu" project on a specific commit, but don't deploy:
+
+ $ sudo cicd -n urubu 916dafc092f797349a54515756f2c8e477326511
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+DRY_RUN=false
+while getopts 'nh' flag; do
+ case "$flag" in
+ n)
+ DRY_RUN=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+NAME="${1:-}"
+SHA="${2:-main}"
+REPO="/srv/git/$NAME.git"
+
+if [ -z "$NAME" ]; then
+ printf 'Missing NAME.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+set +eu
+# shellcheck source=/dev/null
+. /etc/rc
+set -eu
+
+
+uuid() {
+ od -xN20 /dev/urandom |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+tmpname() {
+ printf '%s/uuid-tmpname with spaces.%s' "${TMPDIR:-/tmp}" "$(uuid)"
+}
+
+mkdtemp() {
+ name="$(tmpname)"
+ mkdir -- "$name"
+ printf '%s' "$name"
+}
+
+
+TMP="$(mkdtemp)"
+trap 'rm -rf "$TMP"' EXIT
+
+
+set -x
+chown deployer:deployer "$TMP"
+cd "$TMP"
+sudo -u deployer git clone "$REPO" .
+sudo -u deployer --preserve-env=GIT_CONFIG_GLOBAL git checkout "$SHA"
+guix system describe
+
+if [ -f manifest.scm ]; then
+ guix shell -Cv3 -m manifest.scm -- make dev
+else
+ guix shell -Cv3 -- make dev
+fi
+
+if [ "$DRY_RUN" = false ]; then
+ # COMMENT: pre-receive is always running the previous version!
+ # The same is true for the reconfigure script itself.
+ sudo cp description "$REPO"/description
+ sudo cp aux/ci/git-pre-receive.sh "$REPO"/hooks/pre-receive
+
+ sudo -u deployer rsync \
+ --delete \
+ --chmod=D775,F664 \
+ --chown=deployer:deployer \
+ --exclude 'ci/*' \
+ -a \
+ public/ /srv/www/s/"$NAME"/
+fi
diff --git a/src/scripts/cronjob.sh b/src/scripts/cronjob.sh
new file mode 100755
index 0000000..4cd456e
--- /dev/null
+++ b/src/scripts/cronjob.sh
@@ -0,0 +1,169 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ cronjob COMMAND...
+ cronjob -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ COMMAND the command to be executed
+
+
+ Execute the given command, and send the output to email, with
+ special treatment to the status code. It kills the job it it
+ lasts more than one hour.
+
+ It load the appropriate files, so that the actual cron
+ declaration is smaller.
+
+
+ Examples:
+
+ Run a backup:
+
+ $ cronjob backup -q cron
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${1:-}" ]; then
+ printf 'Missing COMMAND.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+set +eu
+# shellcheck source=/dev/null
+. /etc/rc
+set -eu
+
+
+
+epoch() {
+ awk 'BEGIN { srand(); print(srand()); }'
+}
+
+now() {
+ date '+%Y-%m-%dT%H:%M:%S%:z'
+}
+
+
+uuid() {
+ od -xN20 /dev/random |
+ head -n1 |
+ awk '{OFS="-"; print $2$3,$4,$5,$6,$7$8$9}'
+}
+
+mkstemp() {
+ name="${TMPDIR:-/tmp}/uuid-tmpname with spaces.$(uuid)"
+ touch "$name"
+ printf '%s' "$name"
+}
+
+pre() {
+ # Same as:
+ # sed -u "s|^|[$CMD]: |"
+ # but the "-u" option is not POSIX
+ IFS=''
+ while read -r line; do
+ printf '[%s]: %s\n' "$CMD" "$line"
+ done
+}
+
+duration() {
+ minutes=$((${1} / 60))
+ seconds=$((${1} % 60))
+ printf '%sm%ss' "$minutes" "$seconds"
+}
+
+
+CMD="$*"
+HOSTNAME="$(hostname)"
+FROM="cronjob@$HOSTNAME"
+TIMEOUT='10800' # three hours
+STATUS_F="$(mkstemp)"
+OUT="$(mkstemp)"
+
+email() {
+ {
+ cat <<-EOF
+ Content-Type: text/plain; charset=UTF-8
+ Content-Transfer-Encoding: 8bit
+ From: $FROM
+ To: root@localhost
+ Subject: (exit status: $(cat "$STATUS_F")) - $HOSTNAME: $CMD
+
+ EOF
+ cat "$OUT"
+ } | sendmail -t -f "$FROM"
+ rm -f "$OUT" "$STATUS_F"
+}
+trap email EXIT
+
+{
+ cat <<-EOF
+ Running commad: $CMD
+ Starting at: $(now)
+
+ EOF
+
+ START="$(epoch)"
+ STATUS=0
+ timeout "$TIMEOUT" "$@" || STATUS=$?
+ printf '%s' "$STATUS" > "$STATUS_F"
+ END="$(epoch)"
+ DURATION_SECONDS=$((END - START))
+
+ cat <<-EOF
+
+ Finished at: $(now)
+ Duration: $(duration "$DURATION_SECONDS")
+ EOF
+} 2>&1 | pre | ts '%Y-%m-%dT%H:%M:%S' | tee "$OUT" >> /var/log/cronjobs.log
diff --git a/src/scripts/deploy.sh b/src/scripts/deploy.sh
new file mode 100755
index 0000000..dc30484
--- /dev/null
+++ b/src/scripts/deploy.sh
@@ -0,0 +1,72 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ deploy
+ deploy -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+
+ Do a blue/green deployment of the main service. It makes sure
+ that the new service is up and running before shutting down the
+ old one.
+
+
+ Examples:
+
+ Just do the deploy:
+
+ $ deploy
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+echo FIXME
diff --git a/src/scripts/gc.sh b/src/scripts/gc.sh
new file mode 100755
index 0000000..e037f3c
--- /dev/null
+++ b/src/scripts/gc.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ gc [TYPE]
+ gc -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -h, --help show this message
+
+ TYPE what to do GC on (default: all):
+ - guix
+ - deploy
+ - trash
+ - tmpdir
+ - logs
+
+
+ GC the server, deleting old, unusable data, in order to free
+ disk space system-wide.
+
+
+ Examples:
+
+ Just run it, for all:
+
+ $ gc
+
+
+ Cleanup tmpdir:
+
+ $ gc tmpdir
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+while getopts 'h' flag; do
+ case "$flag" in
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+disk() {
+ df -h / /mnt/backup/ |
+ tail -n +2 |
+ awk '{ printf "%s\t%s/%s\t%s\n", $4, $3, $2, $6 }'
+}
+
+today() {
+ date '+%Y-%m-%d'
+}
+
+gc_guix() {
+ sudo -i guix system delete-generations 1m
+ sudo -i guix gc -d 1m
+}
+
+gc_deploy() {
+ find /opt/deploy \
+ ! -path /opt/deploy -prune \
+ -type d \
+ -not -name "$(today)*" \
+ -exec rm -rvf "{}" ';'
+}
+
+gc_trash() {
+ yes | sudo -i trash-empty
+}
+
+gc_tmpdir() {
+ find "${TMPDIR:-/tmp}" -atime +10 -exec rm -vf "{}" ';'
+}
+
+gc_logs() {
+ find /var/log/ci/ -atime +10 -exec rm -vf "{}" ';'
+}
+
+
+gc_all() {
+ gc_guix
+ gc_deploy
+ gc_trash
+ gc_tmpdir
+ gc_logs
+}
+
+
+TYPE="${1:-all}"
+CMD=gc_"$TYPE"
+if ! command -v "$CMD" >/dev/null; then
+ printf 'Invalid TYPE: "%s".\n\n' "$TYPE" >&2
+ usage >&2
+ exit 2
+fi
+
+BEFORE="$(disk)"
+set -x
+"$CMD"
+set +x
+AFTER="$(disk)"
+
+cat <<-EOF
+ Disk space:
+ before: $BEFORE
+ after: $AFTER
+EOF
diff --git a/src/scripts/reconfigure.sh b/src/scripts/reconfigure.sh
new file mode 100755
index 0000000..8fa47c5
--- /dev/null
+++ b/src/scripts/reconfigure.sh
@@ -0,0 +1,146 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ reconfigure [-n] [-U] [SHA]
+ reconfigure -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -n build the system, but don't switch to it (dry-run)
+ -U pull the latest channels before reconfiguring
+ -h, --help show this message
+
+ SHA the repository SHA to checkout (default: main)
+
+
+ Run a "guix system reconfigure" as root via "sudo -i". If a -U
+ flag is given, perform a "guix pull" (in root profile) prior to
+ the reconfigure. The user must be able to become the "deployer"
+ user, either via "sudo reconfigure" or by being member of the
+ "become-deployer" group.
+
+
+ Examples:
+
+ Reconfigure the system:
+
+ $ reconfigure
+
+
+ Build the system on a custom SHA, but don't switch to it:
+
+ $ reconfigure -n 916dafc092f797349a54515756f2c8e477326511
+
+
+ Update and upgrade:
+
+ $ reconfigure -U
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+UPDATE=false
+DRY_RUN=false
+while getopts 'nUh' flag; do
+ case "$flag" in
+ n)
+ DRY_RUN=true
+ ;;
+ U)
+ UPDATE=true
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ usage >&2
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+# shellcheck source=/dev/null
+. /etc/conf.env
+SHA="${1:-main}"
+REPO="/srv/git/$REPO_NAME"
+NOW="$(date '+%Y-%m-%dT%H:%M:%S%:z')"
+NOW_DIR=/opt/deploy/"$NOW"
+NPROC=$(($(nproc) * 2 + 1))
+
+
+if [ "$(id -un)" != 'root' ]; then
+ printf 'This script must be run as root.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+
+set +eu
+# shellcheck source=/dev/null
+. /etc/rc
+set -eu
+
+
+if [ "$UPDATE" = true ] && [ "$DRY_RUN" = false ]; then
+ sudo -i guix pull -v3
+fi
+
+set -x
+sudo -u deployer git clone --depth=1 "file://$REPO" "$NOW_DIR"
+sudo -u deployer rm -f /opt/deploy/current
+sudo -u deployer ln -rs "$NOW_DIR" /opt/deploy/current
+cd /opt/deploy/current
+sudo -u deployer git fetch --depth=1 "file://$REPO" "$SHA"
+sudo -u deployer --preserve-env=GIT_CONFIG_GLOBAL git checkout "$SHA"
+guix system describe
+
+if [ "$DRY_RUN" = true ]; then
+ sudo -i guix system -c$NPROC -v3 build "$PWD"/src/infrastructure/guix/system.scm
+else
+ # COMMENT: pre-receive is always running the previous version!
+ # The same is true for the reconfigure script itself.
+ cp description "$REPO"/description
+ cp src/infrastructure/ci/git-pre-receive.sh "$REPO"/hooks/pre-receive
+ cp src/infrastructure/guix/channels.scm /etc/guix/
+ cp src/infrastructure/guix/system.scm /etc/guix/
+
+ sudo -i guix system -c$NPROC -v3 reconfigure /etc/guix/system.scm
+
+ sudo -u deployer rsync \
+ --delete \
+ --chmod=D775,F664 \
+ --chown=deployer:deployer \
+ --exclude "$CI_SUFFIX/*" \
+ -a \
+ /run/current-system/profile/share/doc/"$NAME"/ "$HTML_OUTDIR_TOP"/
+
+ ln -sf /var/log/"$NAME".log "$HTML_OUTDIR_PRIV"
+
+ deploy
+fi
diff --git a/src/scripts/report.sh b/src/scripts/report.sh
new file mode 100755
index 0000000..e14e40a
--- /dev/null
+++ b/src/scripts/report.sh
@@ -0,0 +1,260 @@
+#!/bin/sh
+set -eu
+
+usage() {
+ cat <<-'EOF'
+ Usage:
+ report [-C REPO] -o DIRECTORY
+ report -h
+ EOF
+}
+
+help() {
+ cat <<-'EOF'
+
+
+ Options:
+ -C REPO change to REPO when doing Git operations (default: $PWD)
+ -o DIRECTORY the directory where to place the generated files
+ -h, --help show this message
+
+
+ Gather data from Git Notes, and generate an HTML report on CI runs.
+
+ Two refs with notes are expected:
+ 1. refs/notes/ci-data: contains metadata abount the CI runs,
+ with timestamps, filenames and exit status;
+ 2. refs/notes/ci-logs: contains the content of the log.
+
+ When reconstructing the CI run, the $FILENAME present in
+ the refs/notes/ci-data ref names the file, and its content comes
+ from refs/notes/ci-logs.
+
+ On a CI run that generated the numbers from 1 to 10, for a file named
+ 'my-ci-run-2020-01-01-deadbeef.log' that exited successfully, the
+ expected output on the target directory "public" is:
+
+ $ tree public/
+ public/
+ index.html
+ data/
+ my-ci-run-2020-01-01-deadbeef.log
+ ...
+ logs/
+ my-ci-run-2020-01-01-deadbeef.log
+ ...
+
+ $ cat public/data/my-ci-run-2020-01-01-deadbeef.log
+ 0 deadbeef my-ci-run-2020-01-01-deadbeef.log
+
+ $ cat public/logs/my-ci-run-2020-01-01-deadbeef.log
+ 1
+ 2
+ 3
+ 4
+ 5
+ 6
+ 7
+ 8
+ 9
+ 10
+
+ The generated 'index.html' is a webpage with the list of all known
+ CI runs, their status, a link to the commit and a link to the
+ log file.
+
+ To enable fetching these refs by default, do so in the git config:
+
+ $ git config --add remote.origin.fetch '+refs/notes/*:refs/notes/*'
+
+
+ Examples:
+
+ Generate the report on the 'www' directory:
+
+ $ report -o www
+ EOF
+}
+
+
+for flag in "$@"; do
+ case "$flag" in
+ --)
+ break
+ ;;
+ --help)
+ usage
+ help
+ exit
+ ;;
+ *)
+ ;;
+ esac
+done
+
+REPO="$PWD"
+while getopts 'C:o:h' flag; do
+ case "$flag" in
+ C)
+ REPO="$OPTARG"
+ ;;
+ o)
+ OUTDIR="$OPTARG"
+ ;;
+ h)
+ usage
+ help
+ exit
+ ;;
+ *)
+ exit 2
+ ;;
+ esac
+done
+shift $((OPTIND - 1))
+
+if [ -z "${OUTDIR:-}" ]; then
+ printf 'Missing -o OUTDIR.\n\n' >&2
+ usage >&2
+ exit 2
+fi
+
+if [ -r src/infrastructure/config/conf.env ]; then
+ CONF=src/infrastructure/config/conf.env
+else
+ CONF=/etc/conf.env
+fi
+
+# shellcheck source=/dev/null
+. "$CONF"
+
+
+esc() {
+ sed \
+ -e 's|&|\&amp;|g' \
+ -e 's|<|\&lt;|g' \
+ -e 's|>|\&gt;|g' \
+ -e 's|"|\&quot;|g' \
+ -e "s|'|\&#39;|g"
+}
+
+mkdir -p "$OUTDIR"
+cd "$OUTDIR"
+mkdir -p logs data
+
+for c in $(git -C "$REPO" notes list | cut -d' ' -f2); do
+ git -C "$REPO" notes --ref=refs/notes/ci-data show "$c" > data/FILENAME-tmp
+ FILENAME="$(grep '^filename ' data/FILENAME-tmp | cut -d' ' -f2-)"
+ mv data/FILENAME-tmp data/"$FILENAME"
+ git -C "$REPO" notes --ref=refs/notes/ci-logs show "$c" > logs/"$FILENAME"
+done
+
+{
+ cat <<-EOF
+ <!DOCTYPE html>
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="description" content="CI logs for $NAME" />
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
+ <title>$NAME - CI logs</title>
+ <style>
+ body {
+ max-width: 800px;
+ margin: 0 auto;
+ }
+
+ code {
+ display: block;
+ margin: 1em 0em 3em 3em;
+ overflow: auto;
+ }
+
+ pre {
+ display: inline;
+ }
+
+ ol {
+ list-style-type: disc;
+ }
+
+ pre, code {
+ background-color: #ddd;
+ }
+
+ @media(prefers-color-scheme: dark) {
+ :root {
+ color: white;
+ background-color: black;
+ }
+
+ a {
+ color: hsl(211, 100%, 60%);
+ }
+
+ a:visited {
+ color: hsl(242, 100%, 80%);
+ }
+
+ pre, code {
+ background-color: #222;
+ }
+ }
+ </style>
+ </head>
+ <body>
+ <main>
+ <h1>
+ CI logs for
+ <a href="$HOMEPAGE">$NAME</a>
+ </h1>
+ <ol>
+ EOF
+
+
+ PASS='&#x2705;' # ✅
+ WARN='&#x1F40C;' # 🐌
+ FAIL='&#x274C;' # ❌
+ find data/ -type f | LANG=C.UTF-8 sort -r | while read -r f; do
+ STATUS="$( grep '^status ' "$f" | cut -d' ' -f2- | esc)"
+ SHA="$( grep '^sha ' "$f" | cut -d' ' -f2- | esc)"
+ FILENAME="$(grep '^filename ' "$f" | cut -d' ' -f2- | esc)"
+ DURATION="$(grep '^duration ' "$f" | cut -d' ' -f2- | cut -d'"' -f1 | esc)"
+ MESSAGE="$({
+ git -C "$REPO" log -1 --format=%B "$SHA" || {
+ git fetch origin "$SHA"
+ git -C "$REPO" log -1 --format=%B "$SHA"
+ }
+ } | esc)"
+
+ if [ "$STATUS" = 0 ]; then
+ if [ "$DURATION" -le 60 ]; then
+ STATUS_MARKER="$PASS"
+ else
+ STATUS_MARKER="$WARN"
+ fi
+ else
+ STATUS_MARKER="$FAIL"
+ fi
+
+ cat <<-EOF
+ <li id="$FILENAME">
+ <a href="#$FILENAME"><pre>#</pre></a>
+ $STATUS_MARKER - <pre>${DURATION:-?}s</pre>
+ <pre>(<a href="${CGIT_URL}${SHA}">commit</a>)</pre>
+ <a href="logs/$FILENAME"><pre>$FILENAME</pre></a>
+ <pre>(<a href="data/$FILENAME">data</a>)</pre>
+ <br />
+ <code><pre>$MESSAGE</pre></code>
+ </li>
+ EOF
+ done
+
+ cat <<-EOF
+ </ol>
+ </main>
+ </body>
+ </html>
+ EOF
+} > index.html
diff --git a/src/secrets/VPS-root.txt.gpg b/src/secrets/VPS-root.txt.gpg
new file mode 100644
index 0000000..b59ff3b
--- /dev/null
+++ b/src/secrets/VPS-root.txt.gpg
@@ -0,0 +1,18 @@
+-----BEGIN PGP MESSAGE-----
+
+hQIMAxJMwbOrBV1aARAArxPpuV4r4+yYBPz4qhSjbyQmRU+EihwxNC+ARAGpiNn/
+VARwHARJwPrJX0SP7qqtyzz5xqWhjhquAo+GL463T313Wbroaj4hD4NJDe2dagtU
+SFTbxwmj2iWiS6PhwHG7H3YU+eRK0p58guNiMS23delRZUyVxYrlMtq7RPtyLSxw
+7KIi+/bA+bGaxTdu162BPC+upRwBBOVUrQxHpFuwzZZ/g0L6JLG1SzP0/sXTYyJQ
+P2db6p6vUieXCW1VIN7P30/vWzGmCNtOpNtCPSrgvx2C6YGL5OdvJT+gbEjWXhLg
+jLZc7DXE/zLN/fAleFr9U4rquopmP+O/df3JRDvmY+1H6e5QT6Se9a6EsF3JW9ys
+odwKjkYqQGljuJ4oB4rygAx46X8C+0xkKNexsbP/nAJ6UV97mwUgU6lNtCcYoA+B
+iU1UkU9xbjiSr6N6VvNx+0/oFMyoTHB/uwLxuaU7Dk+f+SHAMSCsnvGMLXzaSXPl
+rRdSTAn0mGbDVwuNv/LtOwM2zyss0zfd3gxS2R1+IXX0eSLzCVN2BlUITGqNE4gz
+jT/7LCK0gexJGCoe5Ux7jDxUBVmQKjT3XTCMKcls1MzxfxXfk7iw8Lp3T3OhkCX5
+yfaaAYY3xSB/MBim8PX0jE6W8TbdRvMqyfFr/TehJkjJ4NrSCGBi68OOnAzcl9fS
+YAFGGaEqRK5bClwmYcgwo4hOZjWNNlh061FWDET7Qz8rlZiT4+KLWkERCqUKhJWG
+fVtgfy1yA4QBV9KVIN4GBAoqnR8oo10154SD4CjSQmWjaJsb+Mn10T3XXmW2QFex
+pw==
+=rFvw
+-----END PGP MESSAGE-----
diff --git a/src/secrets/borg-passphrase.txt.gpg b/src/secrets/borg-passphrase.txt.gpg
new file mode 100644
index 0000000..f49548a
--- /dev/null
+++ b/src/secrets/borg-passphrase.txt.gpg
@@ -0,0 +1,18 @@
+-----BEGIN PGP MESSAGE-----
+
+hQIMAxJMwbOrBV1aARAAy2JxbJvEmNkNDuM+bPOcatFwNOo+0YVPQdahDaeIrXqq
+DgUYxZBuauNPP95NhlNWG2TgmeDGxk6K/KEmYFF+6Uv2zJ9sE9IkSvgt50iacO6h
+2Dk/dLGi8a6F2tOpwR3vcx1KFCtMtsIKba0e85Uj9D1d2QsTX/546pTDX/LaxQQI
+D4hAK0t3NEzvTVD3Dd7mFsq1gRTf8+aPnm+J5UdYs6QfimcUKTSDsfmWxmzeEXaj
+znKGdtMqdyjtNibK5U92elDvt+bt/8m3mh50zaFPb8t5xQwJVvgsXZd+tx2DWinz
+APt7UWf5k63HTm2aRX/XFw1Dsjr4ujkWRl9XPiw6C4taCbT0UjV9n236pbfA/WXd
+e3tZy26Csd3UIIEg26tdH9t87TfZN2TBH6wZge1yyFUWckXywJKaeegMArFZ6s0A
+7MqFknA9g0vgJFIiOZB80YGl9xDKmAA1RTSh1g0eBvz0OSAe+Z5EgmJ9IcnBMRyb
+xZFM2Y8R7z94WoEGl7CYtT0UfobRcQLy7vqKTVzaZmNLAYNUYbM5bBr1NZxPHe1G
+44Lxtdjmw53uDZLo4AKGNdDjfyTHL0hctWhCTQJ5oaXm1G6FNJF7YxHejdTrqIBW
+CNk7IcKAkIQVG5y09uBTKi4Yo3n7/yIh8UmeyYWPkkHuuoGyCJHMBTnwlq2Kou7S
+YAGQfq0rVlgWrtdrJXXpEqxAv8xWsu2UR1+0yKyVaWsMghEU6HXTVoLtQcM6WP6w
+bDc4CEU7peGhqeAw+zeNcvsaWub9eNBNpTZu1IVsyDG1I+Qqni8SVpgztAASYR9d
+pg==
+=tQYt
+-----END PGP MESSAGE-----
diff --git a/src/secrets/root@papo.im.id_rsa.txt.gpg b/src/secrets/root@papo.im.id_rsa.txt.gpg
new file mode 100644
index 0000000..c3bd663
--- /dev/null
+++ b/src/secrets/root@papo.im.id_rsa.txt.gpg
@@ -0,0 +1,18 @@
+-----BEGIN PGP MESSAGE-----
+
+hQIMAxJMwbOrBV1aARAAlyc6jYsn5hVbnfZktuOsXZcn+3Z0aHFLbQJsL4yR32yN
+nGE5avTpGSff3qIKcjvN1G4+AN7O97nos3iULQWUTmhrCtZzxRgA0+hwI+qVqLRm
+BorRrIKzOZsQBCrbdhomm1Hwy+g/kEkU91Ux3sCLSfS8B5315maqZv5MaM6KyBMU
+bCbkjnokiCmkp0UTq4VBM1sh/gSoqgpajIp/q24MBg1T8v0Y4LPL2E7lXY0IvuF7
+q0XYG/E8nUv8aC1rgeU8EUAHGhwTRvE3/BVYNSNZv9RHsFSpZG/FpuKu2FO8VpIG
+zgjOKIYH+dA7ilu86suRRuiGMzLw4ifSlpcBmsz1a+8ITSxoVQC9V34rIdujhyYu
+Mk5I79+54ZP974tv7lY88NCF8a5vQekRIeRGQxql0U1RP5l0I0nBq/PRiU8n2EF9
+gO9CpOiviT8odM/r34ZFxeRJ1x/gPXaUCiw9B2E0N2vemElaSSyY7+bFHJJIgmYi
+DYUChG15HcsZeBpbqxkDQhNrP8z/0Tlm8Kp5YPQ0ErVlCINnx/3z4o5jkRigi/GB
+MPRFfG0jg8CQADcaK5+nG7uhFtmG/uPoB1xoM96Jmqa7J7+uKZE2CthkOoOyYn/f
+SWjpetoV8sJBcUYyWA9fQSRbeblJ28jbbVryUg6bVQ560V/Jm6Caisx8rTTd91nS
+YAFibbyZMndIhJ9UYBdFIo1SqS5SIiizU9SpeLWCa/n+GdxTueOpO4Qz0oE+694T
+VGrEQxDouQtHvcLFGkDLGz1K9i2ILBIeK/AfZ2lowp9pmny1x/9TjmjtNf62+53I
+tw==
+=tL9t
+-----END PGP MESSAGE-----
diff --git a/src/secrets/rsync.net.txt.gpg b/src/secrets/rsync.net.txt.gpg
new file mode 100644
index 0000000..0352e45
--- /dev/null
+++ b/src/secrets/rsync.net.txt.gpg
@@ -0,0 +1,18 @@
+-----BEGIN PGP MESSAGE-----
+
+hQIMAxJMwbOrBV1aARAAznUOLmXnP72G64k+Gz8UmpAdQVTn7dmUqaLQAm3G2ONF
+Gdjw5FaLdNEpwMmBVrQeWHxlyL4TtdTkvhDuk3Y7glX6IyNb2E1mOtODOwyn9YW0
+Pq5dq9Q9jMp3DgrcHNF4XayhUYBOdsgZiFVCZGrAigxWiXiIBh2tT/WF7wYDtUCH
+8gWAp2WqJ2sfh/v2Slv/jWfTNJEQv3t/2xO4KthT/D75Nfr6QnTZBaO28nZURQLe
+eebNC9v1UW6CUV1saWCYEYhE66UAZHzNcYPxZujJS7tjr3Sc5PZKsRs3BBliL0+P
+LWvApBZ6zT/PfqMZJkOXnPCY6kM29Wj8bDD2DE80dZS+MLdPGpxNZlOJ9CBq9l2I
+f12jBBZ0Ncb/TzqMeoyjOxi3BQ4m85s4KcX9v2bmyGxq7dhXVPo7OrBYtzGJDEBU
+7B8I083zYDxhFtW8HpCyRJhAsJeD3I+r+44Lbf05tJglFV96MK78jZeILVvJbK0L
+paNIwoorzUPVbbenFzeOLo0qVaivpMhUPo+0p+oiyv0sK/edO4tI3G4RWUpzGTdF
+ECf87NFZ4F5dtJyLxz6014CrILbVhyqX4Mfxv74nEDijCcEdl2xAGniohLUzoyTH
+2HMwwGouoMne5DxEViY2WFK/pk0Ee1kqUNYcUML82nkOe50ykOmIUrVKW+pzXZ7S
+YAEnGi77S2Ds57UiRcbVqa/TLjuRFbsnqotVMitrXQjDE45eov8erNTVt7TlG0Dk
+X+CnBuqK1qma0XxjZHnzRwrmsP/7fz9W5ZAGwV5U55uC6OUkee6G3BNzZgVhEBAs
+qg==
+=z0m6
+-----END PGP MESSAGE-----