From d69daa37bf2a249ed8d1fa1a0a768f6200e689fe Mon Sep 17 00:00:00 2001 From: Kaushik Narayan R Date: Fri, 16 Jan 2026 00:11:58 -0800 Subject: [PATCH] Initial commit (I feel unsafe) --- .editorconfig | 25 + .gitattributes | 31 + .gitignore | 23 + .shellcheckrc | 1 + README.md | 176 ++++ actual_server-backup | 40 + actual_server-compose_template.yaml | 26 + actual_server-cronjob | 2 + actual_server-setup | 25 + actual_server-teardown | 14 + actual_server-update | 11 + api.spotify-manager.knravish.me.conf | 17 + auth.knravish.me.conf | 21 + .../nginx_snippets/authelia-authrequest.conf | 32 + .../nginx_snippets/authelia-location.conf | 32 + authelia/nginx_snippets/proxy.conf | 37 + authelia/nginx_snippets/websocket.conf | 3 + authelia/secrets/README.md | 6 + authelia_server-backup | 39 + authelia_server-compose_template.yaml | 53 + authelia_server-configuration.yaml | 164 +++ authelia_server-cronjob | 2 + authelia_server-setup | 35 + authelia_server-teardown | 1 + authelia_server-update | 11 + budget.knravish.me.conf | 16 + dash.knravish.me.conf | 18 + file_transfers copy.ps1 | 150 +++ foundry_server-backup | 40 + foundry_server-cronjob | 1 + foundry_server-setup | 37 + foundry_server-start.service | 13 + foundry_server-teardown | 15 + foundry_server-update | 0 freshStart copy.ps1 | 1 + ghost_server-backup | 45 + ghost_server-config.production copy.json | 38 + ghost_server-credentials copy.exp | 14 + ghost_server-cronjob | 1 + ghost_server-setup | 52 + git.knravish.me.conf | 18 + gitea_server-backup | 44 + gitea_server-compose_template.yaml | 40 + gitea_server-cronjob | 1 + gitea_server-setup | 33 + gitea_server-teardown | 0 gitea_server-update | 0 homepage_server-backup | 39 + homepage_server-compose_template.yaml | 32 + homepage_server-cronjob | 2 + homepage_server-geticon | 22 + homepage_server-getimage | 18 + homepage_server-setup | 20 + homepage_server-teardown | 14 + homepage_server-update | 11 + instance-bash_aliases | 6 + instance-bash_autocompletions | 3 + instance-setup | 274 +++++ lnk.knravish.me.conf | 14 + mealie_server-backup | 39 + mealie_server-compose_template.yaml | 27 + mealie_server-cronjob | 2 + mealie_server-setup | 20 + mealie_server-teardown | 14 + mealie_server-update | 11 + memos_server-backup | 39 + memos_server-compose_template.yaml | 15 + memos_server-cronjob | 2 + memos_server-setup | 26 + memos_server-teardown | 14 + memos_server-update | 11 + minecraft_server-backup | 45 + minecraft_server-cronjob | 1 + minecraft_server-setup | 21 + minecraft_server-start.service | 17 + minecraft_server-start.socket | 3 + notes.knravish.me.conf | 18 + paste.knravish.me.conf | 18 + pdf.knravish.me.conf | 18 + planning.knravish.me.conf | 16 + pwpush_server-compose_template.yaml | 35 + pwpush_server-settings.yaml | 981 ++++++++++++++++++ pwpush_server-setup | 14 + pwpush_server-teardown | 14 + recipes.knravish.me.conf | 18 + shlink_server-compose.yaml | 13 + shlink_server-setup | 20 + shlink_server-teardown | 14 + spotmgr_server-backup | 42 + spotmgr_server-compose_template.yaml | 56 + spotmgr_server-cronjob | 1 + spotmgr_server-setup | 22 + stirling_server-compose_template.yaml | 36 + stirling_server-cronjob | 1 + stirling_server-setup | 11 + stirling_server-teardown | 14 + stirling_server-update | 11 + syncthing.knravish.me.conf | 16 + syncthing_server-backup | 39 + syncthing_server-cronjob | 1 + syncthing_server-setup | 18 + ubuntu-cronjob | 2 + ubuntu_auto_apt_upgrade | 26 + vikunja_server-backup | 39 + vikunja_server-compose_template.yaml | 24 + vikunja_server-cronjob | 2 + vikunja_server-setup | 22 + vikunja_server-teardown | 14 + vikunja_server-update | 11 + vpn.knravish.me.conf | 16 + vtt.knravish.me.conf | 16 + wg/Split Tunneling WireGuard.pdf | Bin 0 -> 128703 bytes wg/all_proxied/PostUp copy.ps1 | 26 + wg/connections/README.md | 1 + wg_server-backup | 40 + wg_server-compose_template.yaml | 29 + wg_server-cronjob | 2 + wg_server-setup | 22 + wg_server-teardown | 14 + wg_server-update | 11 + windows copy.md | 223 ++++ 121 files changed, 4153 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .shellcheckrc create mode 100644 README.md create mode 100644 actual_server-backup create mode 100644 actual_server-compose_template.yaml create mode 100644 actual_server-cronjob create mode 100644 actual_server-setup create mode 100644 actual_server-teardown create mode 100644 actual_server-update create mode 100644 api.spotify-manager.knravish.me.conf create mode 100644 auth.knravish.me.conf create mode 100644 authelia/nginx_snippets/authelia-authrequest.conf create mode 100644 authelia/nginx_snippets/authelia-location.conf create mode 100644 authelia/nginx_snippets/proxy.conf create mode 100644 authelia/nginx_snippets/websocket.conf create mode 100644 authelia/secrets/README.md create mode 100644 authelia_server-backup create mode 100644 authelia_server-compose_template.yaml create mode 100644 authelia_server-configuration.yaml create mode 100644 authelia_server-cronjob create mode 100644 authelia_server-setup create mode 100644 authelia_server-teardown create mode 100644 authelia_server-update create mode 100644 budget.knravish.me.conf create mode 100644 dash.knravish.me.conf create mode 100644 file_transfers copy.ps1 create mode 100644 foundry_server-backup create mode 100644 foundry_server-cronjob create mode 100644 foundry_server-setup create mode 100644 foundry_server-start.service create mode 100644 foundry_server-teardown create mode 100644 foundry_server-update create mode 100644 freshStart copy.ps1 create mode 100644 ghost_server-backup create mode 100644 ghost_server-config.production copy.json create mode 100644 ghost_server-credentials copy.exp create mode 100644 ghost_server-cronjob create mode 100644 ghost_server-setup create mode 100644 git.knravish.me.conf create mode 100644 gitea_server-backup create mode 100644 gitea_server-compose_template.yaml create mode 100644 gitea_server-cronjob create mode 100644 gitea_server-setup create mode 100644 gitea_server-teardown create mode 100644 gitea_server-update create mode 100644 homepage_server-backup create mode 100644 homepage_server-compose_template.yaml create mode 100644 homepage_server-cronjob create mode 100644 homepage_server-geticon create mode 100644 homepage_server-getimage create mode 100644 homepage_server-setup create mode 100644 homepage_server-teardown create mode 100644 homepage_server-update create mode 100644 instance-bash_aliases create mode 100644 instance-bash_autocompletions create mode 100644 instance-setup create mode 100644 lnk.knravish.me.conf create mode 100644 mealie_server-backup create mode 100644 mealie_server-compose_template.yaml create mode 100644 mealie_server-cronjob create mode 100644 mealie_server-setup create mode 100644 mealie_server-teardown create mode 100644 mealie_server-update create mode 100644 memos_server-backup create mode 100644 memos_server-compose_template.yaml create mode 100644 memos_server-cronjob create mode 100644 memos_server-setup create mode 100644 memos_server-teardown create mode 100644 memos_server-update create mode 100644 minecraft_server-backup create mode 100644 minecraft_server-cronjob create mode 100644 minecraft_server-setup create mode 100644 minecraft_server-start.service create mode 100644 minecraft_server-start.socket create mode 100644 notes.knravish.me.conf create mode 100644 paste.knravish.me.conf create mode 100644 pdf.knravish.me.conf create mode 100644 planning.knravish.me.conf create mode 100644 pwpush_server-compose_template.yaml create mode 100644 pwpush_server-settings.yaml create mode 100644 pwpush_server-setup create mode 100644 pwpush_server-teardown create mode 100644 recipes.knravish.me.conf create mode 100644 shlink_server-compose.yaml create mode 100644 shlink_server-setup create mode 100644 shlink_server-teardown create mode 100644 spotmgr_server-backup create mode 100644 spotmgr_server-compose_template.yaml create mode 100644 spotmgr_server-cronjob create mode 100644 spotmgr_server-setup create mode 100644 stirling_server-compose_template.yaml create mode 100644 stirling_server-cronjob create mode 100644 stirling_server-setup create mode 100644 stirling_server-teardown create mode 100644 stirling_server-update create mode 100644 syncthing.knravish.me.conf create mode 100644 syncthing_server-backup create mode 100644 syncthing_server-cronjob create mode 100644 syncthing_server-setup create mode 100644 ubuntu-cronjob create mode 100644 ubuntu_auto_apt_upgrade create mode 100644 vikunja_server-backup create mode 100644 vikunja_server-compose_template.yaml create mode 100644 vikunja_server-cronjob create mode 100644 vikunja_server-setup create mode 100644 vikunja_server-teardown create mode 100644 vikunja_server-update create mode 100644 vpn.knravish.me.conf create mode 100644 vtt.knravish.me.conf create mode 100644 wg/Split Tunneling WireGuard.pdf create mode 100644 wg/all_proxied/PostUp copy.ps1 create mode 100644 wg/connections/README.md create mode 100644 wg_server-backup create mode 100644 wg_server-compose_template.yaml create mode 100644 wg_server-cronjob create mode 100644 wg_server-setup create mode 100644 wg_server-teardown create mode 100644 wg_server-update create mode 100644 windows copy.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f158368 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,25 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# https://editorconfig.org + +# top-most EditorConfig file +root = true + +[*] +end_of_line = lf +insert_final_newline = true + +charset = utf-8 + +indent_style = space +indent_size = 2 + +trim_trailing_whitespace = true +max_line_length = 80 + +[*.txt] +indent_style = tab +indent_size = 4 + +[*.{diff,md}] +trim_trailing_whitespace = false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..71afda6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,31 @@ +########################################################################## + +# The .gitattributes file tells git how to handle line endings, +# how to recognize and diff different file types, specifies merge +# strategies, content filters(?) to run on commit/checkout, etc. + +# See more: +# https://git-scm.com/book/en/v2/Customizing-Git-Git-Attributes + +########################################################################## + +# Set the default behavior, in case people don't have core.autocrlf set. +* text=auto eol=lf + +# Scripts +*.sh text eol=lf +*.ps1 text eol=crlf + +# Docs +*.md text diff=markdown +*.txt text + +# Config +.editorconfig text +*.env text +.gitattributes text +*.yml text +*.yaml text + +# Ignore files +*.*ignore text diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ad4348e --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +*env +rclone.conf + +# SSH key pair +*.key +*.pub + +authelia/secrets/* +!authelia/secrets/README.md + +wg/connections/*.conf +wg/connections/README.md + +wg/all_proxied/*.ps1 +!wg/all_proxied/*copy.ps1 + +ghost_server-credentials.exp +ghost_server-config.production.json + +# lel +file_transfers.ps1 +freshStart.ps1 +windows.md diff --git a/.shellcheckrc b/.shellcheckrc new file mode 100644 index 0000000..256d0e6 --- /dev/null +++ b/.shellcheckrc @@ -0,0 +1 @@ +external-sources=true \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..519ca9c --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# The Playground + +--- + +## Blueprint usage + +**_take a deep breath, clear your mind, and open Spotify_** + +--- + +### Pre-requisite #0: A fresh start + +#### Fill `*-env` for all (fml) + +```bash +######################## +# instance-env + +export BACKUP_BUCKET='b2:the-bucket-name-goes-here' # Backblaze +export NOTIF_URL='https://ntfy.sh/the-topic-name-goes-here' # ntfy +export BASE_DOMAIN='knravish.me' + +######################## +# Some common stuff + +export BUCKET_PATH="${BACKUP_BUCKET}/path/to/backup/in/object/storage" + +export VOLUME_PATH="${HOME}/${USER}-data" # or wherever your service's data is + +export PORT= # your service's webserver + +# PUID and PGID for running containers as non-root +PUID=$(id -u "$USER") +export PUID +PGID=$(id -g "$USER") +export PGID + +######################## +# Application specific stuff +# Just look it up bruh I can't be arsed + +######################## +# ok but WireGuard is a PITA +export UDP_PORT= +export GUI_PORT= + +# shellcheck disable=SC2016 +export PASSWORD_HASH= +export WG_HOST="vpn.${BASE_DOMAIN}" + +# hacky? +WG_DEVICE=$(ip route get 8.8.8.8 2>&1 | awk '{ printf "%s",$5;}') +export WG_DEVICE +``` + +### Then + +- run `file_transfers.ps1` (assuming Windows host for now) +- update DNS records as needed + +### Pre-requisite #1: Ports in VPC/VCN firewall rules + +- allow all ICMP traffic for pinging (already open?) +- list of active listeners + - `*` - equivalent to `0.0.0.0, [::]` + - `%lo` - loopback + - `enp0s6` - name of the Internet-facing gateway interface of the host + - `Forwarded` - if port is open in the host's firewall (for VPS? if open in virtual network security rules) + +| Address | Port | Protocol | Desc. | Forwarded? | +| --------------- | ----- | -------- | ------------------------------------------ | ---------- | +| | | ICMP | All ICMP traffic | O | +| \* | 22 | TCP | SSH | O | +| 127.0.0.53%lo | 53 | TCP, UDP | systemd-resolved (stub? vestigial?) | X | +| 10.0.0.3%enp0s6 | 68 | UDP | DHCP | X | +| \* | 80 | TCP | Nginx (HTTP) | O | +| \* | 443 | TCP | Nginx (HTTPS) | O | +| 127.0.0.1 | 2368 | TCP | Ghost blog | X | +| 127.0.0.1 | 3456 | TCP | Vikunja | X | +| 127.0.0.1 | 5006 | TCP | Actual Budget | X | +| 127.0.0.1 | 5100 | TCP | Password Pusher (pwpush) | X | +| 127.0.0.1 | 8080 | TCP | Shlink | X | +| 127.0.0.1 | 9001 | TCP | Spotify Manager (that's us!) | X | +| 127.0.0.1 | 8081 | TCP | Stirling-PDF | X | +| 127.0.0.1 | 9091 | TCP | Authelia | X | +| 127.0.0.1 | 8384 | TCP | Syncthing (web GUI) | X | +| \* | 21027 | UDP | Syncthing (discovery broadcasting) | O | +| \* | 22000 | TCP, UDP | Syncthing (sync protocol; UDP is for QUIC) | O | +| \* | 25565 | TCP | Minecraft server - Java edition, 1.20.4 | O | +| \* | 51820 | UDP | WireGuard (VPN tunnel) | O | +| 127.0.0.1 | 51821 | TCP | WireGuard (web GUI) | X | +| 127.0.0.1 | 5230 | TCP | Usememos | X | +| 127.0.0.1 | 3000 | TCP | Homepage | X | +| \* | 30000 | TCP | Foundry VTT | X | +| 127.0.0.1 | 3001 | TCP | Gitea | X | + +### Pre-requisite #2: Config the master script + +- start with `instance-setup` + - reevaluate sudo perms... have given too much stuff too much permissions :\) + - ensure ufw is disabled + - (Oracle VPS only) open iptables to all (`-I INPUT -j ACCEPT` or something) + - maybe use new pro token + - check email address too + +--- + +## Applications + +### Authelia + +### Actual + +- PWA on mobile! + +### Ghost + +- requires checks for ghost, cli, and node version updates + +### Minecraft + +- version-locked 1.20.4 +- backup of everything, including JAR file + +### Password Pusher + +### Shlink + +- managed on [shlink.io webapp](https://app.shlink.io) +- API key for GUI management, else run command in container + +### Spotify Manager + +- yippee! +- be conservative with dep. updates + +### Stirling-PDF + +- guest creds are `'guest':'temppass3'` + +### Wireguard/wg-easy + +- access VPS services on its `10.0.0.3/24` address + +## Tooling and config + +### bash + +- place new aliases in `/etc/skel` file as well +- do not place non-sensitive stuff in `/etc/environment` +- `cp -pr` for recursive copying and without changing mode or access time + +### nginx + +- current practice - place configs in `conf.d`, change extension to not end in `.conf` for disabled sites + - old practice - `sites-enabled` soft links to `sites-available` files as needed +- serving some temporary files to share from /var/www/tmpfiles +- the build with added modules is fked up, ignore + +### rclone + +- config is for Backblaze B2, 10GB total +- always log!!! and notify!!! + +### systemd + +- `WantedBy` should be + - `default.target` for user services + - `multi-user.target` for system services + +### cron + +- cron doesn't get the same env as a normal login/shell, so give it a minimal set of vars +- set `USER` at the start of every user crontab +- set `XDG_RUNTIME_DIR` and `DBUS_SESSION_BUS_ADDRESS` for users that run systemd user services +- stagger cronjobs to avoid resource contention diff --git a/actual_server-backup b/actual_server-backup new file mode 100644 index 0000000..d6b1d93 --- /dev/null +++ b/actual_server-backup @@ -0,0 +1,40 @@ +#!/bin/bash + +# shellcheck source=actual_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] actual backup\n" + + mkdir -p /tmp/"${USER}"-backup/{user,server}-files + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + cp -pr "${VOLUME_PATH}"/* /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v + if [ $? -ne 0 ]; then + curl -Ss \ + -H "Title: Actual Server" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Actual Server" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/actual_server-compose_template.yaml b/actual_server-compose_template.yaml new file mode 100644 index 0000000..c05d363 --- /dev/null +++ b/actual_server-compose_template.yaml @@ -0,0 +1,26 @@ +--- +services: + actual: + image: ghcr.io/actualbudget/actual + container_name: actual + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:5006 + deploy: + resources: + limits: + memory: 2048M + volumes: + - type: bind + source: ${VOLUME_PATH} + target: /data + bind: + create_host_path: true + user: ${PUID}:${PGID} + healthcheck: + test: ['CMD-SHELL', 'node src/scripts/health-check.js'] + interval: 60s + timeout: 10s + retries: 3 + start_period: 20s diff --git a/actual_server-cronjob b/actual_server-cronjob new file mode 100644 index 0000000..58bd1a6 --- /dev/null +++ b/actual_server-cronjob @@ -0,0 +1,2 @@ +0 10 * * * /home/actual_server/actual_server-backup +0 11 * * 2 /home/actual_server/actual_server-update diff --git a/actual_server-setup b/actual_server-setup new file mode 100644 index 0000000..e4e8cae --- /dev/null +++ b/actual_server-setup @@ -0,0 +1,25 @@ +#!/bin/bash + +# shellcheck source=actual_server-env +. "${HOME}"/"${USER}"-env + +echo -e "\n[+] setting up actual\n\n-------\n" + +mkdir -p "${VOLUME_PATH}" + +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[/] waiting for migrations to run..." +sleep 10 # wait for migrations to run + +echo "[+] restoring backup data" + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + +rm -r "${VOLUME_PATH:?}"/* + +rclone copy "${BUCKET_PATH}" "${VOLUME_PATH}" -v + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/actual_server-teardown b/actual_server-teardown new file mode 100644 index 0000000..e8728b0 --- /dev/null +++ b/actual_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=actual_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/actual_server-update b/actual_server-update new file mode 100644 index 0000000..7a669db --- /dev/null +++ b/actual_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating actual\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/api.spotify-manager.knravish.me.conf b/api.spotify-manager.knravish.me.conf new file mode 100644 index 0000000..bb2e162 --- /dev/null +++ b/api.spotify-manager.knravish.me.conf @@ -0,0 +1,17 @@ +server { + server_name api.spotify-manager.knravish.me; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:9001; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/auth.knravish.me.conf b/auth.knravish.me.conf new file mode 100644 index 0000000..34d47b3 --- /dev/null +++ b/auth.knravish.me.conf @@ -0,0 +1,21 @@ +server { + server_name auth.knravish.me; + index index.html index.htm; + + set $upstream http://127.0.0.1:9091; + + location / { + include /etc/nginx/snippets/proxy.conf; + proxy_pass $upstream; + } + + location = /api/verify { + proxy_pass $upstream; + } + + location /api/authz/ { + proxy_pass $upstream; + } + + listen 80; +} diff --git a/authelia/nginx_snippets/authelia-authrequest.conf b/authelia/nginx_snippets/authelia-authrequest.conf new file mode 100644 index 0000000..8f76f76 --- /dev/null +++ b/authelia/nginx_snippets/authelia-authrequest.conf @@ -0,0 +1,32 @@ +## Send a subrequest to Authelia to verify if the user is authenticated and has permission to access the resource. +auth_request /internal/authelia/authz; + +## Save the upstream metadata response headers from Authelia to variables. +auth_request_set $user $upstream_http_remote_user; +auth_request_set $groups $upstream_http_remote_groups; +auth_request_set $name $upstream_http_remote_name; +auth_request_set $email $upstream_http_remote_email; + +## Inject the metadata response headers from the variables into the request made to the backend. +proxy_set_header Remote-User $user; +proxy_set_header Remote-Groups $groups; +proxy_set_header Remote-Email $email; +proxy_set_header Remote-Name $name; + +## Configure the redirection when the authz failure occurs. Lines starting with 'Modern Method' and 'Legacy Method' +## should be commented / uncommented as pairs. The modern method uses the session cookies configuration's authelia_url +## value to determine the redirection URL here. It's much simpler and compatible with the mutli-cookie domain easily. + +## Modern Method: Set the $redirection_url to the Location header of the response to the Authz endpoint. +auth_request_set $redirection_url $upstream_http_location; + +## Modern Method: When there is a 401 response code from the authz endpoint redirect to the $redirection_url. +error_page 401 =302 $redirection_url; + +## Legacy Method: Set $target_url to the original requested URL. +## This requires http_set_misc module, replace 'set_escape_uri' with 'set' if you don't have this module. +# set_escape_uri $target_url $scheme://$http_host$request_uri; + +## Legacy Method: When there is a 401 response code from the authz endpoint redirect to the portal with the 'rd' +## URL parameter set to $target_url. This requires users update 'auth.knravish.me/' with their external authelia URL. +# error_page 401 =302 https://auth.knravish.me/?rd=$target_url; \ No newline at end of file diff --git a/authelia/nginx_snippets/authelia-location.conf b/authelia/nginx_snippets/authelia-location.conf new file mode 100644 index 0000000..b38faf3 --- /dev/null +++ b/authelia/nginx_snippets/authelia-location.conf @@ -0,0 +1,32 @@ +set $upstream_authelia http://127.0.0.1:9091/api/authz/auth-request; + +## Virtual endpoint created by nginx to forward auth requests. +location /internal/authelia/authz { + ## Essential Proxy Configuration + internal; + proxy_pass $upstream_authelia; + + ## Headers + ## The headers starting with X-* are required. + proxy_set_header X-Original-Method $request_method; + proxy_set_header X-Original-URL $scheme://$http_host$request_uri; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header Content-Length ""; + proxy_set_header Connection ""; + + ## Basic Proxy Configuration + proxy_pass_request_body off; + proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; # Timeout if the real server is dead + proxy_redirect http:// $scheme://; + proxy_http_version 1.1; + proxy_cache_bypass $cookie_session; + proxy_no_cache $cookie_session; + proxy_buffers 4 32k; + client_body_buffer_size 128k; + + ## Advanced Proxy Configuration + send_timeout 5m; + proxy_read_timeout 240; + proxy_send_timeout 240; + proxy_connect_timeout 240; +} \ No newline at end of file diff --git a/authelia/nginx_snippets/proxy.conf b/authelia/nginx_snippets/proxy.conf new file mode 100644 index 0000000..a2cd50d --- /dev/null +++ b/authelia/nginx_snippets/proxy.conf @@ -0,0 +1,37 @@ +## The only custom header to be added for our uses +proxy_set_header Access-Control-Allow-Origin *; + +## Headers +proxy_set_header Host $host; +proxy_set_header X-Original-URL $scheme://$http_host$request_uri; +proxy_set_header X-Forwarded-Proto $scheme; +proxy_set_header X-Forwarded-Host $http_host; +proxy_set_header X-Forwarded-URI $request_uri; +proxy_set_header X-Forwarded-Ssl on; +proxy_set_header X-Forwarded-For $remote_addr; +proxy_set_header X-Real-IP $remote_addr; + +## Basic Proxy Configuration +client_body_buffer_size 128k; +proxy_next_upstream error timeout invalid_header http_500 http_502 http_503; ## Timeout if the real server is dead. +proxy_redirect http:// $scheme://; +proxy_http_version 1.1; +proxy_cache_bypass $cookie_session; +proxy_no_cache $cookie_session; +proxy_buffers 64 256k; + +## Trusted Proxies Configuration +## Please read the following documentation before configuring this: +## https://www.authelia.com/integration/proxies/nginx/#trusted-proxies +# set_real_ip_from 10.0.0.0/8; +# set_real_ip_from 172.16.0.0/12; +# set_real_ip_from 192.168.0.0/16; +# set_real_ip_from fc00::/7; +real_ip_header X-Forwarded-For; +real_ip_recursive on; + +## Advanced Proxy Configuration +send_timeout 5m; +proxy_read_timeout 360; +proxy_send_timeout 360; +proxy_connect_timeout 360; \ No newline at end of file diff --git a/authelia/nginx_snippets/websocket.conf b/authelia/nginx_snippets/websocket.conf new file mode 100644 index 0000000..656426f --- /dev/null +++ b/authelia/nginx_snippets/websocket.conf @@ -0,0 +1,3 @@ +## WebSocket Example +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection "upgrade"; \ No newline at end of file diff --git a/authelia/secrets/README.md b/authelia/secrets/README.md new file mode 100644 index 0000000..12358e1 --- /dev/null +++ b/authelia/secrets/README.md @@ -0,0 +1,6 @@ +# Secrets + +- enc_key +- jwt_sec +- ses_sec +- smtp_pass diff --git a/authelia_server-backup b/authelia_server-backup new file mode 100644 index 0000000..46b0dae --- /dev/null +++ b/authelia_server-backup @@ -0,0 +1,39 @@ +#!/bin/bash + +# shellcheck source=authelia_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] authelia backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + cp -pr "${VOLUME_PATH}"/* /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Authelia" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Authelia" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/authelia_server-compose_template.yaml b/authelia_server-compose_template.yaml new file mode 100644 index 0000000..ad27773 --- /dev/null +++ b/authelia_server-compose_template.yaml @@ -0,0 +1,53 @@ +--- +secrets: + JWT_SECRET: + file: '${SECRETS_PATH}/jwt_sec' + SESSION_SECRET: + file: '${SECRETS_PATH}/ses_sec' + STORAGE_ENCRYPTION_KEY: + file: '${SECRETS_PATH}/enc_key' + SMTP_PASSWORD: + file: '${SECRETS_PATH}/smtp_pass' + +services: + redis: + container_name: 'authelia-redis' + image: redis:alpine + command: redis-server --save 60 1 --loglevel warning + pull_policy: always + restart: unless-stopped + networks: + authelia_server_network: + aliases: [] + volumes: + - ${REDIS_PATH}:/data + user: ${PUID}:${PGID} + authelia: + container_name: 'authelia' + image: authelia/authelia + pull_policy: always + restart: unless-stopped + networks: + authelia_server_network: + aliases: [] + ports: + - '127.0.0.1:9091:9091' + secrets: + - 'JWT_SECRET' + - 'SESSION_SECRET' + - 'STORAGE_ENCRYPTION_KEY' + - 'SMTP_PASSWORD' + environment: + AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: '/run/secrets/JWT_SECRET' + AUTHELIA_SESSION_SECRET_FILE: '/run/secrets/SESSION_SECRET' + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/run/secrets/STORAGE_ENCRYPTION_KEY' + AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE: '/run/secrets/SMTP_PASSWORD' + PUID: ${PUID} + PGID: ${PGID} + volumes: + - ${VOLUME_PATH}:/config + +networks: + authelia_server_network: + external: true + name: 'authelia_server_network' diff --git a/authelia_server-configuration.yaml b/authelia_server-configuration.yaml new file mode 100644 index 0000000..ae1ee1f --- /dev/null +++ b/authelia_server-configuration.yaml @@ -0,0 +1,164 @@ +authentication_backend: + file: + path: /config/users.yaml + watch: true + +access_control: + default_policy: deny + networks: + - name: 'internal' + networks: + - '10.0.0.0/8' + - '172.16.0.0/12' + - '192.168.0.0/18' + rules: + # go from most to least specific + ###### bypasses ###### + # CORS preflight + - domain: '*.knravish.me' + methods: 'OPTIONS' + policy: 'bypass' + ### status endpoints ### + # https://auth.knravish.me/api/health - status + - domain: 'auth.knravish.me' + resources: '^\/api\/health$' + policy: 'bypass' + # https://budget.knravish.me/info - info + - domain: 'budget.knravish.me' + resources: '^\/info$' + policy: 'bypass' + # https://blog.knravish.me/ghost/api/admin/site - info + - domain: 'blog.knravish.me' + resources: '^\/ghost\/api\/admin\/site$' + policy: 'bypass' + # # https://git.knravish.me/api/healthz - health + # - domain: 'git.knravish.me' + # resources: '^\/api\/healthz$' + # policy: 'bypass' + # https://notes.knravish.me/api/v1/workspace/profile - info + - domain: 'notes.knravish.me' + resources: '^\/api\/v1\/workspace\/profile$' + policy: 'bypass' + # https://pdf.knravish.me/api/v1/info/status - status + - domain: 'pdf.knravish.me' + resources: '^\/api\/v1\/info\/status$' + policy: 'bypass' + # https://planning.knravish.me/manifest.webmanifest - PWA + # for the homepage widget + # https://planning.knravish.me/api/v1/projects + # https://planning.knravish.me/api/v1/tasks/all?filter=done%3Dfalse&sort_by=due_date + - domain: 'planning.knravish.me' + resources: + - '^\/manifest.webmanifest$' + - '^\/api\/v1\/projects$' + - '^\/api\/v1\/tasks\/all\?filter=done%3Dfalse&sort_by=due_date$' + policy: 'bypass' + # https://recipes.knravish.me/api/app/about - status + - domain: 'recipes.knravish.me' + resources: + - '^\/api\/households\/statistics$' # homepage widget + - '^\/api\/app\/about$' + policy: 'bypass' + # https://syncthing.knravish.me/rest/noauth/health + - domain: 'syncthing.knravish.me' + resources: '^\/rest\/noauth\/health$' + policy: 'bypass' + # https://vpn.knravish.me/api/release - status + - domain: 'vpn.knravish.me' + resources: + - '^\/api\/wireguard\/client$' # homepage widget + - '^\/api\/release$' + policy: 'bypass' + # https://vtt.knravish.me/api/status + - domain: 'vtt.knravish.me' + resources: '^\/api\/status$' + policy: 'bypass' + ###### 1FA ###### + # sensitive data - only self + - domain: + - 'budget.knravish.me' + subject: + - 'user:self' + policy: 'one_factor' + # sensitive admin - only self + - domain: + - 'vpn.knravish.me' + - 'syncthing.knravish.me' + subject: + - 'user:self' + policy: 'one_factor' + # ghost blog admin + - domain: 'blog.knravish.me' + resources: '^\/ghost([\/?].*)?$' + subject: + - 'group:admin' + policy: 'one_factor' + # foundry VTT + - domain: 'vtt.knravish.me' + subject: + - 'group:admin' + - 'group:foundry' + policy: 'one_factor' + # mealie recipes + - domain: 'recipes.knravish.me' + subject: + - 'group:admin' + - 'group:mealie' + policy: 'one_factor' + ###### 2FA ###### + # master bypass - super_admin (currently only self) + - domain: '*.knravish.me' + subject: + - 'group:super_admin' + policy: 'two_factor' + +password_policy: + zxcvbn: + enabled: true + +# SECRET +# identity_validation: +# reset_password: +# jwt_secret: '' + +session: + # SECRET + # secret: '' + redis: + host: 'authelia-redis' + inactivity: '1w' + expiration: '2w' + remember_me: '3M' + cookies: + - domain: 'knravish.me' + authelia_url: 'https://auth.knravish.me' + +storage: + # SECRET + # encryption_key: '' + local: + path: '/config/db.sqlite3' + +notifier: + smtp: + address: 'smtp://smtp.purelymail.com:587' + timeout: '15s' + username: 'noreply@knravish.me' + # SECRET + # password: '' + sender: 'Authelia ' + identifier: 'knravish.me' + subject: '[Authelia] {title}' + +theme: 'auto' + +server: + endpoints: + authz: + auth-request: + implementation: 'AuthRequest' + authn_strategies: + - name: 'HeaderAuthorization' + schemes: + - 'Basic' + - name: 'CookieSession' diff --git a/authelia_server-cronjob b/authelia_server-cronjob new file mode 100644 index 0000000..9328bf5 --- /dev/null +++ b/authelia_server-cronjob @@ -0,0 +1,2 @@ +1 10 * * * /home/authelia_server/authelia_server-backup +1 11 * * 2 /home/authelia_server/authelia_server-update diff --git a/authelia_server-setup b/authelia_server-setup new file mode 100644 index 0000000..6ebaf01 --- /dev/null +++ b/authelia_server-setup @@ -0,0 +1,35 @@ +#!/bin/bash + +# shellcheck source=authelia_server-env +. "${HOME}"/"${USER}"-env + +echo -e "\n[+] setting up authelia\n\n-------\n" + +echo -e "\n[!] not really automated cuz of the nginx and secrets stuff\n" + +mkdir -p "${REDIS_PATH}" +mkdir -p "${VOLUME_PATH}" +mkdir -p "${SECRETS_PATH}" + +chown -R "${USER}":"${USER}" "${SECRETS_PATH}" +chmod -R 600 "${SECRETS_PATH}" + +echo -e "\n[Q] STOP! scp the secrets if you haven't\n" +sleep 5 +echo -e "\n[!] hope you've copied the secrets MANUALLY\n" + +cp "${HOME}"/"${USER}"-configuration.yaml "${VOLUME_PATH}"/configuration.yml + +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker network create -d bridge "${USER}"_network +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] restoring from backup..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + +rm -rf "${VOLUME_PATH}" +rclone copy "${BUCKET_PATH}" "${VOLUME_PATH}" -v + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/authelia_server-teardown b/authelia_server-teardown new file mode 100644 index 0000000..fea2473 --- /dev/null +++ b/authelia_server-teardown @@ -0,0 +1 @@ +um not so simple, need to edit the nginx configs \ No newline at end of file diff --git a/authelia_server-update b/authelia_server-update new file mode 100644 index 0000000..437a3fd --- /dev/null +++ b/authelia_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating authelia\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/budget.knravish.me.conf b/budget.knravish.me.conf new file mode 100644 index 0000000..62384b7 --- /dev/null +++ b/budget.knravish.me.conf @@ -0,0 +1,16 @@ +server { + server_name budget.knravish.me; + index index.html index.htm; + + include /etc/nginx/snippets/authelia-location.conf; + + set $upstream http://127.0.0.1:5006; + + location / { + include /etc/nginx/snippets/proxy.conf; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass $upstream; + } + + listen 80; +} diff --git a/dash.knravish.me.conf b/dash.knravish.me.conf new file mode 100644 index 0000000..52e289a --- /dev/null +++ b/dash.knravish.me.conf @@ -0,0 +1,18 @@ +server { + server_name dash.knravish.me; + index index.html index.htm; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:3000; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/file_transfers copy.ps1 b/file_transfers copy.ps1 new file mode 100644 index 0000000..47b4de1 --- /dev/null +++ b/file_transfers copy.ps1 @@ -0,0 +1,150 @@ +$dirname = $PSScriptRoot +$remote_home_folder = "your_default_user@1.2.3.4" +$key = "your.private.key" + +function TransferFile { + param ( + [Parameter(Mandatory)] + [string]$FileName, + [ValidateNotNullOrEmpty()] + [string]$DestPath = "" + ) + + scp -i "${dirname}\${key}" "${dirname}\${FileName}" "${remote_home_folder}:${DestPath}" +} + +# backups +TransferFile "actual_server-backup" +TransferFile "authelia_server-backup" +TransferFile "foundry_server-backup" +TransferFile "ghost_server-credentials.exp" +TransferFile "ghost_server-backup" +TransferFile "homepage_server-backup" +TransferFile "mealie_server-backup" +TransferFile "memos_server-backup" +TransferFile "minecraft_server-backup" +# TransferFile "stirling_server-backup" +TransferFile "syncthing_server-backup" +TransferFile "wg_server-backup" + +# updates +TransferFile "actual_server-update" +TransferFile "authelia_server-update" +TransferFile "foundry_server-update" +# TransferFile "ghost_server-update" +TransferFile "homepage_server-update" +TransferFile "mealie_server-update" +TransferFile "memos_server-update" +# TransferFile "minecraft_server-update" +TransferFile "stirling_server-update" +# TransferFile "syncthing_server-update" +TransferFile "wg_server-update" + +# cronjobs +TransferFile "ubuntu-cronjob" +TransferFile "actual_server-cronjob" +TransferFile "authelia_server-cronjob" +TransferFile "foundry_server-cronjob" +TransferFile "ghost_server-cronjob" +TransferFile "homepage_server-cronjob" +TransferFile "mealie_server-cronjob" +TransferFile "memos_server-cronjob" +TransferFile "minecraft_server-cronjob" +TransferFile "syncthing_server-cronjob" +TransferFile "wg_server-cronjob" + +# env vars +TransferFile "instance-env" +TransferFile "actual_server-env" +TransferFile "authelia_server-env" +TransferFile "foundry_server-env" +TransferFile "ghost_server-env" +TransferFile "homepage_server-env" +TransferFile "mealie_server-env" +TransferFile "memos_server-env" +TransferFile "minecraft_server-env" +TransferFile "pwpush_server-env" +# TransferFile "shlink_server-env" +TransferFile "stirling_server-env" +TransferFile "syncthing_server-env" +TransferFile "wg_server-env" + +# config files +## misc. +TransferFile "rclone.conf" ".config/rclone" +TransferFile "authelia_server-configuration.yaml" +TransferFile "ghost_server-config.production.json" +TransferFile "pwpush_server-settings.yaml" +### systemd +TransferFile "minecraft_server-start.service" +TransferFile "minecraft_server-start.socket" +## nginx configs +### authelia nginx snippets +TransferFile "authelia\nginx_snippets\authelia-authrequest.conf" +TransferFile "authelia\nginx_snippets\authelia-location.conf" +TransferFile "authelia\nginx_snippets\proxy.conf" +TransferFile "authelia\nginx_snippets\websocket.conf" +### sites +TransferFile "auth.knravish.me.conf" +TransferFile "budget.knravish.me.conf" +TransferFile "dash.knravish.me.conf" +TransferFile "lnk.knravish.me.conf" +TransferFile "notes.knravish.me.conf" +TransferFile "paste.knravish.me.conf" +TransferFile "pdf.knravish.me.conf" +TransferFile "recipes.knravish.me.conf" +TransferFile "syncthing.knravish.me.conf" +TransferFile "vpn.knravish.me.conf" +TransferFile "vtt.knravish.me.conf" + +# docker-compose files + +TransferFile "actual_server-compose_template.yaml" +TransferFile "authelia_server-compose_template.yaml" +TransferFile "homepage_server-compose_template.yaml" +TransferFile "mealie_server-compose_template.yaml" +TransferFile "memos_server-compose_template.yaml" +TransferFile "pwpush_server-compose_template.yaml" +TransferFile "shlink_server-compose.yaml" # TransferFile "shlink_server-compose_template.yaml" +TransferFile "stirling_server-compose_template.yaml" +TransferFile "wg_server-compose_template.yaml" + +# setup scripts +TransferFile "instance-setup" # run as ubuntu +TransferFile "actual_server-setup" +TransferFile "authelia_server-setup" +TransferFile "foundry_server-setup" +TransferFile "ghost_server-setup" +TransferFile "homepage_server-setup" +TransferFile "mealie_server-setup" +TransferFile "memos_server-setup" +TransferFile "minecraft_server-setup" +TransferFile "pwpush_server-setup" +TransferFile "shlink_server-setup" +TransferFile "stirling_server-setup" +TransferFile "syncthing_server-setup" +TransferFile "wg_server-setup" + +# teardown scripts - run as ubuntu +TransferFile "actual_server-teardown" +TransferFile "authelia_server-teardown" +TransferFile "foundry_server-teardown" +# TransferFile "ghost_server-teardown" +TransferFile "homepage_server-teardown" +TransferFile "mealie_server-teardown" +TransferFile "memos_server-teardown" +# TransferFile "minecraft_server-teardown" +TransferFile "pwpush_server-teardown" +TransferFile "shlink_server-teardown" +TransferFile "stirling_server-teardown" +# TransferFile "syncthing_server-teardown" +TransferFile "wg_server-teardown" + +# secrets +TransferFile "authelia\secrets\enc_key" "authelia_secrets" +TransferFile "authelia\secrets\jwt_sec" "authelia_secrets" +TransferFile "authelia\secrets\ses_sec" "authelia_secrets" +TransferFile "authelia\secrets\smtp_pass" "authelia_secrets" + +# miscellaneous +TransferFile "ubuntu_auto_apt_upgrade" diff --git a/foundry_server-backup b/foundry_server-backup new file mode 100644 index 0000000..6f342fa --- /dev/null +++ b/foundry_server-backup @@ -0,0 +1,40 @@ +#!/bin/bash + +# shellcheck source=foundry_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] foundry backup\n" + + mkdir -p /tmp/"${USER}"-backup + + systemctl --user stop "${USER}"-start.service + + cp -pr "${FOUNDRY_DATA_PATH}"/* /tmp/"${USER}"-backup + + systemctl --user restart "${USER}"-start.service + + rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v + if [ $? -ne 0 ]; then + curl -Ss \ + -H "Title: Foundry VTT" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Foundry VTT" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/foundry_server-cronjob b/foundry_server-cronjob new file mode 100644 index 0000000..32ed55f --- /dev/null +++ b/foundry_server-cronjob @@ -0,0 +1 @@ +2 10,22 * * * /home/foundry_server/foundry_server-backup diff --git a/foundry_server-setup b/foundry_server-setup new file mode 100644 index 0000000..9d7fb5c --- /dev/null +++ b/foundry_server-setup @@ -0,0 +1,37 @@ +#!/bin/bash + +# shellcheck source=foundry_server-env +. "${HOME}"/"${USER}"-env + +echo -e "\n[+] setting up foundry\n\n-------\n" + +echo "[+] nvm and node" + +curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash +export NVM_DIR="$HOME/.nvm" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm +[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion + +nvm install --lts +nvm alias default node + +echo "[+] foundry" + +mkdir -p "${HOME}"/foundry +mkdir -p "${FOUNDRY_DATA_PATH}" +cd foundry || exit +wget -O foundryvtt.zip "${FOUNDRY_TIMED_URL}" +unzip foundryvtt.zip +rm foundryvtt.zip + +echo "[+] restoring backup data" + +rclone copy "${BUCKET_PATH}" "${FOUNDRY_DATA_PATH}" -v + +echo "[+] setting up systemctl and starting" + +mkdir -p "${HOME}"/.config/systemd/user/ +cp "${HOME}"/"${USER}"-start.service "${HOME}"/.config/systemd/user/ + +systemctl --user daemon-reload +systemctl --user enable --now "${USER}"-start.service diff --git a/foundry_server-start.service b/foundry_server-start.service new file mode 100644 index 0000000..4398dda --- /dev/null +++ b/foundry_server-start.service @@ -0,0 +1,13 @@ +[Unit] +Description=Foundry VTT +After=network.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=1 +WorkingDirectory=%h/foundry +ExecStart=/bin/bash -c ". ${HOME}/.nvm/nvm.sh ; node resources/app/main.js" + +[Install] +WantedBy=default.target diff --git a/foundry_server-teardown b/foundry_server-teardown new file mode 100644 index 0000000..9fb2020 --- /dev/null +++ b/foundry_server-teardown @@ -0,0 +1,15 @@ +#!/bin/bash + +username=foundry_server + +# application +sudo machinectl shell ${username}@ /bin/bash -c "systemctl --user disable --now ${username}-start.service ; systemctl --user daemon-reload" +sudo machinectl shell ${username}@ /bin/bash -c ". ~/.nvm/nvm.sh ; nvm deactivate ; nvm uninstall --lts" + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/foundry_server-update b/foundry_server-update new file mode 100644 index 0000000..e69de29 diff --git a/freshStart copy.ps1 b/freshStart copy.ps1 new file mode 100644 index 0000000..c83d1ed --- /dev/null +++ b/freshStart copy.ps1 @@ -0,0 +1 @@ +ssh -i $PSScriptRoot/your.private.key your_default_user@1.2.3.4 diff --git a/ghost_server-backup b/ghost_server-backup new file mode 100644 index 0000000..f0c308c --- /dev/null +++ b/ghost_server-backup @@ -0,0 +1,45 @@ +#!/bin/bash + +# shellcheck source=ghost_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log +{ + echo -e "\n[+] ghost backup\n" + + cd "${BLOG_PATH}" || exit + + if ! /usr/bin/expect "${HOME}"/"${USER}"-credentials.exp; then + curl -Ss \ + -H "Title: Ghost Blog" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed - ghost backup failure" \ + "${NOTIF_URL}" + rm -r "${BLOG_PATH}"/backup* + exit 1 + fi + + echo "[+] local backup taken" + + if ! rclone copyto "${BLOG_PATH}"/backup*.zip "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Ghost Blog" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed - rclone failure" \ + "${NOTIF_URL}" + rm -r "${BLOG_PATH}"/backup* + exit 1 + fi + + curl -Ss \ + -H "Title: Ghost Blog" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r "${BLOG_PATH}"/backup* + +} &>>"$logFile" diff --git a/ghost_server-config.production copy.json b/ghost_server-config.production copy.json new file mode 100644 index 0000000..c87ae06 --- /dev/null +++ b/ghost_server-config.production copy.json @@ -0,0 +1,38 @@ +{ + "url": "https://blog.knravish.me", + "server": { + "port": 2368, + "host": "127.0.0.1" + }, + "database": { + "client": "mysql", + "connection": { + "host": "postgres_hostname", + "user": "postgres_username", + "password": "postgres_password", + "database": "defaultdb", + "port": , + "ssl": { + "ca": "", + "rejectUnauthorized": true + } + } + }, + "mail": { + "transport": "Direct" + }, + "logging": { + "transports": [ + "file", + "stdout" + ] + }, + "process": "systemd", + "paths": { + "contentPath": "/var/www/blog.knravish.me/content" + }, + "bootstrap-socket": { + "port": 8000, + "host": "localhost" + } +} diff --git a/ghost_server-credentials copy.exp b/ghost_server-credentials copy.exp new file mode 100644 index 0000000..4fcf71c --- /dev/null +++ b/ghost_server-credentials copy.exp @@ -0,0 +1,14 @@ +#!/usr/bin/expect + +set email "" +set pw "" + +spawn ghost backup + +expect "Ghost administrator email address" +send "$email\r" + +expect "Ghost administrator password" +send "$pw\r" + +expect eof diff --git a/ghost_server-cronjob b/ghost_server-cronjob new file mode 100644 index 0000000..5fb9c40 --- /dev/null +++ b/ghost_server-cronjob @@ -0,0 +1 @@ +3 10 * * * /home/ghost_server/ghost_server-backup diff --git a/ghost_server-setup b/ghost_server-setup new file mode 100644 index 0000000..dd2767c --- /dev/null +++ b/ghost_server-setup @@ -0,0 +1,52 @@ +#!/bin/bash + +# shellcheck source=ghost_server-env +. "${HOME}"/"${USER}"-env + +email_address=hello@knravish.me +echo -e "\n[+] setting up ghost\n\n-------\n" + +echo "[+] node and companions" +# ghost doesn't play well with nvm for some reason, probably because of installation location and sudo access +# Download and import the Nodesource GPG key +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +NODE_MAJOR=20 # Use a supported version +echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list +sudo apt-get update +sudo apt-get install nodejs -y + +echo "[+] getting ready..." + +ghost_cli_ver="1.26.0" +sudo npm i -g ghost-cli@${ghost_cli_ver} + +sudo mkdir -p "${BLOG_PATH}" +sudo chown "${USER}":"${USER}" "${BLOG_PATH}" +sudo chmod 775 "${BLOG_PATH}" + +# ghost really needs to update to newer nginx versions and conventions... +sudo mkdir -p /etc/nginx/sites-available/ /etc/nginx/sites-enabled/ /etc/nginx/snippets/ + +echo "[+] ooh, interactive stuff" + +# currently track manually, maybe automate +ghost_ver="5.105.0" + +cd "${BLOG_PATH}" && ghost install ${ghost_ver} --no-setup + +sudo cp "${HOME}"/"${USER}"-config.production.json "${BLOG_PATH}"/ +sudo chown "${USER}":"${USER}" "${BLOG_PATH}"/"${USER}"-config.production.json +mv "${BLOG_PATH}"/"${USER}"-config.production.json "${BLOG_PATH}"/config.production.json + +cd "${BLOG_PATH}" && ghost setup --auto --sslemail ${email_address} + +echo "[+] restoring backup data" + +sudo rm -r "${BLOG_PATH}"/content/* + +rclone copyto "${BUCKET_PATH}" "${BLOG_PATH}"/ghostBackup.zip +sudo unzip "${BLOG_PATH}"/ghostBackup.zip -d "${BLOG_PATH}"/content/ +sudo chown -R ghost:ghost "${BLOG_PATH}"/content/ + +echo -e "\n-----\nIMPORTANT\n-----\n[X] modify the nginx default config file to include the sites-enabled directory\n" diff --git a/git.knravish.me.conf b/git.knravish.me.conf new file mode 100644 index 0000000..2f7c350 --- /dev/null +++ b/git.knravish.me.conf @@ -0,0 +1,18 @@ +server { + server_name git.knravish.me; + index index.html index.htm; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:3001; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/gitea_server-backup b/gitea_server-backup new file mode 100644 index 0000000..121ae45 --- /dev/null +++ b/gitea_server-backup @@ -0,0 +1,44 @@ +#!/bin/bash + +# shellcheck source=gitea_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] gitea backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop gitea + + cp -pr "${VOLUME_PATH}"/config /tmp/"${USER}"-backup + cp -pr "${VOLUME_PATH}"/data /tmp/"${USER}"-backup + + # shellcheck disable=SC2024 + sudo docker exec -u "${PUID}:${PGID}" -it gitea-postgres sh -c \ + 'pg_dumpall -c --if-exists -U gitea' >/tmp/"${USER}"-backup/db.out + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start gitea + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Gitea" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -rf /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Gitea" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -rf /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/gitea_server-compose_template.yaml b/gitea_server-compose_template.yaml new file mode 100644 index 0000000..185ebaa --- /dev/null +++ b/gitea_server-compose_template.yaml @@ -0,0 +1,40 @@ +--- +services: + gitea: + image: docker.gitea.com/gitea:1-rootless + container_name: gitea + pull_policy: always + restart: unless-stopped + volumes: + - ${VOLUME_PATH}/data:/var/lib/gitea + - ${VOLUME_PATH}/config:/etc/gitea + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - 127.0.0.1:${PORT}:3000 + # - 2222:2222 # for internal SSH. unnecessary? + environment: + - GITEA__database__DB_TYPE=postgres + - GITEA__database__HOST=db:5432 + - GITEA__database__NAME=gitea + - GITEA__database__USER=gitea + - GITEA__database__PASSWD=gitea + - USER=git + - USER_UID=${PUID} + - USER_GID=${PGID} + depends_on: + - db + user: ${PUID}:${PGID} + + db: + image: postgres:16 + container_name: gitea-postgres + pull_policy: always + restart: unless-stopped + environment: + - POSTGRES_USER=gitea + - POSTGRES_PASSWORD=gitea + - POSTGRES_DB=gitea + volumes: + - ${VOLUME_PATH}/postgres:/var/lib/postgresql/data + user: ${PUID}:${PGID} diff --git a/gitea_server-cronjob b/gitea_server-cronjob new file mode 100644 index 0000000..911e99b --- /dev/null +++ b/gitea_server-cronjob @@ -0,0 +1 @@ +4 10 * * * /home/gitea_server/gitea_server-backup diff --git a/gitea_server-setup b/gitea_server-setup new file mode 100644 index 0000000..e8270b7 --- /dev/null +++ b/gitea_server-setup @@ -0,0 +1,33 @@ +#!/bin/bash + +echo -e "\n[+] setting up gitea\n\n-------\n" + +# shellcheck source=gitea_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${VOLUME_PATH}"/{data,config,postgres} +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] preparing to restore from backup..." + +sleep 5 +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop gitea + +mkdir -p "${VOLUME_PATH}"/restore_files +rclone copy "${BUCKET_PATH}" "${VOLUME_PATH}"/restore_files + +echo "[+] restoring from backup..." + +cp -fr "${VOLUME_PATH}"/restore_files/config "${VOLUME_PATH}" +cp -fr "${VOLUME_PATH}"/restore_files/data "${VOLUME_PATH}" +chown -R "${PUID}":"${PGID}" "${VOLUME_PATH}"/config "${VOLUME_PATH}"/data + +cat "${VOLUME_PATH}"/restore_files/db.out | sudo docker exec -i gitea-postgres psql -qXU gitea + +echo "[+] restarting..." +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml restart + +# cleanup +rm -rf "${VOLUME_PATH}"/restore_files diff --git a/gitea_server-teardown b/gitea_server-teardown new file mode 100644 index 0000000..e69de29 diff --git a/gitea_server-update b/gitea_server-update new file mode 100644 index 0000000..e69de29 diff --git a/homepage_server-backup b/homepage_server-backup new file mode 100644 index 0000000..bfc5fdf --- /dev/null +++ b/homepage_server-backup @@ -0,0 +1,39 @@ +#!/bin/bash + +# shellcheck source=homepage_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] homepage backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + cp -pr "${VOLUME_PATH}"/* /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Homepage" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Homepage" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/homepage_server-compose_template.yaml b/homepage_server-compose_template.yaml new file mode 100644 index 0000000..bf231b9 --- /dev/null +++ b/homepage_server-compose_template.yaml @@ -0,0 +1,32 @@ +--- +services: + dockerproxy: + image: ghcr.io/tecnativa/docker-socket-proxy:latest + container_name: dockerproxy + environment: + - CONTAINERS=1 # Allow access to viewing containers + - SERVICES=0 # Allow access to viewing services (necessary when using Docker Swarm) + - TASKS=0 # Allow access to viewing tasks (necessary when using Docker Swarm) + - POST=0 # Disallow any POST operations (effectively read-only) + ports: + - 127.0.0.1:${DOCKER_PORT}:${DOCKER_PORT} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro # Mounted as read-only + restart: unless-stopped + + homepage: + image: ghcr.io/gethomepage/homepage:latest + container_name: homepage + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:${PORT} + init: true + volumes: + - ${VOLUME_PATH}/config:/app/config # Make sure your local config directory exists + - ${VOLUME_PATH}/icons:/app/public/icons # icons + - ${VOLUME_PATH}/images:/app/public/images # images + environment: + PUID: ${PUID} + PGID: ${PGID} + HOMEPAGE_ALLOWED_HOSTS: dash.knravish.me diff --git a/homepage_server-cronjob b/homepage_server-cronjob new file mode 100644 index 0000000..0e105ff --- /dev/null +++ b/homepage_server-cronjob @@ -0,0 +1,2 @@ +5 10 * * * /home/homepage_server/homepage_server-backup +5 11 * * 2 /home/homepage_server/homepage_server-update diff --git a/homepage_server-geticon b/homepage_server-geticon new file mode 100644 index 0000000..8ced154 --- /dev/null +++ b/homepage_server-geticon @@ -0,0 +1,22 @@ +#!/bin/bash + +# shellcheck source=homepage_server-env +. "${HOME}"/"${USER}"-env + +base_url=https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons + +svg_url=${base_url}/svg/${1}.svg +png_url=${base_url}/png/${1}.png + +if ! curl -I "${svg_url}" | grep -E "HTTP/.* 404" >/dev/null; then + curl -Ss -O --output-dir "${VOLUME_PATH}"/icons "${svg_url}" + echo "svg" + exit 0 +elif ! curl -I "${png_url}" | grep -E "HTTP/.* 404" >/dev/null; then + curl -Ss -O --output-dir "${VOLUME_PATH}"/icons "${png_url}" + echo "png" + exit 0 +else + echo "Not Found" + exit 1 +fi diff --git a/homepage_server-getimage b/homepage_server-getimage new file mode 100644 index 0000000..bff1d26 --- /dev/null +++ b/homepage_server-getimage @@ -0,0 +1,18 @@ +#!/bin/bash + +# shellcheck source=homepage_server-env +. "${HOME}"/"${USER}"-env + +headers=$(curl -SsIXGET "$1") + +status_code=$(echo "$headers" | grep -E "HTTP/.* [0-9]{3}" | awk '{print $2}') + +if [[ $status_code == "200" ]]; then + ext=$(echo "$headers" | grep "content-type:" | awk -F/ '{print $2}' | tr -d " \t\n\r") + curl -Ss -o "${VOLUME_PATH}"/images/"${2}"."${ext}" "${1}" + echo "found" + exit 0 +else + echo "Not Found" + exit 1 +fi diff --git a/homepage_server-setup b/homepage_server-setup new file mode 100644 index 0000000..7f2b3bb --- /dev/null +++ b/homepage_server-setup @@ -0,0 +1,20 @@ +#!/bin/bash + +echo -e "\n[+] setting up homepage\n\n-------\n" + +# shellcheck source=homepage_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${VOLUME_PATH}" +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] restoring from backup..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + +rm -rf "${VOLUME_PATH}" +rclone copy "${BUCKET_PATH}" "${VOLUME_PATH}" -v + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/homepage_server-teardown b/homepage_server-teardown new file mode 100644 index 0000000..4b0eed0 --- /dev/null +++ b/homepage_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=homepage_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/homepage_server-update b/homepage_server-update new file mode 100644 index 0000000..2fea949 --- /dev/null +++ b/homepage_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating homepage\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/instance-bash_aliases b/instance-bash_aliases new file mode 100644 index 0000000..6181da9 --- /dev/null +++ b/instance-bash_aliases @@ -0,0 +1,6 @@ +#!/bin/bash + +alias less="less -r" +alias dsizes="sudo du --max-depth=1 -h" +alias workas="sudo machinectl shell" +alias psdeets="ps -o pid,vsz=MEMORY -o user,group=GROUP -o comm,args=ARGS -p" diff --git a/instance-bash_autocompletions b/instance-bash_autocompletions new file mode 100644 index 0000000..4b1f41e --- /dev/null +++ b/instance-bash_autocompletions @@ -0,0 +1,3 @@ +#!/bin/bash + +complete -W "$(compgen -u)" workas diff --git a/instance-setup b/instance-setup new file mode 100644 index 0000000..0ab7551 --- /dev/null +++ b/instance-setup @@ -0,0 +1,274 @@ +#!/bin/bash + +echo -e "\n[+] Let's begin!\n\n-------\n" + +# define these first +[[ -z "$BASE_DOMAIN" ]] && echo "base domain missing" && exit 1 +[[ -z "$CF_EMAIL_ALIAS" ]] && echo "domain email missing" && exit 1 +[[ -z "$UBUNTU_PRO_TOKEN" ]] && echo "ubuntu pro token missing" && exit 1 +[[ -z "$B2_COLON_BUCKET_NAME" ]] && echo "b2 bucket name missing" && exit 1 +[[ -z "$NTFY_URL" ]] && echo "ntfy endpoint missing" && exit 1 + +domain=$BASE_DOMAIN +email_address=${CF_EMAIL_ALIAS} + +echo "BASE_DOMAIN=${BASE_DOMAIN}" | sudo tee -a /etc/environment +echo "BACKUP_BUCKET=${B2_COLON_BUCKET_NAME}" | sudo tee -a /etc/environment # current: the startingOut one +echo "NOTIF_URL=${NTFY_URL}" | sudo tee -a /etc/environment # current: endpoint on ntfy.sh + +# some useful aliases +cat instance-bash_aliases | tee -a ~/.bash_aliases +cat instance-bash_aliases | sudo tee -a /etc/skel/.bash_aliases + +# some useful autocompletions +chmod 774 instance-bash_autocompletions +./instance-bash_autocompletions + +cd ~ || exit +sudo apt-get update +sudo apt-get upgrade -y +sudo pro attach "$UBUNTU_PRO_TOKEN" + +if [[ $(cloud-init query platform) == 'oracle' ]]; then + # https://www.reddit.com/r/oraclecloud/comments/r8lkf7/a_quick_tips_to_people_who_are_having_issue/ + echo "[+] disabling ufw and netfilter rules (OCI default)" + sudo ufw disable + sudo iptables -I INPUT -j ACCEPT + sudo iptables-save | sudo dd of=/etc/iptables/rules.v4 +fi + +echo "[+] packages" +# JDK 17 or higher needed for MC +sudo apt-get install build-essential curl gnupg2 ca-certificates lsb-release ubuntu-keyring apt-transport-https expect -y +sudo apt-get install openjdk-21-jdk-headless systemd-container fail2ban -y +sudo systemctl enable --now fail2ban.service + +echo "[+] docker" +sudo install -m 0775 -d /etc/apt/keyrings +sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc +sudo chmod a+r /etc/apt/keyrings/docker.asc +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/ubuntu $(lsb_release -cs 2>/dev/null) stable" | + sudo tee /etc/apt/sources.list.d/docker.list >/dev/null + +echo "[+] nginx" +# http://nginx.org/en/linux_packages.html#Ubuntu +curl -L https://nginx.org/keys/nginx_signing.key | gpg --dearmor | + sudo tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null +expected_nginx_fingerprint='573BFD6B3D8FBC641079A6ABABF5BD827BD9BF62' +if ! gpg --dry-run --quiet --no-keyring --import --import-options \ + import-show /usr/share/keyrings/nginx-archive-keyring.gpg | + grep -c $expected_nginx_fingerprint; then + echo -e "\n[!] Nginx GPG key fingerprint does not match, aborting...\n" + sudo rm /usr/share/keyrings/nginx-archive-keyring.gpg + exit 1 +fi +echo "deb [signed-by=/usr/share/keyrings/nginx-archive-keyring.gpg] \ + http://nginx.org/packages/ubuntu $(lsb_release -cs 2>/dev/null) nginx" | + sudo tee /etc/apt/sources.list.d/nginx.list +echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" | + sudo tee /etc/apt/preferences.d/99nginx + +echo "[+] syncthing" +sudo curl -L -o /etc/apt/keyrings/syncthing-archive-keyring.gpg https://syncthing.net/release-key.gpg +echo "deb [signed-by=/etc/apt/keyrings/syncthing-archive-keyring.gpg]\ + https://apt.syncthing.net/ syncthing stable-v2" | + sudo tee /etc/apt/sources.list.d/syncthing.list +echo -e "Package: *\nPin: origin apt.syncthing.net\nPin-Priority: 990\n" | + sudo tee /etc/apt/preferences.d/syncthing.pref + +echo "[+] putting it all together" +sudo apt-get update +sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin nginx syncthing -y +if ! sudo docker run hello-world | grep -c 'installation appears to be working correctly'; then + echo -e "\n[!] Docker installation failed, aborting...\n" + exit 1 +fi + +echo "[+] rclone" + +curl https://rclone.org/install.sh | sudo bash + +echo "[+] certbot from snap ugh" + +sudo snap install core +sudo snap refresh core +sudo apt-get remove certbot +sudo snap install --classic certbot +sudo ln -s /snap/bin/certbot /usr/bin/certbot + +echo "[+] add users for applications" +# format - tool name underscore 'server' +users=( + "actual_server" + "authelia_server" + "foundry_server" + "ghost_server" + "gitea_server" + "homepage_server" + "mealie_server" + "memos_server" + "minecraft_server" + "pwpush_server" + "shlink_server" + "spotmgr_server" + "stirling_server" + "syncthing_server" + "vikunja_server" + "wg_server" +) +for username in "${users[@]}"; do + sudo useradd -m -U -s /bin/bash "${username}" + + # setup script + sudo cp ~/"${username}"-setup /home/"${username}"/ + sudo chmod 774 /home/"${username}"/"${username}"-setup + sudo chown "${username}":"${username}" /home/"${username}"/"${username}"-setup + sudo cp ~/"${username}"-env /home/"${username}"/ + sudo chmod 600 /home/"${username}"/"${username}"-env + sudo chown "${username}":"${username}" /home/"${username}"/"${username}"-env + + # user services won't linger by default + sudo loginctl enable-linger "${username}" +done + +# admin privileges, needed for anyone running docker +admin_users=( + "actual_server" + "authelia_server" + "ghost_server" + "gitea_server" + "homepage_server" + "mealie_server" + "memos_server" + "pwpush_server" + "shlink_server" + "spotmgr_server" + "stirling_server" + "vikunja_server" + "wg_server" +) +for username in "${admin_users[@]}"; do + sudo usermod -aG sudo "${username}" + echo "${username} ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/"${username}" + + # compose files + sudo cp ~/"${username}"-compose_template.yaml /home/"${username}"/ + sudo chmod 664 /home/"${username}"/"${username}"-compose_template.yaml + sudo chown "${username}":"${username}" /home/"${username}"/"${username}"-compose_template.yaml + sudo cp ~/"${username}"-compose.yaml /home/"${username}"/ + sudo chmod 600 /home/"${username}"/"${username}"-compose.yaml + sudo chown "${username}":"${username}" /home/"${username}"/"${username}"-compose.yaml +done + +echo "[+] distribute and apply respective config files" + +echo -e "\t[-] rclone" + +for username in "${users[@]}"; do + sudo mkdir -p /home/"${username}"/.config/rclone/ + sudo cp ~/.config/rclone/rclone.conf /home/"${username}"/.config/rclone/ + sudo chmod -R 600 /home/"${username}"/.config/rclone/rclone.conf + sudo chown -R "${username}":"${username}" /home/"${username}"/ +done + +# consider switching to acme.sh instead of certbot to avoid snap +echo -e "\t[-] nginx and certbot" + +cert_subdomains=( + "api.spotify-manager" + "auth" + "budget" + "dash" + "git" + "lnk" + "notes" + "paste" + "planning" + "pdf" + "recipes" + "syncthing" + "vpn" + "vtt" +) +# ghost handles SSL by itself, might be worth looking into it to either shift to certbot +for subdomain in "${cert_subdomains[@]}"; do + # revoke existing certs if any + sudo certbot revoke -n --delete-after-revoke --cert-name "${subdomain}"."${domain}" + sudo cp ~/"${subdomain}"."${domain}".conf /etc/nginx/conf.d/ + sudo chmod 664 /etc/nginx/conf.d/"${subdomain}"."${domain}".conf + sudo chown root:root /etc/nginx/conf.d/"${subdomain}"."${domain}".conf + if ! sudo nginx -t; then + echo -e "\n\t[!] Bad Nginx config for ${subdomain}.${domain}, aborting...\n" + exit 1 + fi + sudo nginx -s reload + + # ---------------------------------------------------------------------- + # STOP! + # Check DNS records before proceeding + # ---------------------------------------------------------------------- + + # https://letsencrypt.org/docs/duplicate-certificate-limit/#description + # certbot has 5 per week duplicate cert limit. use --test-cert flag for testing + if ! sudo certbot -n --nginx --agree-tos -m "${email_address}" -d "${subdomain}"."${domain}"; then + echo -e "\n\t[!] Certbot failed to get cert for ${subdomain}.${domain}, aborting...\n" + exit 1 + fi + sudo nginx -s reload +done + +echo -e "\t[-] user-specific files" + +# bash variable expansion ftw - https://stackoverflow.com/a/63821858/7630441 +user_files=( + "authelia_server-configuration.yaml" + "foundry_server-start.service" + "ghost_server-config.production.json" + "ghost_server-credentials.exp" + "minecraft_server-start.service" + "minecraft_server-start.socket" + "pwpush_server-settings.yaml" +) + +for f in "${user_files[@]}"; do + username=${f%%-*} # strips the part from before the hyphen + sudo cp ~/"${f}" /home/"${username}"/ + sudo chmod 664 /home/"${username}"/"${f}" + sudo chown "${username}":"${username}" /home/"${username}"/"${f}" +done + +echo -e "[+] cronjobs: backups, updates" + +for username in "${users[@]}"; do + sudo cp ~/"${username}"-backup /home/"${username}"/ + sudo chmod 774 /home/"${username}"/"${username}"-backup + sudo chown "${username}":"${username}" /home/"${username}"/"${username}"-backup + sudo cp ~/"${username}"-update /home/"${username}"/ + sudo chmod 774 /home/"${username}"/"${username}"-update + sudo chown "${username}":"${username}" /home/"${username}"/"${username}"-update + + { + # first add some useful env vars that aren't in cron's exec env + echo "USER=$username" + echo "XDG_RUNTIME_DIR=/run/user/$(id -u "$username")" + echo "DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$username")/bus" + # then the defined cronjob + cat ~/"${username}"-cronjob + } >~/"${username}".cronjobs + + # install to crontab + sudo crontab -u "${username}" ~/"${username}".cronjobs + rm ~/"${username}".cronjobs +done + +# shellcheck disable=SC2024 +sudo crontab -l -u ubuntu >~/ubuntu.cronjobs +cat ~/ubuntu-cronjob >>~/ubuntu.cronjobs +sudo crontab -u ubuntu ~/ubuntu.cronjobs +rm ~/ubuntu.cronjobs + +for username in "${users[@]}"; do + chmod ug+x "${username}"-teardown +done diff --git a/lnk.knravish.me.conf b/lnk.knravish.me.conf new file mode 100644 index 0000000..8c1229f --- /dev/null +++ b/lnk.knravish.me.conf @@ -0,0 +1,14 @@ +server { + server_name lnk.knravish.me; + charset utf-8; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:8080; + } + + listen 80; +} diff --git a/mealie_server-backup b/mealie_server-backup new file mode 100644 index 0000000..7c01eaf --- /dev/null +++ b/mealie_server-backup @@ -0,0 +1,39 @@ +#!/bin/bash + +# shellcheck source=mealie_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] mealie backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + cp -pr "${VOLUME_PATH}"/* /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Mealie" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Mealie" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/mealie_server-compose_template.yaml b/mealie_server-compose_template.yaml new file mode 100644 index 0000000..820562e --- /dev/null +++ b/mealie_server-compose_template.yaml @@ -0,0 +1,27 @@ +--- +services: + mealie: + image: ghcr.io/mealie-recipes/mealie + container_name: mealie + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:9000 + deploy: + resources: + limits: + memory: 2048M + volumes: + - type: bind + source: ${VOLUME_PATH} + target: /app/data + bind: + create_host_path: true + environment: + ALLOW_SIGNUP: false + PUID: ${PUID} + PGID: ${PGID} + TZ: America/Phoenix + MAX_WORKERS: 1 + WEB_CONCURRENCY: 1 + BASE_URL: ${BASE_URL} diff --git a/mealie_server-cronjob b/mealie_server-cronjob new file mode 100644 index 0000000..b932d08 --- /dev/null +++ b/mealie_server-cronjob @@ -0,0 +1,2 @@ +6 10 * * * /home/mealie_server/mealie_server-backup +6 11 * * 2 /home/mealie_server/mealie_server-update diff --git a/mealie_server-setup b/mealie_server-setup new file mode 100644 index 0000000..2f3635b --- /dev/null +++ b/mealie_server-setup @@ -0,0 +1,20 @@ +#!/bin/bash + +# shellcheck source=mealie_server-env +. "${HOME}"/"${USER}"-env + +echo -e "\n[+] setting up mealie\n\n-------\n" + +envsubst < "${HOME}"/"${USER}"-compose_template.yaml > "${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] restoring backup data" + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + +rm -r "${VOLUME_PATH:?}"/* + +rclone copy "${BUCKET_PATH}" "${VOLUME_PATH}" -v + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/mealie_server-teardown b/mealie_server-teardown new file mode 100644 index 0000000..f8a3311 --- /dev/null +++ b/mealie_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=mealie_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/mealie_server-update b/mealie_server-update new file mode 100644 index 0000000..ae6f081 --- /dev/null +++ b/mealie_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating mealie\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/memos_server-backup b/memos_server-backup new file mode 100644 index 0000000..8a381e3 --- /dev/null +++ b/memos_server-backup @@ -0,0 +1,39 @@ +#!/bin/bash + +# shellcheck source=memos_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] memos backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + cp -pr "${VOLUME_PATH}"/* /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Memos" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Memos" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/memos_server-compose_template.yaml b/memos_server-compose_template.yaml new file mode 100644 index 0000000..1456dad --- /dev/null +++ b/memos_server-compose_template.yaml @@ -0,0 +1,15 @@ +--- +services: + memos: + image: neosmemo/memos:stable + container_name: memos + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:${PORT} + volumes: + - type: bind + source: ${VOLUME_PATH} + target: /var/opt/memos + bind: + create_host_path: true diff --git a/memos_server-cronjob b/memos_server-cronjob new file mode 100644 index 0000000..5599c2a --- /dev/null +++ b/memos_server-cronjob @@ -0,0 +1,2 @@ +7 10 * * * /home/memos_server/memos_server-backup +7 11 * * 2 /home/memos_server/memos_server-update diff --git a/memos_server-setup b/memos_server-setup new file mode 100644 index 0000000..6a45cda --- /dev/null +++ b/memos_server-setup @@ -0,0 +1,26 @@ +#!/bin/bash + +echo -e "\n[+] setting up usememos\n\n-------\n" + +# shellcheck source=memos_server-env +. "${HOME}"/"${USER}"-env + +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo -e "\n[+] restoring from backup..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + +sudo rm -rf "${HOME}"/.memos/* + +mkdir memos_data +rclone copy "${BUCKET_PATH}" "${HOME}"/memos_data -v + +sudo cp memos_data/* "${HOME}"/.memos +rm -rf memos_data + +echo "[+] restarting..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/memos_server-teardown b/memos_server-teardown new file mode 100644 index 0000000..42d4f5a --- /dev/null +++ b/memos_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=memos_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/memos_server-update b/memos_server-update new file mode 100644 index 0000000..0641102 --- /dev/null +++ b/memos_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating memos\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/minecraft_server-backup b/minecraft_server-backup new file mode 100644 index 0000000..db1a90c --- /dev/null +++ b/minecraft_server-backup @@ -0,0 +1,45 @@ +#!/bin/bash + +# shellcheck source=minecraft_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] minecraft backup\n" + + mkdir -p /tmp/"${USER}"-backup + + insock=${HOME}/${USER}.stdin + + # https://www.reddit.com/r/admincraft/comments/vgdbi/minecraft_backups_saveoff_and_saveall/ + echo "/save-off" >"${insock}" + echo "/save-all" >"${insock}" + systemctl --user stop "${USER}"-start.{socket,service} + + cp -pr "${DATA_PATH}"/* /tmp/"${USER}"-backup + + systemctl --user restart "${USER}"-start.{socket,service} + echo "/save-on" >"${insock}" + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Minecraft Server" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Minecraft Server" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/minecraft_server-cronjob b/minecraft_server-cronjob new file mode 100644 index 0000000..b387e37 --- /dev/null +++ b/minecraft_server-cronjob @@ -0,0 +1 @@ +8 10 * * * /home/minecraft_server/minecraft_server-backup diff --git a/minecraft_server-setup b/minecraft_server-setup new file mode 100644 index 0000000..bc29727 --- /dev/null +++ b/minecraft_server-setup @@ -0,0 +1,21 @@ +#!/bin/bash + +echo -e "\n[+] setting up the minecraft server\n\n-------\n" + +# shellcheck source=minecraft_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${DATA_PATH}" + +echo "[+] restoring backup data" + +rclone copy "${BUCKET_PATH}" "${DATA_PATH}"/ -v + +echo "[+] setting up systemctl and starting" + +mkdir -p "${HOME}"/.config/systemd/user/ +cp "${HOME}"/"${USER}"-start.{service,socket} "${HOME}"/.config/systemd/user/ + +systemctl --user daemon-reload +systemctl --user restart "${USER}"-start.socket +systemctl --user enable --now "${USER}"-start.service diff --git a/minecraft_server-start.service b/minecraft_server-start.service new file mode 100644 index 0000000..f1bf0a2 --- /dev/null +++ b/minecraft_server-start.service @@ -0,0 +1,17 @@ +[Unit] +Description=Minecraft server +After=network.target + +[Service] +Type=simple +Restart=on-failure +RestartSec=1 +WorkingDirectory=%h/%u +ExecStart=/usr/bin/java -Xms1024M -jar %h/%u/server.jar nogui +Sockets=%u-start.socket +StandardInput=socket +StandardOutput=truncate:%h/%u.run.log +StandardError=append:%h/%u.err.log + +[Install] +WantedBy=default.target diff --git a/minecraft_server-start.socket b/minecraft_server-start.socket new file mode 100644 index 0000000..fdf831b --- /dev/null +++ b/minecraft_server-start.socket @@ -0,0 +1,3 @@ +[Socket] +ListenFIFO=%h/%u.stdin +Service=%u-start.service diff --git a/notes.knravish.me.conf b/notes.knravish.me.conf new file mode 100644 index 0000000..6315c13 --- /dev/null +++ b/notes.knravish.me.conf @@ -0,0 +1,18 @@ +server { + server_name notes.knravish.me; + index index.html index.htm; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:5230; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/paste.knravish.me.conf b/paste.knravish.me.conf new file mode 100644 index 0000000..e897152 --- /dev/null +++ b/paste.knravish.me.conf @@ -0,0 +1,18 @@ +server { + server_name paste.knravish.me; + index index.html index.htm; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:5100; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/pdf.knravish.me.conf b/pdf.knravish.me.conf new file mode 100644 index 0000000..4b6f53b --- /dev/null +++ b/pdf.knravish.me.conf @@ -0,0 +1,18 @@ +server { + server_name pdf.knravish.me; + index index.html index.htm; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:8081; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/planning.knravish.me.conf b/planning.knravish.me.conf new file mode 100644 index 0000000..94ad0d5 --- /dev/null +++ b/planning.knravish.me.conf @@ -0,0 +1,16 @@ +server { + server_name planning.knravish.me; + index index.html index.htm; + + include /etc/nginx/snippets/authelia-location.conf; + + set $upstream http://127.0.0.1:3456; + + location / { + include /etc/nginx/snippets/proxy.conf; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass $upstream; + } + + listen 80; +} \ No newline at end of file diff --git a/pwpush_server-compose_template.yaml b/pwpush_server-compose_template.yaml new file mode 100644 index 0000000..6f0bb65 --- /dev/null +++ b/pwpush_server-compose_template.yaml @@ -0,0 +1,35 @@ +--- +services: + pwpush: + image: pglombardo/pwpush + container_name: pwpush + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:5100 + environment: + PWP__MAIL__SMTP_PASSWORD: ${SMTP_PASSWORD} + DATABASE_URL: 'postgres://postgres:${PG_PASS}@postgres:5432/postgres' + volumes: + - ${VOLUME_PATH}/config/${USER}-settings.yaml:/opt/PasswordPusher/config/settings.yml + - type: bind + source: ${VOLUME_PATH}/files + target: /opt/PasswordPusher/storage + bind: + create_host_path: true + depends_on: + - postgres + postgres: + image: docker.io/postgres:16 + container_name: 'pwpush-postgres' + pull_policy: always + restart: unless-stopped + volumes: + - type: bind + source: ${VOLUME_PATH}/database + target: /var/lib/postgresql/data + bind: + create_host_path: true + environment: + POSTGRES_PASSWORD: ${PG_PASS} + user: ${PUID}:${PGID} diff --git a/pwpush_server-settings.yaml b/pwpush_server-settings.yaml new file mode 100644 index 0000000..4c88ee3 --- /dev/null +++ b/pwpush_server-settings.yaml @@ -0,0 +1,981 @@ +# Global Application Configuration +# +# This file uses YAML syntax. Indentation must be 2 spaces (not tabs). +# +# See also https://docs.pwpush.com/docs/config-strategies/ +# for a further explanation of the larger settings available here. + +### Application Defaults +# + +### URL Pushes +# +# Enable or disable URL based pushes. These allow you to share URLs securely. +# Like regular pushes, they expire after a set time or amount of views. +# +# Note that `enable_logins` is required for URL based pushes to work. It is a +# feature for logged in users only. +# +# Environment variable override: +# PWP__ENABLE_URL_PUSHES='false' +# +enable_url_pushes: true + +### File Uploads +# +# File uploads are disabled by default since they require a place to store +# those files. +# +# If enabling file uploads, make sure to fill out the 'files' section below. +# +# Note that `enable_logins` is required for file uploads to work. It is a +# feature for logged in users only. +# +# Environment variable override: +# PWP__ENABLE_FILE_PUSHES='false' +# +enable_file_pushes: true + +### Logins (User accounts) +# +# Logins are disabled by default since they require an MTA (email) server +# available to send emails through. +# +# If enabling logins, make sure to fill out the 'mail' section below. +# +# For instructions on how to enable logins, see this page: +# https://github.com/pglombardo/PasswordPusher/discussions/276 +# +# Environment variable override: +# PWP__ENABLE_LOGINS='false' +# +enable_logins: true + +## Disable Signups +# +# Disallow new user accounts to be created in the application. +# +# Set this after you have your desired user accounts created. It will +# not allow any further user account creation. +# +# Environment variable override: +# PWP__DISABLE_SIGNUPS='false' +# +disable_signups: false + +## Limit Signups to Specific Email Domains +# +# By default, anyone can sign up for an account. The following default regular +# expression just validates if it is a valid email address. +# +# signup_email_regexp: '\A[^@\s]+@[^@\s]+\z' +# +# If you would like to limit signups to specific email domains, you can extend +# the regular expression below to include the domains you want to allow. +# +# For example, to only allow signups from the domain 'hey.com', you would +# change the following to: +# +# signup_email_regexp: '\A[^@\s]+@(hey\.com)\z' +# +# or for multiple domains: +# +# signup_email_regexp: '\A[^@\s]+@(hey\.com|gmail\.com)\z' +# +# Tip: use https://rubular.com to test out your regular expressions. It includes +# a guide to what each component means in regexp. +# +# Environment variable override: +# PWP__SIGNUP_EMAIL_REGEXP='\A[^@\s]+@[^@\s]+\z' +# +signup_email_regexp: '\A[^@\s]+@[^@\s]+\z' + +### Allow Anonymous +# +# By default, Password Pusher can be used by anonymous users to push +# new passwords and generate secret URLs. If you want to limit functionality +# to logged in users only, set the following value to true. +# +# This does not affect password secret URLs themselves as anonymous is always +# allowed there. +# +# Environment variable override: +# PWP__ALLOW_ANONYMOUS='true' +# +allow_anonymous: true + +### Host Domain +# +# The domain (without protocol) where this instance is hosted +# Used in generating fully qualified URLs. +# +# Make sure to set this for email links to work correctly. +# +# Environment variable override: +# PWP__HOST_DOMAIN='pwpush.com' +# +host_domain: 'paste.knravish.me' + +### Host Protocol +# +# The protocol to reach the domain above +# Used in generating fully qualified URLs. +# +# Make sure to set this for email links to work correctly. +# +# Environment variable override: +# PWP__HOST_PROTOCOL='https' +# +host_protocol: 'https' + +### Base URL Override +# +# Set the following value to force the base URL of generated links. +# +# Environment variable override: +# PWP__OVERRIDE_BASE_URL='https://pwpush.mydomain.com' +# +# You could even add a port if needed: +# PWP__OVERRIDE_BASE_URL='https://pwpush.mydomain.com:5100' +# +# Set this value without a trailing slash ('/'). +# +# override_base_url: 'https://pwpush.mydomain.com' + +### Show version on the footer +# +# Enable/disable PasswordPusher version on the footer. +# +# Environment variable override: +# PWP__SHOW_VERSION=true +# +# Default: true +show_version: true + +### Show the GDPR cookie consent banner +# +# Enable/disable the GDPR cookie consent banner. +# +# Environment variable override: +# PWP__SHOW_GDPR_CONSENT_BANNER=true +# +# Default: true +show_gdpr_consent_banner: true + +### Timezone +# +# Set the timezone for the application. A full list of timezone strings +# can be found here: +# https://en.wikipedia.org/wiki/List_of_tz_database_time_zones +# +# Environment variable override: +# PWP__TIMEZONE='America/New_York' +# +# Default: 'America/New_York' +timezone: 'America/Phoenix' + +### Allowed Hosts +# +# This is a list of allowed hosts for the application. This is used to +# prevent host header attacks. +# +# When set, the application will only respond to requests with a host header +# that matches one of the values in this list. +# +# This feature is generally only used when the application is behind a proxy. +# +# It's generally not required to use this unless you are getting the related error +# in the application. localhost and the IP that the application is running on +# are always allowed. +# +# Note: If you need more than one value to the environment variable, separate +# entries by a single space. +# +# Environment variable override: +# PWP__ALLOWED_HOSTS='pwpush.com pwpush.mydomain.com pwpush.myotherdomain' +# +# allowed_hosts: +# - 'pwpush.com' +# - 'pwpush.mydomain.com' +# - 'pwpush.myotherdomain.com' + +## Expiration Settings for Password Pushes +# +pw: + # Expire Password Pushes After XX Days + # + # Controls the "Expire After Days" for Password Pushes + # + # Environment variable overrides: + # PWP__PW__EXPIRE_AFTER_DAYS_DEFAULT=7 + # PWP__PW__EXPIRE_AFTER_DAYS_MIN=1 + # PWP__PW__EXPIRE_AFTER_DAYS_MAX=90 + # + expire_after_days_default: 7 + expire_after_days_min: 1 + expire_after_days_max: 90 + + # Expire Password Pushes After XX Views + # + # Controls the "Expire After Views" form settings in Password#new + # + # Environment variable overrides: + # PWP__PW__EXPIRE_AFTER_VIEWS_DEFAULT=5 + # PWP__PW__EXPIRE_AFTER_VIEWS_MIN=1 + # PWP__PW__EXPIRE_AFTER_VIEWS_MAX=100 + # + expire_after_views_default: 5 + expire_after_views_min: 1 + expire_after_views_max: 100 + + # Retrieval Step for Password Pushes + # + # This enables or disables the "1-click retrieval step" feature entirely. For the default value + # when it is enabled here, see the next setting. + # + # Environment variable override: + # PWP__PW__ENABLE_RETRIEVAL_STEP='false' + # + enable_retrieval_step: true + + # Default Form Value for the Retrieval Step + # + # When the retrieval step is enabled (above), what is the default value on the form? + # + # When true, secret URLs will be generated as /p/xxxxxxxx/r which will show a page + # requiring a click to view the page /p/xxxxxxxx + # + # Environment variable override: + # PWP__PW__RETRIEVAL_STEP_DEFAULT='true' + # + retrieval_step_default: false + + # Deletable Password Pushes + # + # default: true + # + # This enables or disables the "Allow Immediate Deletion" feature entirely. For the default value + # when it is enabled here, see the next setting. + # + # Environment variable override: + # PWP__PW__ENABLE_DELETABLE_PUSHES='false' + # + enable_deletable_pushes: true + + # Deletable Pushes Default Value + # + # default: true + # + # When this is set to true, this option does two things: + # 1. Sets the default check state for the "Allow viewers to + # optionally delete password before expiration" checkbox + # 2. JSON API: Sets the default value for newly pushed passwords if + # unspecified + # + # Environment variable override: + # PWP__PW__DELETABLE_PUSHES_DEFAULT='false' + # + deletable_pushes_default: true + + # Blur Payloads + # + # default: true + # + # This option does not affect the JSON API - web UI only. + # When this is set to true, this option will display the pushed text payload as + # blurred out text. This is useful for recipients in public places who don't + # want to reveal the sensitive information until when they choose. + # + # The blur is disabled with a single mouse click. + # + # Setting this option to false will disable the blur feature entirely for password pushes. + # + # Note: This is a global on/off switch currently. This may be made configurable per push + # in the future by adding a new checkbox and a `blur_default` setting. + # + # Environment variable override: + # PWP__PW__ENABLE_BLUR='false' + # + enable_blur: true + +## Expiration Settings for URL Pushes +# +url: + # Expire URL Pushes After XX Days + # + # Controls the "Expire After Days" for URL Pushes + # + # Environment variable overrides: + # PWP__URL__EXPIRE_AFTER_DAYS_DEFAULT=7 + # PWP__URL__EXPIRE_AFTER_DAYS_MIN=1 + # PWP__URL__EXPIRE_AFTER_DAYS_MAX=90 + # + expire_after_days_default: 7 + expire_after_days_min: 1 + expire_after_days_max: 90 + + # Expire URL Pushes After XX Views + # + # Controls the "Expire After Views" form settings in Password#new + # + # Environment variable overrides: + # PWP__URL__EXPIRE_AFTER_VIEWS_DEFAULT=5 + # PWP__URL__EXPIRE_AFTER_VIEWS_MIN=1 + # PWP__URL__EXPIRE_AFTER_VIEWS_MAX=100 + # + expire_after_views_default: 5 + expire_after_views_min: 1 + expire_after_views_max: 100 + + # Retrieval Step for URL Pushes + # + # This enables or disables the "1-click retrieval step" feature entirely. For the default value + # when it is enabled here, see the next setting. + # + # Environment variable override: + # PWP__URL__ENABLE_RETRIEVAL_STEP='false' + # + enable_retrieval_step: true + + # Default Form Value for the Retrieval Step + # + # When the retrieval step is enabled (above), what is the default value on the form? + # + # When true, secret URLs will be generated as /r/xxxxxxxx/r which will show a page + # requiring a click to view the page /r/xxxxxxxx + # + # Environment variable override: + # PWP__URL__RETRIEVAL_STEP_DEFAULT='true' + # + retrieval_step_default: false + +### File Upload: Expiration & Storage Settings +# +files: + # Expire File Pushes After XX Days + # + # Controls the "Expire After Days" for File Pushes + # + # Environment variable overrides: + # PWP__FILES__EXPIRE_AFTER_DAYS_DEFAULT=7 + # PWP__FILES__EXPIRE_AFTER_DAYS_MIN=1 + # PWP__FILES__EXPIRE_AFTER_DAYS_MAX=90 + # + expire_after_days_default: 7 + expire_after_days_min: 1 + expire_after_days_max: 90 + + # Expire File Pushes After XX Views + # + # Controls the "Expire After Views" form settings for File Pushes + # + # Environment variable overrides: + # PWP__FILES__EXPIRE_AFTER_VIEWS_DEFAULT=5 + # PWP__FILES__EXPIRE_AFTER_VIEWS_MIN=1 + # PWP__FILES__EXPIRE_AFTER_VIEWS_MAX=100 + # + expire_after_views_default: 5 + expire_after_views_min: 1 + expire_after_views_max: 100 + + # Retrieval Step for File Pushes + # + # This enables or disables the "1-click retrieval step" feature entirely. For the default value + # when it is enabled here, see the next setting. + # + # Environment variable override: + # PWP__FILES__ENABLE_RETRIEVAL_STEP='false' + # + enable_retrieval_step: true + + # Default Form Value for the Retrieval Step + # + # When the retrieval step is enabled (above), what is the default value on the form? + # + # When true, secret URLs will be generated as /f/xxxxxxxx/r which will show a page + # requiring a click to view the page /f/xxxxxxxx + # + # Environment variable override: + # PWP__FILES__RETRIEVAL_STEP_DEFAULT='true' + # + retrieval_step_default: true + + # Deletable File Pushes + # + # default: true + # + # This enables or disables the "Allow Immediate Deletion" feature entirely. For the default value + # when it is enabled here, see the next setting. + # + # Environment variable override: + # PWP__FILES__ENABLE_DELETABLE_PUSHES='false' + # + enable_deletable_pushes: true + + # Deletable File Pushes Default Value + # + # default: true + # + # When this is set to true, this option does two things: + # 1. Sets the default check state for the "Allow viewers to + # optionally delete password before expiration" checkbox + # 2. JSON API: Sets the default value for newly pushed passwords if + # unspecified + # + # Environment variable override: + # PWP__FILES__DELETABLE_PUSHES_DEFAULT='false' + # + deletable_pushes_default: true + + # Blur Payloads + # + # default: true + # + # This option does not affect the JSON API - web UI only. + # When this is set to true, this option will display the pushed text payload as + # blurred out text. This is useful for recipients in public places who don't + # want to reveal the sensitive information until when they choose. + # + # The blur is disabled with a single mouse click. + # + # Setting this option to false will disable the blur feature entirely for file pushes. + # + # Note: This is a global on/off switch currently. This may be made configurable per push + # in the future by adding a new checkbox and a `blur_default` setting. + # + # Environment variable override: + # PWP__FILES__BLUR='false' + # + enable_blur: true + + # Maximum File Upload Count + # + # default: 10 + # + # This option controls the maximum number of files that can be uploaded + # in a single push. + # + # Environment variable override: + # PWP__FILES__MAX_FILE_UPLOADS=10 + # + max_file_uploads: 10 + + # File Storage + # + # Password Pusher can store uploaded files into Amazon S3, Google Cloud Services + # or Microsoft Azure. + # + # Choose your file storage preference by setting the following option to + # one of the following values: + # * local - use local disk (likely won't work in container environments) + # * amazon - use Amazon S3 (and provide 's3' credentials below) + # * google - use Google Cloud Storage (and provide 'gcs' credentials below) + # * microsoft - use Microsoft Azure Storage (and provide 'as' credentials below) + # + # Environment variable override: + # PWP__FILES__STORAGE='local' + # + storage: 'local' + + # Amazon S3 Storage Credentials + s3: + # Environment Variable Override: PWP__FILES__S3__ENDPOINT='' + endpoint: '' + # Environment Variable Override: PWP__FILES__S3__ACCESS_KEY_ID='' + access_key_id: '_' + # Environment Variable Override: PWP__FILES__S3__SECRET_ACCESS_KEY='' + secret_access_key: '' + # Environment Variable Override: PWP__FILES__S3__REGION='' + region: 'us-east-1' + # Environment Variable Override: PWP__FILES__S3__BUCKET='' + bucket: 'pwpush-files' + + # Google Cloud Storage Credentials + gcs: + # Environment Variable Override: PWP__FILES__GCS__PROJECT='' + project: '' + # Environment Variable Override: PWP__FILES__GCS__CREDENTIALS='' + credentials: '' + # Environment Variable Override: PWP__FILES__GCS__BUCKET='' + bucket: '' + # + # Optionally use IAM instead of the credentials when signing URLs. + # This is useful if you are authenticating your GKE applications with Workload Identity, + # See here: https://edgeguides.rubyonrails.org/active_storage_overview.html#google-cloud-storage-service + # + # Environment Variable Override: PWP__FILES__GCS__IAM=true + iam: false + # Environment Variable Override: PWP__FILES__GCS__GSA_EMAIL='email@domain.com' + gsa_email: null + + # Microsoft Azure Storage Credentials + as: + # Environment Variable Override: PWP__FILES__AS__STORAGE_ACCOUNT_NAME='' + storage_account_name: '' + # Environment Variable Override: PWP__FILES__AS__STORAGE_ACCESS_KEY='' + storage_access_key: '' + # Environment Variable Override: PWP__FILES__AS__CONTAINER='' + container: '' + +### Password Generator Defaults +# +# Set the defaults of the front page password generator. +# +gen: + # Whether generated passwords have numbers + # + # Environment variable override: + # PWP__GEN__HAS_NUMBERS='true' + # + has_numbers: true + + # Whether generated passwords will be title cased + # + # Environment variable override: + # PWP__GEN__TITLE_CASED='true' + # + title_cased: true + + # Whether generated passwords will use separators between syllables + # + # Environment variable override: + # PWP__GEN__USE_SEPARATORS='true' + # + use_separators: true + + # List of consonants to generate from + # + # Environment variable override: + # PWP__GEN__CONSONANTS='bcdfghklmnprstvz' + # + consonants: 'bcdfghklmnprstvz' + + # List of vowels to generate from + # + # Environment variable override: + # PWP__GEN__VOWELS='aeiouy' + # + vowels: 'aeiouy' + + # If `use_separators` is enabled above, the list of separators to use (randomly) + # + # Environment variable override: + # PWP__GEN__SEPARATORS='-_=' + # + separators: '-_=' + + # The maximum length of each syllable that a generated password can have + # + # Environment variable override: + # PWP__GEN__MAX_SYLLABLE_LENGTH=3 + # + max_syllable_length: 3 + + # The minimum length of each syllable that a generated password can have + # + # Environment variable override: + # PWP__GEN__MIN_SYLLABLE_LENGTH=1 + # + min_syllable_length: 1 + + # The exact number of syllables that a generated password will have + # + # Environment variable override: + # PWP__GEN__SYLLABLES_COUNT=3 + # + syllables_count: 3 + +brand: + ### Site Title + # + # Environment variable override: PWP__BRAND__TITLE='Acme Corp.' + # + title: 'Password Pusher' + + ### Site Tagline + # + # Environment variable override: PWP__BRAND__TAGLINE='Security First' + # + tagline: 'Go Ahead. Email Another Password.' + + ### Site Disclaimer + # + # Environment variable override: PWP__BRAND__DISCLAIMER='Use at own use risk.' + # disclaimer: 'This is a dummy disclaimer and should not be considered legally binding or taken seriously in any way. The content provided here is for entertainment and illustrative purposes only. Any resemblance to actual disclaimers is purely coincidental. We do not endorse or encourage the use of this disclaimer for any real-world applications, and we strongly advise consulting a legal professional for creating legitimate and appropriate disclaimers for your specific needs. By reading this disclaimer, you agree not to hold us responsible for any confusion, amusement, or bewilderment it may cause. This disclaimer has no legal validity, and any attempt to rely on it for legal, financial, or any other serious matters is ill-advised. Please use disclaimers responsibly and in accordance with applicable laws and regulations.' + + ### Show Footer Menu Toggle + # + # Environment variable override: PWP__BRAND__SHOW_FOOTER_MENU='true' + # + show_footer_menu: true + + ### Site logo + # + # ..for both a light and dark theme + # + # You can also replace these relative paths with fully qualified HTTP links to + # external resources such as Amazon S3 etc. + # e.g. PWP__BRAND__DARK_LOGO='https://mys3bucket.amazonaws.com/a/some-image.png' + # + # Environment variable override: PWP__BRAND__LIGHT_LOGO='https://mys3bucket.amazonaws.com/a/lea+giuliana.png' + # Environment variable override: PWP__BRAND__DARK_LOGO='https://mys3bucket.amazonaws.com/a/lea+giuliana.png' + # + # light_logo: 'logo-transparent-sm-bare.png' + # dark_logo: 'logo-transparent-sm-dark-bare.png' + + ### Favicon & icon images for mobile. When people on mobile (phones/tablets), bookmark + # the site or it is shown in history, these icons are used. + # + # You can also replace these relative paths with fully qualified HTTP links to + # external resources such as Amazon S3 etc. + # e.g. PWP__BRAND__ICON_57x57='https://mys3bucket.amazonaws.com/a/some-image.png' + # + # Although you should set all of the following values, at a bare minimum, make sure + # to set at least icon_57x57 and icon_96x96. Without these two, things are guaranteed + # to not work. + # + # You can use an icon generator such as: + # https://www.favicongenerator.com + # https://www.favicon-generator.org + + # + # Environment variable override: PWP__BRAND__ICON_57x57='/path/to/image' + # icon_57x57: 'apple-icon-57x57.png' + # Environment variable override: PWP__BRAND__ICON_60x60='/path/to/image' + # icon_60x60: 'apple-icon-60x60.png' + # Environment variable override: PWP__BRAND__ICON_72x72='/path/to/image' + # icon_72x72: 'apple-icon-72x72.png' + # Environment variable override: PWP__BRAND__ICON_76x76='/path/to/image' + # icon_76x76: 'apple-icon-76x76.png' + # Environment variable override: PWP__BRAND__ICON_114x114='/path/to/image' + # icon_114x114: 'apple-icon-114x114.png' + # Environment variable override: PWP__BRAND__ICON_120x120='/path/to/image' + # icon_120x120: 'apple-icon-120x120.png' + # Environment variable override: PWP__BRAND__ICON_144x144='/path/to/image' + # icon_144x144: 'apple-icon-144x144.png' + # Environment variable override: PWP__BRAND__ICON_152x152='/path/to/image' + # icon_152x152: 'apple-icon-152x152.png' + # Environment variable override: PWP__BRAND__ICON_180x180='/path/to/image' + # icon_180x180: 'apple-icon-180x180.png' + # Environment variable override: PWP__BRAND__ICON_192x192='/path/to/image' + # icon_192x192: 'android-icon-192x192.png' + # Environment variable override: PWP__BRAND__ICON_32x32='/path/to/image' + # icon_32x32: 'favicon-32x32.png' + # Environment variable override: PWP__BRAND__ICON_96x96='/path/to/image' + # icon_96x96: 'favicon-96x96.png' + # Environment variable override: PWP__BRAND__ICON_16x16='/path/to/image' + # icon_16x16: 'favicon-16x16.png' + # Environment variable override: PWP__BRAND__ICON_144x144='/path/to/image' + # ms_icon_144x144: 'ms-icon-144x144.png' + +### Throttling +# +# Configure the application throttling limits. +# +# Throttling enforces a minimum time interval +# between subsequent HTTP requests from a particular client, as +# well as by defining a maximum number of allowed HTTP requests +# per a given time period (per second or minute) +# +# See https://github.com/rack/rack-attack +# +throttling: + # ..maximum number of allowed HTTP requests per minute + # + # Default: 120 + # + # Environment Variable Override: PWP__THROTTLING__MINUTE='60' + minute: 120 + + # ..maximum number of allowed HTTP requests per second + # + # Default: 60 + # + # Environment Variable Override: PWP__THROTTLING__SECOND='20' + second: 60 + +### Trusted Proxies +# +# By default, Password Pusher will only proxy related headers from proxies on +# the local network. If you are using a proxy that is not on the local network, +# you will need to add the IP address of the proxy to the list below. +# +# This is useful if you are using a remote reverse proxy such as Cloudflare to +# serve the application. If local, you can leave this setting as is. +# +# Multiple IP addresses can be added by separating them with a comma. +# +# Environment Variable Override: +# PWP__TRUSTED_PROXIES='' +# PWP__TRUSTED_PROXIES=',' +# +# trusted_proxies: +# - '1.2.3.4' +# - '2.3.4.5' + +## Cloudflare Proxy +# +# If you are using Cloudflare as a proxy, you will need to set the following +# value to true. This will cause the application to fetch the list of Cloudflare +# proxy IP addresses and add them to the list of trusted proxies. +# +# Note that on application boot, this will trigger two HTTPs requests to fetch +# the list of Cloudflare IP addresses. The requests each have a timeout of 15 seconds +# and may delay on container boot. +# +# Environment Variable Override: +# PWP__CLOUDFLARE_PROXY='false' +# +cloudflare_proxy: false + +### Mail Server Configuration +# +# When logins are enabled, an SMTP server is required to send emails to users +# for things such as forgot password, unlock account, confirm account etc. +# If `enable_logins` is set to true above, the following _are required_ to be +# filled out with valid values. +# +# These values are passed through to ActionMailer configuration. The documentation +# for ActionMailer is at: +# https://guides.rubyonrails.org/action_mailer_basics.html#action-mailer-configuration +# +# IMPORTANT: Also set host_domain and host_protocol above for email links to work correctly! +# +mail: + # Email delivery errors will be shown in the application + # Environment Variable Override: PWP__MAIL__RAISE_DELIVERY_ERRORS='false' + raise_delivery_errors: true + + # Allows you to use a remote mail server. Just change it from its default "localhost" setting. + # Environment Variable Override: PWP__MAIL__SMTP_ADDRESS='smtp.example.com' + smtp_address: mail.smtp2go.com + + # If you need to specify a HELO domain, you can do it here. + # Environment Variable Override: PWP__MAIL__SMTP_DOMAIN='xyz.dev' + # smtp_domain: '' + + # Port of the SMTP server + # Environment Variable Override: PWP__MAIL__SMTP_PORT='587' + smtp_port: 2525 + + # If your mail server requires authentication, you need to specify the + # authentication type here. This is a string and one of :plain (will send + # the password in the clear), :login (will send password Base64 encoded) + # or :cram_md5 (combines a Challenge/Response mechanism to exchange + # information and a cryptographic Message Digest 5 algorithm to hash + # important information) + # + # Important: Comment this out if your server doesn't require authentication. + # + # Environment Variable Override: PWP__MAIL__SMTP_AUTHENTICATION='plain' + # smtp_authentication: 'plain' + + # If your mail server requires authentication, set the username in this setting. + # Environment Variable Override: PWP__MAIL__SMTP_USER_NAME='apikey' + smtp_user_name: 'freshStart-OCI' + + # If your mail server requires authentication, set the password in this setting. + # Environment Variable Override: PWP__MAIL__SMTP_PASSWORD='something@&#$' + # smtp_password: '' + + # Use STARTTLS when connecting to your SMTP server and fail if unsupported. + # Environment Variable Override: PWP__MAIL__SMTP_STARTTLS='true' + # smtp_starttls: false + + # Detects if STARTTLS is enabled in your SMTP server and starts to use it. Defaults to true. + # Environment Variable Override: PWP__MAIL__SMTP_ENABLE_STARTTLS_AUTO='false' + smtp_enable_starttls_auto: true + + # Number of seconds to wait while attempting to open a connection. + # Environment Variable Override: PWP__MAIL__SMTP_OPEN_TIMEOUT='10' + smtp_open_timeout: 10 + + # Number of seconds to wait until timing-out a read(2) call. + # Environment Variable Override: PWP__MAIL__SMTP_READ_TIMEOUT='10' + smtp_read_timeout: 10 + + # When using TLS, you can set how OpenSSL checks the certificate. This is + # useful if you need to validate a self-signed and/or a wildcard certificate. + # This can be one of the OpenSSL verify constants, :none or :peer + # Environment Variable Override: PWP__MAIL__SMTP_OPENSSL_VERIFY_MODE='none' + # smtp_openssl_verify_mode: 'peer' + + # Configure the e-mail address which will be shown as 'From' in emails + # See config/initializers/devise.rb where this is used + # Environment Variable Override: PWP__MAIL__MAILER_SENDER='"Password Pusher" ' + mailer_sender: '"Password Pusher" ' + +### Docker Pre-compilation +# +# This is useful if you modified the assets (e.g. CSS, JS, images) to customize +# the theme etc... Assets are precompiled before serving. +# +# If you set a custom theme, you will need to precompile the assets on container boot. +# +# Pre-compilation isn't supported in this yaml file. It is only supported +# through the environment variable PWP_PRECOMPILE='true'. +# +# Environment Variable: PWP_PRECOMPILE='false' + +### Themes +# +# Password Pusher uses Bootswatch themes. See https://bootswatch.com/ +# +# The following are the available themes. The default theme is 'default'. +# +# 'cerulean', 'cosmo', 'cyborg', 'darkly', 'flatly', 'journal', 'litera', 'lumen', +# 'lux', 'materia', 'minty', 'morph', 'pulse', 'quartz', 'sandstone', 'simplex', +# 'sketchy', 'slate', 'solar', 'spacelab', 'superhero', 'united', 'vapor', 'yeti', 'zephyr' +# +# To change the theme, set the `theme` value to the name of the theme you +# want to use. +# +# Environment Variable Override: PWP__THEME='default' +# +# Important Note: This setting can only be controlled by environment variable. (PWP__THEME) +# It cannot be set in this config file. +theme: 'default' + +### Site Default Locale +# +# The default language for the application. This must be one of the +# valid/supported language codes from the list above. +# +# Note: This locale _must_ be in the list of enabled_language_codes. +# +# Example: default_locale: :es +# +# Environment Variable Override: PWP__DEFAULT_LOCALE='es' +default_locale: en + +### Language & Internationalization +# +# List of enabled languages for the application. +# +# To remove the availability of languages from the application entirely, +# comment out (or remove) the language code(s) from the `enabled_language_codes` +# list below. +# +enabled_language_codes: + - ca # 'Català' + - cs # 'Čeština' + - da # 'Dansk' + - de # 'Deutsch' + - en # 'English' + - en-GB # 'English (UK)' + - es # 'Español' + - eu # 'Euskara' + - fi # 'Suomi' + - fr # 'Français' + - hi # 'हिन्दी' + - hu # 'Magyar' + - id # 'Indonesian' + - is # 'Íslenska' + - it # 'Italiano' + - ja # '日本語' + - ko # '한국어' + - lv # 'Latviski' + - nl # 'Nederlands' + - 'no' # 'Norsk' # _no_ keyword in Ruby evaluates to false #-( + - pl # 'Polski' + - pt-BR # 'Português' + - pt-PT # 'Português' + - ro # 'Română' + - ru # 'Русский' + - sr # 'Српски' + - sk # 'Slovenský' + - sv # 'Svenska' + - th # 'ไทย' + - uk # 'Українська' + - ur # 'اردو' + - zh-CN # '中文' + +### Language & Internationalization +# +# Map of language codes to language name. +# Used internally for the language selector model. +# +# : '' +language_codes: + ca: 'Català' + cs: 'Čeština' + da: 'Dansk' + de: 'Deutsch' + en: 'English' + es: 'Español' + eu: 'Euskara' + en-GB: 'English (UK)' + fi: 'Suomi' + fr: 'Français' + hi: 'हिन्दी' + hu: 'Magyar' + id: 'Indonesian' + is: 'Íslenska' + it: 'Italiano' + ja: '日本語' + ko: '한국어' + lv: 'Latviski' + nl: 'Nederlands' + 'no': 'Norsk' # _no_ keyword in Ruby evaluates to false :-( + pl: 'Polski' + pt-BR: 'Português' + pt-PT: 'Português' + ro: 'Română' + ru: 'Русский' + sk: 'Slovenský' + sr: 'Српски' + sv: 'Svenska' + th: 'ไทย' + uk: 'Українська' + ur: 'اردو' + zh-CN: '中文' + +# Used internally for the language selector model. +# This provides a conversion of language codes to +# country codes show the correct flag icons. +# See: +# https://github.com/lipis/flag-icons +# https://flagicons.lipis.dev +country_codes: + ca: :es-ct + cs: :cz + da: :dk + de: :de + en: :us + en-GB: :gb + es: :es + eu: :es-pv + fi: :fi + fr: :fr + hi: :in + hu: :hu + id: :id + is: :is + it: :it + ja: :jp + ko: :kr + lv: :lv + nl: :nl + 'no': :no # _no_ keyword in Ruby evaluates to false :-( + pl: :pl + pt-BR: :br + pt-PT: :pt + ro: :ro + ru: :ru + sk: :sk + sr: :rs + sv: :se + th: :th + uk: :ua + ur: :pk + zh-CN: :cn + +# Configure the logging verbosity of the application. +# +# Valid values are: :debug, :info, :warn, :error, :fatal +log_level: :warn + +# In containers, it is usually desired to log to stdout +# instead of using log files (e.g. log/production.log). +# +log_to_stdout: true diff --git a/pwpush_server-setup b/pwpush_server-setup new file mode 100644 index 0000000..f56d6c2 --- /dev/null +++ b/pwpush_server-setup @@ -0,0 +1,14 @@ +#!/bin/bash + +# shellcheck source=pwpush_server-env +. "${HOME}"/"${USER}"-env + +echo -e "\n[+] setting up pwpush\n\n-------\n" + +mkdir -p "${VOLUME_PATH}"/config +mkdir -p "${VOLUME_PATH}"/database +cp "${USER}"-settings.yaml "${VOLUME_PATH}"/config + +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d diff --git a/pwpush_server-teardown b/pwpush_server-teardown new file mode 100644 index 0000000..950d40d --- /dev/null +++ b/pwpush_server-teardown @@ -0,0 +1,14 @@ +#!/bin/sh + +username=pwpush_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/recipes.knravish.me.conf b/recipes.knravish.me.conf new file mode 100644 index 0000000..a31b8d5 --- /dev/null +++ b/recipes.knravish.me.conf @@ -0,0 +1,18 @@ +server { + server_name recipes.knravish.me; + index index.html index.htm; + + location / { + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header Host $http_host; + proxy_pass http://127.0.0.1:9925; + proxy_redirect off; + proxy_set_header Access-Control-Allow-Origin *; + proxy_read_timeout 600s; + proxy_send_timeout 600s; + } + + listen 80; +} diff --git a/shlink_server-compose.yaml b/shlink_server-compose.yaml new file mode 100644 index 0000000..a620354 --- /dev/null +++ b/shlink_server-compose.yaml @@ -0,0 +1,13 @@ +--- +services: + shlink: + image: ghcr.io/shlinkio/shlink:stable + container_name: my_shlink + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:8080:8080 + environment: + DEFAULT_DOMAIN: lnk.knravish.me + IS_HTTPS_ENABLED: true + DISABLE_TRACKING: true diff --git a/shlink_server-setup b/shlink_server-setup new file mode 100644 index 0000000..274df17 --- /dev/null +++ b/shlink_server-setup @@ -0,0 +1,20 @@ +#!/bin/bash + +echo -e "\n[+] setting up shlink\n\n-------\n" + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +declare -A codes=( + ["in"]="https://linkedin.com/in/kaushik-ravishankar" + ["github"]="https://github.com/20kaushik02" + ["folio"]="https://knravish.me" + ["k23"]="https://k23.kurukshetraceg.org.in" +) + +# give it some time to start +sleep 1 + +for shortcode in "${!codes[@]}"; do + echo "$shortcode - ${codes[$shortcode]}" + sudo docker exec -it my_shlink shlink short-url:create -c "$shortcode" -rnf "${codes[$shortcode]}" +done diff --git a/shlink_server-teardown b/shlink_server-teardown new file mode 100644 index 0000000..36de4b4 --- /dev/null +++ b/shlink_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=shlink_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/spotmgr_server-backup b/spotmgr_server-backup new file mode 100644 index 0000000..836a596 --- /dev/null +++ b/spotmgr_server-backup @@ -0,0 +1,42 @@ +#!/bin/bash + +# shellcheck source=spotmgr_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] spotify-manager backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start postgres + # shellcheck disable=SC2024 + sudo docker exec -u "${PUID}:${PGID}" -it spotify-manager-postgres sh -c \ + 'pg_dumpall -c --if-exists -U postgres' >/tmp/"${USER}"-backup/db.out + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Spotify Manager" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -rf /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Spotify Manager" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -rf /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/spotmgr_server-compose_template.yaml b/spotmgr_server-compose_template.yaml new file mode 100644 index 0000000..6b8a24c --- /dev/null +++ b/spotmgr_server-compose_template.yaml @@ -0,0 +1,56 @@ +--- +services: + postgres: + container_name: spotify-manager-postgres + image: postgres + restart: on-failure + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: spotify-manager + volumes: + - ${VOLUME_PATH}/pgdata:/var/lib/postgresql/data + - /etc/passwd:/etc/passwd:ro + user: ${PUID}:${PGID} + healthcheck: + test: ['CMD-SHELL', 'psql -U postgres -d spotify-manager -c "select version();"'] + interval: 1s + retries: 5 + timeout: 5s + redis: + container_name: spotify-manager-redis + image: redis + restart: on-failure + volumes: + - ${VOLUME_PATH}/redisdata:/data + user: ${PUID}:${PGID} + healthcheck: + test: ['CMD-SHELL', 'redis-cli ping | grep PONG'] + interval: 1s + retries: 5 + timeout: 3s + api: + container_name: spotify-manager-api + image: kaushikr2/spotify-manager-api + init: true + restart: on-failure + ports: + - 127.0.0.1:9001:9001 + depends_on: + postgres: + condition: service_healthy + restart: true + redis: + condition: service_healthy + restart: true + environment: + NODE_ENV: production + SPOTMGR_PORT: 9001 + SPOTMGR_CLIENT_ID: ${SPOTMGR_CLIENT_ID} + SPOTMGR_CLIENT_SECRET: ${SPOTMGR_CLIENT_SECRET} + SPOTMGR_SESSION_SECRET: ${SPOTMGR_SESSION_SECRET} + SPOTMGR_TRUST_PROXY: 1 + SPOTMGR_BASE_DOMAIN: 'spotify-manager.knravish.me' + SPOTMGR_REDIRECT_URI: 'https://api.spotify-manager.knravish.me/api/auth/callback' + SPOTMGR_APP_URI: 'https://spotify-manager.knravish.me/' + SPOTMGR_DB_URI: 'postgres://postgres:${POSTGRES_PASSWORD}@postgres:5432/spotify-manager' + SPOTMGR_REDIS_URI: redis://redis:6379 diff --git a/spotmgr_server-cronjob b/spotmgr_server-cronjob new file mode 100644 index 0000000..bb1d96b --- /dev/null +++ b/spotmgr_server-cronjob @@ -0,0 +1 @@ +9 10 * * * /home/spotmgr_server/spotmgr_server-backup diff --git a/spotmgr_server-setup b/spotmgr_server-setup new file mode 100644 index 0000000..8860e53 --- /dev/null +++ b/spotmgr_server-setup @@ -0,0 +1,22 @@ +#!/bin/bash + +echo -e "\n[+] setting up spotify-manager\n\n-------\n" + +# shellcheck source=spotmgr_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${VOLUME_PATH}"/{pg,redis}data +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] restoring database from backup..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start postgres + +rclone copy "${BUCKET_PATH}" "${HOME}" -v +cat db.out | sudo docker exec -i spotify-manager-postgres psql -U postgres -X + +echo "[+] restarting..." +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/stirling_server-compose_template.yaml b/stirling_server-compose_template.yaml new file mode 100644 index 0000000..9c4e569 --- /dev/null +++ b/stirling_server-compose_template.yaml @@ -0,0 +1,36 @@ +--- +services: + stirling: + image: frooodle/s-pdf:latest + container_name: stirling-pdf + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:8080 + volumes: + - type: bind + source: ${VOLUME_PATH}/trainingData + target: /usr/share/tessdata + bind: + create_host_path: true + - type: bind + source: ${VOLUME_PATH}/extraConfigs + target: /configs + bind: + create_host_path: true + - type: bind + source: ${VOLUME_PATH}/logs + target: /logs + bind: + create_host_path: true + environment: + PUID: ${PUID} + PGID: ${PGID} + DOCKER_ENABLE_SECURITY: true + SECURITY_ENABLE_LOGIN: true + SECURITY_INITIALLOGIN_USERNAME: ${INITIAL_USERNAME} + SECURITY_INITIALLOGIN_PASSWORD: ${INITIAL_PASSWORD} + SECURITY_CSRFDISABLED: false + SYSTEM_SHOWUPDATEONLYADMIN: true + INSTALL_BOOK_AND_ADVANCED_HTML_OPS: false + LANGS: en_US diff --git a/stirling_server-cronjob b/stirling_server-cronjob new file mode 100644 index 0000000..b560763 --- /dev/null +++ b/stirling_server-cronjob @@ -0,0 +1 @@ +10 11 * * 2 /home/stirling_server/stirling_server-update diff --git a/stirling_server-setup b/stirling_server-setup new file mode 100644 index 0000000..69d7bb8 --- /dev/null +++ b/stirling_server-setup @@ -0,0 +1,11 @@ +#!/bin/bash + +echo -e "\n[+] setting up stirling-pdf\n\n-------\n" + +# shellcheck source=stirling_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${VOLUME_PATH}" +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d diff --git a/stirling_server-teardown b/stirling_server-teardown new file mode 100644 index 0000000..6d4d56e --- /dev/null +++ b/stirling_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=stirling_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/stirling_server-update b/stirling_server-update new file mode 100644 index 0000000..220a290 --- /dev/null +++ b/stirling_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating stirling-pdf\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/syncthing.knravish.me.conf b/syncthing.knravish.me.conf new file mode 100644 index 0000000..91f12dd --- /dev/null +++ b/syncthing.knravish.me.conf @@ -0,0 +1,16 @@ +server { + server_name syncthing.knravish.me; + index index.html index.htm; + + include /etc/nginx/snippets/authelia-location.conf; + + set $upstream http://127.0.0.1:8384; + + location / { + include /etc/nginx/snippets/proxy.conf; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass $upstream; + } + + listen 80; +} diff --git a/syncthing_server-backup b/syncthing_server-backup new file mode 100644 index 0000000..e1ca41e --- /dev/null +++ b/syncthing_server-backup @@ -0,0 +1,39 @@ +#!/bin/bash + +# shellcheck source=syncthing_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] syncthing backup\n" + + mkdir -p /tmp/"${USER}"-backup + + syncthing cli operations shutdown + + cp -pr "${CONFIG_PATH}"/* /tmp/"${USER}"-backup + + systemctl --user restart syncthing.service + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" --exclude ./*.db/** -v; then + curl -Ss \ + -H "Title: Syncthing" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Syncthing" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/syncthing_server-cronjob b/syncthing_server-cronjob new file mode 100644 index 0000000..4f71fac --- /dev/null +++ b/syncthing_server-cronjob @@ -0,0 +1 @@ +11 10 * * * /home/syncthing_server/syncthing_server-backup diff --git a/syncthing_server-setup b/syncthing_server-setup new file mode 100644 index 0000000..ebe3877 --- /dev/null +++ b/syncthing_server-setup @@ -0,0 +1,18 @@ +#!/bin/bash + +# Syncthing starts running and installs user service upon installation (in instance-setup) +echo -e "\n[+] setting up syncthing\n-------\n" + +# shellcheck source=syncthing_server-env +. "${HOME}/${USER}-env" + +echo -e "[+] restoring config from backup..." + +syncthing cli operations shutdown + +rm -rf "${CONFIG_PATH}"/*.db # regenerate db to avoid data loss/errors +rclone copy "${BUCKET_PATH}" "${CONFIG_PATH}" -v + +echo "[+] restarting..." + +systemctl --user restart syncthing.service diff --git a/ubuntu-cronjob b/ubuntu-cronjob new file mode 100644 index 0000000..81d104a --- /dev/null +++ b/ubuntu-cronjob @@ -0,0 +1,2 @@ +USER=ubuntu +0 7 * * 2 /home/ubuntu/ubuntu_auto_apt_upgrade diff --git a/ubuntu_auto_apt_upgrade b/ubuntu_auto_apt_upgrade new file mode 100644 index 0000000..7dba9ea --- /dev/null +++ b/ubuntu_auto_apt_upgrade @@ -0,0 +1,26 @@ +#!/bin/bash + +mkdir -p "${HOME}"/upgrade_logs +logFile=${HOME}/upgrade_logs/$(date +%y_%m).log +rebootDelayInMinutes=10 + +{ + echo "[+] $(date -I'seconds')" + echo "[+] Auto apt upgrade starting..." + sudo apt-get update + + sudo apt-get upgrade -y + + if [[ -s /var/run/reboot-required ]]; then + curl -Ss \ + -H "Title: System Reboot scheduled" \ + -H "Priority: 3" \ + -H "Tags: loudspeaker,reboot" \ + -d "Rebooting in $rebootDelayInMinutes minutes. Reason: package updates" \ + "${NOTIF_URL}" + echo "[!] Rebooting in $rebootDelayInMinutes minutes..." + echo 'sudo reboot' | at now + $rebootDelayInMinutes minutes + else + echo "[+] Upgrade complete, no reboot required." + fi +} &>>"$logFile" diff --git a/vikunja_server-backup b/vikunja_server-backup new file mode 100644 index 0000000..5becb93 --- /dev/null +++ b/vikunja_server-backup @@ -0,0 +1,39 @@ +#!/bin/bash + +# shellcheck source=vikunja_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] vikunja backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + cp -pr "${VOLUME_PATH}"/* /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: Vikunja" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: Vikunja" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -r /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/vikunja_server-compose_template.yaml b/vikunja_server-compose_template.yaml new file mode 100644 index 0000000..13c4394 --- /dev/null +++ b/vikunja_server-compose_template.yaml @@ -0,0 +1,24 @@ +services: + vikunja: + image: vikunja/vikunja + container_name: vikunja + pull_policy: always + restart: unless-stopped + ports: + - 127.0.0.1:${PORT}:3456 + user: ${PUID}:${PGID} + volumes: + - type: bind + source: ${VOLUME_PATH}/files + target: /app/vikunja/files + bind: + create_host_path: true + - type: bind + source: ${VOLUME_PATH}/db + target: /db + bind: + create_host_path: true + environment: + VIKUNJA_SERVICE_JWTSECRET: ${JWT_SECRET} + VIKUNJA_SERVICE_PUBLICURL: ${BASE_URL} + VIKUNJA_DATABASE_PATH: /db/vikunja.db diff --git a/vikunja_server-cronjob b/vikunja_server-cronjob new file mode 100644 index 0000000..94ca47f --- /dev/null +++ b/vikunja_server-cronjob @@ -0,0 +1,2 @@ +12 10 * * * /home/vikunja_server/vikunja_server-backup +12 11 * * 2 /home/vikunja_server/vikunja_server-update diff --git a/vikunja_server-setup b/vikunja_server-setup new file mode 100644 index 0000000..2f5ccae --- /dev/null +++ b/vikunja_server-setup @@ -0,0 +1,22 @@ +#!/bin/bash + +echo -e "\n[+] setting up vikunja\n\n-------\n" + +# shellcheck source=vikunja_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${VOLUME_PATH}"/{files,db} + +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] restoring from backup..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + +rm -r "${VOLUME_PATH:?}"/* + +rclone copy "${BUCKET_PATH}" "${VOLUME_PATH}" -v + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/vikunja_server-teardown b/vikunja_server-teardown new file mode 100644 index 0000000..6ff72ab --- /dev/null +++ b/vikunja_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=vikunja_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/vikunja_server-update b/vikunja_server-update new file mode 100644 index 0000000..be09450 --- /dev/null +++ b/vikunja_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating vikunja\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/vpn.knravish.me.conf b/vpn.knravish.me.conf new file mode 100644 index 0000000..6aabfaf --- /dev/null +++ b/vpn.knravish.me.conf @@ -0,0 +1,16 @@ +server { + server_name vpn.knravish.me; + index index.html index.htm; + + include /etc/nginx/snippets/authelia-location.conf; + + set $upstream http://127.0.0.1:51821; + + location / { + include /etc/nginx/snippets/proxy.conf; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass $upstream; + } + + listen 80; +} diff --git a/vtt.knravish.me.conf b/vtt.knravish.me.conf new file mode 100644 index 0000000..8e0b04a --- /dev/null +++ b/vtt.knravish.me.conf @@ -0,0 +1,16 @@ +server { + server_name vtt.knravish.me; + + include /etc/nginx/snippets/authelia-location.conf; + + set $upstream http://127.0.0.1:30000; + + location / { + include /etc/nginx/snippets/proxy.conf; + include /etc/nginx/snippets/websocket.conf; + include /etc/nginx/snippets/authelia-authrequest.conf; + proxy_pass $upstream; + } + + listen 80; +} diff --git a/wg/Split Tunneling WireGuard.pdf b/wg/Split Tunneling WireGuard.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a73dba0abab27488406c930bea47b9afc4356a9a GIT binary patch literal 128703 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%0D8^Q*CtYS5s!-;kw9YaR~F~HV2}#s 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 E4?Xo6&Hw-a literal 0 HcmV?d00001 diff --git a/wg/all_proxied/PostUp copy.ps1 b/wg/all_proxied/PostUp copy.ps1 new file mode 100644 index 0000000..0211346 --- /dev/null +++ b/wg/all_proxied/PostUp copy.ps1 @@ -0,0 +1,26 @@ +# Wireguard tunnel interface details +$wgInterface = Get-NetAdapter -InterfaceAlias $env:WIREGUARD_TUNNEL_NAME +$wgAddress = (Get-NetIPAddress -InterfaceAlias $env:WIREGUARD_TUNNEL_NAME -AddressFamily IPv4 ).IPAddress + +# add default 0.0.0.0/0 route with low priority +route add 0.0.0.0 mask 0.0.0.0 0.0.0.0 IF $wgInterface.ifIndex metric 999 + +# Set the interface metric for the WireGuard tunnel +Set-NetIPInterface -InterfaceIndex $wgInterface.ifIndex -InterfaceMetric 999 + +# Navigate to the 3proxy directory +Set-Location "\\3proxy-0.9.4-x64\bin64\" +$cfg_file = "3proxy-wireguard.cfg" + +# Create 3proxy configuration file +'auth none' | Out-File -FilePath $cfg_file -Encoding ASCII +'internal 127.0.0.1' | Out-File -FilePath $cfg_file -Append -Encoding ASCII +"external ${wgAddress}" | Out-File -FilePath $cfg_file -Append -Encoding ASCII + +# rest of the proxy configuration +'socks' | Out-File -FilePath $cfg_file -Append -Encoding ASCII +'log "%USERPROFILE%\.logs\3proxy\%Y%m%d.log" D' | Out-File -FilePath $cfg_file -Append -Encoding ASCII +'rotate 30' | Out-File -FilePath $cfg_file -Append -Encoding ASCII + +# Start 3proxy in the background +Start-Process -FilePath '.\3proxy.exe' -ArgumentList $cfg_file -NoNewWindow diff --git a/wg/connections/README.md b/wg/connections/README.md new file mode 100644 index 0000000..134cd41 --- /dev/null +++ b/wg/connections/README.md @@ -0,0 +1 @@ +# Remember to keep your wg confs safe diff --git a/wg_server-backup b/wg_server-backup new file mode 100644 index 0000000..67e1b9c --- /dev/null +++ b/wg_server-backup @@ -0,0 +1,40 @@ +#!/bin/bash + +# shellcheck source=wg_server-env +. "${HOME}"/"${USER}"-env + +mkdir -p "${HOME}"/backup_logs +logFile=${HOME}/backup_logs/$(date +%y_%m).log + +{ + echo -e "\n[+] wg-easy backup\n" + + mkdir -p /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop + + sudo cp -pr "${VOLUME_PATH}"/wg0.json /tmp/"${USER}"-backup + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start + + sudo chown "${USER}":"${USER}" /tmp/"${USER}"-backup/* + if ! rclone copy /tmp/"${USER}"-backup "${BUCKET_PATH}" -v; then + curl -Ss \ + -H "Title: WG-Easy" \ + -H "Priority: 3" \ + -H "Tags: warning,backup" \ + -d "Backup not completed" \ + "${NOTIF_URL}" + rm -rf /tmp/"${USER}"-backup + exit 1 + fi + + curl -Ss \ + -H "Title: WG-Easy" \ + -H "Priority: 2" \ + -H "Tags: heavy_check_mark,backup" \ + -d "Backup completed" \ + "${NOTIF_URL}" + rm -rf /tmp/"${USER}"-backup + +} &>>"$logFile" diff --git a/wg_server-compose_template.yaml b/wg_server-compose_template.yaml new file mode 100644 index 0000000..4589de1 --- /dev/null +++ b/wg_server-compose_template.yaml @@ -0,0 +1,29 @@ +--- +services: + wg-easy: + image: ghcr.io/wg-easy/wg-easy:14 # breaking changes... + container_name: wg-easy + volumes: + - type: bind + source: ${VOLUME_PATH} + target: /etc/wireguard + bind: + create_host_path: true + ports: + - '${UDP_PORT}:51820/udp' + - '127.0.0.1:${GUI_PORT}:51821/tcp' + pull_policy: always + restart: unless-stopped + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + net.ipv4.ip_forward: 1 + net.ipv4.conf.all.src_valid_mark: 1 + environment: + PASSWORD_HASH: ${PASSWORD_HASH} + WG_HOST: ${WG_HOST} + WG_DEVICE: ${WG_DEVICE} # WAN interface + WG_PERSISTENT_KEEPALIVE: 25 + WG_POST_UP: 'iptables -I FORWARD -i wg0 -d 10.0.0.0/8 -j REJECT; iptables -I FORWARD -i wg0 -s 10.8.0.0/24 -d 10.0.0.0/8 -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE' + WG_POST_DOWN: 'iptables -I FORWARD -D wg0 -d 10.0.0.0/8 -j REJECT; iptables -I FORWARD -D wg0 -s 10.8.0.0/24 -d 10.0.0.0/8 -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE' diff --git a/wg_server-cronjob b/wg_server-cronjob new file mode 100644 index 0000000..bca8236 --- /dev/null +++ b/wg_server-cronjob @@ -0,0 +1,2 @@ +13 10 * * * /home/wg_server/wg_server-backup +13 11 * * 2 /home/wg_server/wg_server-update diff --git a/wg_server-setup b/wg_server-setup new file mode 100644 index 0000000..d5ee4b6 --- /dev/null +++ b/wg_server-setup @@ -0,0 +1,22 @@ +#!/bin/bash + +echo -e "\n[+] setting up wg-easy\n\n-------\n" + +# shellcheck source=wg_server-env +. "${HOME}"/"${USER}"-env + +envsubst <"${HOME}"/"${USER}"-compose_template.yaml >"${HOME}"/"${USER}"-compose.yaml + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d + +echo "[+] restoring configs from backup..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml stop +sudo rm "${VOLUME_PATH}"/* + +rclone copy "${BUCKET_PATH}" "${HOME}" -v +sudo cp wg0.json "${VOLUME_PATH}"/ + +echo "[+] restarting..." + +sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml start diff --git a/wg_server-teardown b/wg_server-teardown new file mode 100644 index 0000000..2e5d32b --- /dev/null +++ b/wg_server-teardown @@ -0,0 +1,14 @@ +#!/bin/bash + +username=wg_server + +# application +sudo docker compose -f /home/${username}/${username}-compose.yaml down -v + +uid_num=$(id -u $username) +sudo killall -9 -v -g -u $username +sudo crontab -r -u $username +sudo deluser --remove-all-files $username + +# clean-up +sudo find / -user "$uid_num" -delete diff --git a/wg_server-update b/wg_server-update new file mode 100644 index 0000000..9602685 --- /dev/null +++ b/wg_server-update @@ -0,0 +1,11 @@ +#!/bin/bash + +mkdir -p "${HOME}"/update_logs +logFile=${HOME}/update_logs/$(date +%y_%m).log +{ + echo -e "\n[+] updating wg-easy\n" + + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml pull && + sudo docker compose -f "${HOME}"/"${USER}"-compose.yaml up -d --always-recreate-deps --remove-orphans && + yes | sudo docker image prune -af +} &>>"$logFile" diff --git a/windows copy.md b/windows copy.md new file mode 100644 index 0000000..58da7c1 --- /dev/null +++ b/windows copy.md @@ -0,0 +1,223 @@ +# Windows machine stuff + +## Windows SSH server setup + +- make sure openssh server optional feature is enabled + +```powershell +powershell.exe "Get-WindowsCapability -Online | ? Name -like 'OpenSSH.Server*'" +``` + +- configuration, firewall rule, ssh-agent + +```powershell +# Set the sshd service to be started automatically +Get-Service -Name sshd | Set-Service -StartupType Automatic + +# Now start the sshd service +Start-Service sshd + +# Configure port if needed +New-NetFirewallRule -Name sshd -DisplayName 'OpenSSH Server (sshd)' -Enabled True -Direction Inbound -Protocol TCP -Action Allow -LocalPort 22 + +# Generate SSH keypair +ssh-keygen.exe -t ed25519 + +# Not sure if this ssh-agent stuff is needed but ok + +# By default the ssh-agent service is disabled. Configure it to start automatically. +# Make sure you're running as an Administrator. +Get-Service ssh-agent | Set-Service -StartupType Automatic + +# Start the service +Start-Service ssh-agent + +# This should return a status of Running +Get-Service ssh-agent + +# Now load your key files into ssh-agent +ssh-add $env:USERPROFILE\.ssh\id_ed25519 + +# Main part + +# Get the public key file generated previously on your client +$authorizedKey = Get-Content -Path $env:USERPROFILE\.ssh\id_ed25519.pub + +# Generate the PowerShell to be run remote that will copy the public key file generated previously on your client to the authorized_keys file on your server +$remotePowershell = "powershell New-Item -Force -ItemType Directory -Path $env:USERPROFILE\.ssh; Add-Content -Force -Path $env:USERPROFILE\.ssh\authorized_keys -Value '$authorizedKey'" + +# Connect to your server and run the PowerShell using the $remotePowerShell variable +ssh "$(whoami)@localhost" $remotePowershell +``` + +- edit `%PROGRAMDATA%/ssh/sshd_config` as administrator + +```ssh-config +PermitRootLogin no +MaxAuthTries 1 + +PubkeyAuthentication yes + +PasswordAuthentication no + +#Match Group administrators +# AuthorizedKeysFile __PROGRAMDATA__/ssh/administrators_authorized_keys +``` + +## WSL + +### prevent shutdown + +- [see here](https://github.com/microsoft/WSL/issues/8854#issuecomment-1490454734) +- the service file + +```systemd +[Unit] +Description=Keep Distro Alive + +[Service] +# cleanup if already waiting +# get waitfor path by `which waitfor` +ExecStartPre=/mnt/c/Windows/system32/waitfor.exe /si MakeDistroAlive +ExecStart=/mnt/c/Windows/system32/waitfor.exe MakeDistroAlive + +[Install] +WantedBy=multi-user.target +``` + +### networking mode - mirrored + +- [see here](https://superuser.com/a/1671057) +- create/add to `%USERPROFILE%/.wslconfig`: + +```ini +[wsl2] +networkingMode=mirrored +``` + +### get Wireguard interface + +- interface should exist, else it'll be blank + +```bash +wg_if=$(ip -4 -brief addr show | grep $wg_if_addr | awk '{printf "%s",$1;}') +curl -v --interface $wg_if +``` + +## 3proxy (native port for windows) + +- download `.zip` from [github releases (last checked ver - 0.9.4)](https://github.com/3proxy/3proxy/releases) and extract +- create a `.cfg` file: + +```ini +system "echo '3proxy up!'" + +config "\\3proxy-0.9.4-x64\bin64\3proxy.cfg" +monitor "\\3proxy-0.9.4-x64\bin64\3proxy.cfg" + +log "%USERPROFILE%\.logs\3proxy\%Y%m%d.log" D # the D at the end is important + +rotate 30 + +external 10.8.0.2 +internal 127.0.0.1 + +service + +auth none +socks +``` + +## The Wireguard split-tunnel problem + +- i wish to route certain _applications_: not IP address ranges, but programs, over a wireguard tunnel +- in particular, i wish to do this for traffic originating from my wireguard peer running on my local Windows machine, and the tunnel in question connects to a cloud VPS running Wireguard +- i believe this is called 'application-based split tunneling' + +- my understanding is that this is something that wireguard's Windows client does not support out of the box +- however, if my assumption is correct, i have an alternative approach in mind: + - first, whenever the wireguard interface gets created on my machine, it would also create the corresponding routes for the tunnel, and these routes have a low metric value + - here, i would set the metric value of the new route(s) to a value higher than the default route that routes most/all the traffic, thereby deprioritizing the tunnel + - automate this part to update the metric on interface creation/teardown + - this would effectively leave the tunnel active but unused + - then, i would create a SOCKS proxy on my local machine, to localhost itself + - finally, i would bind any application that would use the wireguard tunnel to this proxy, through the application's settings itself if it provides such functionality, or through third-party applications, such as Proxifier + +### Disable automatic route creation + +- in the [Interface] section of your tunnel config, add `Table = off` + - this informs WireGuard not to create a default route automatically +- note that this also disables blocking of untunneled traffic (kill-switch functionality), which is what we want in order to achieve split-tunneling +- this is a must. direct route manipulation through scripting is not permitted, if the kill-switch functionality is active. it will simply drop traffic (IIRC) + +### Enable Wireguard scripts + +- [scripts are not enabled in Wireguard Windows by default](https://github.com/WireGuard/wireguard-windows/blob/master/docs/adminregistry.md) + - go to `Computer\HKEY_LOCAL_MACHINE\SOFTWARE` + - right-click `SOFTWARE` in the navigation pane, click `New -> Key`, name it `WireGuard` + - create a new `DWORD (32-bit) Value` in the new created key named `DangerousScriptExecution` + - set its value to `1` +- now you can add `PreUp`, `PostUp`, `PreDown`, `PostDown` scripts + +#### PostUp script + +- since we disabled automatic default route(s) addition to table, we have to add the necessary routes + - modify routes according to allowedIPs + - given case is 0.0.0.0/0 + - for other cases, see what routes WireGuard generates on its own normally, and add the missing route(s) from those + - other routes get added automatically + +```powershell +'postup start' | Out-File -FilePath "${PSScriptRoot}\PostUp.log" + +# Wireguard tunnel interface details +$wgInterface = Get-NetAdapter -InterfaceAlias $env:WIREGUARD_TUNNEL_NAME +$wgAddress = (Get-NetIPAddress -InterfaceAlias $env:WIREGUARD_TUNNEL_NAME -AddressFamily IPv4 ).IPAddress + +# add default 0.0.0.0/0 route with low priority +route add 0.0.0.0 mask 0.0.0.0 0.0.0.0 IF $wgInterface.ifIndex metric 999 + +# Set the interface metric for the WireGuard tunnel +Set-NetIPInterface -InterfaceIndex $wgInterface.ifIndex -InterfaceMetric 999 + +# Navigate to the 3proxy directory +Set-Location "\\3proxy-0.9.4-x64\bin64\" +$cfg_file = "3proxy-wireguard.cfg" + +# Create 3proxy configuration file +'auth none' | Out-File -FilePath $cfg_file -Encoding ASCII +'internal 127.0.0.1' | Out-File -FilePath $cfg_file -Append -Encoding ASCII +"external ${wgAddress}" | Out-File -FilePath $cfg_file -Append -Encoding ASCII + +# rest of the proxy configuration +'socks' | Out-File -FilePath $cfg_file -Append -Encoding ASCII +'log "%USERPROFILE%\.logs\3proxy\%Y%m%d.log" D' | Out-File -FilePath $cfg_file -Append -Encoding ASCII +'rotate 30' | Out-File -FilePath $cfg_file -Append -Encoding ASCII + +# Start 3proxy in the background +Start-Process -FilePath '.\3proxy.exe' -ArgumentList $cfg_file -NoNewWindow + +'postup end' | Out-File -FilePath "${PSScriptRoot}\PostUp.log" -Append + +``` + +#### PreDown script + +- make sure to specify all routes created in the `PostUp` script + +```powershell +'predown start' | Out-File -FilePath "${PSScriptRoot}\PreDown.log" + +# WireGuard tunnel details +$wgInterface = Get-NetAdapter -Name $env:WIREGUARD_TUNNEL_NAME + +# Delete the default 0.0.0.0/0 route using the interface index +route delete 0.0.0.0 mask 0.0.0.0 0.0.0.0 if $wgInterface.ifIndex + +# Terminate any running instances of 3proxy.exe +Set-Location "\\3proxy-0.9.4-x64\bin64\" +Stop-Process -Name "3proxy.exe" -Force + +'predown end' | Out-File -FilePath "${PSScriptRoot}\PreDown.log" -Append + +```