diff --git a/.env b/.env new file mode 100644 index 0000000..53d78a2 --- /dev/null +++ b/.env @@ -0,0 +1,75 @@ +# Your domain name without protocol +# If you don't have your own domain (highly recommended) +# comment this out for first deployment, add your fly.dev domain here +# then reset your secrets and redeploy your app (untested) +DOMAIN_NAME=domain.name.com + +################### +# E-mail settings # +################### + +# the e-mail address used to send e-mails from both vaultwarden and restic +SMTP_FROM=from@mail.com + +# the e-mail address to notify on case of restic backup failure +SMTP_TO=to@mail.com + +# https://github.com/dani-garcia/vaultwarden/wiki/SMTP-Configuration +SMTP_HOST=smtp.gmail.com +SMTP_PORT=587 +SMTP_USERNAME=88888888@gmail.com +SMTP_PASSWORD=88888888 + +################### +# Restic settings # +################### + +# You don't need to initialize this repo beforehand +RESTIC_REPOSITORY=s3://88888888.r2.cloudflarestorage.com/vaultwarden +RESTIC_PASSWORD=88888888 + +# If using S3 (or B2, wasabi, Minio) you'll need those +AWS_ACCESS_KEY_ID=88888888 +AWS_SECRET_ACCESS_KEY=88888888 + + +######################## +# Vaultwarden settings # +######################## + +# You can read more about vaultwarden environment variables here +# https://github.com/dani-garcia/vaultwarden/wiki/Configuration-overview#configuration-options + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Configuration-overview#setting-the-domain-url +# If you don't have your own domain (highly recommended) +# comment this out for first deployment, add your fly.dev domain here +# then reset your secrets and redeploy your app (untested) +DOMAIN=https://{$DOMAIN_NAME} + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users +SIGNUPS_ALLOWED=false + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users#restricting-registrations-to-certain-email-domains +# SIGNUPS_DOMAINS_WHITELIST=example.com + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Disable-registration-of-new-users#restricting-registrations-to-certain-email-domains +# SIGNUPS_VERIFY=true + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Disable-invitations +# INVITATIONS_ALLOWED=false + +# This is commented by default, because I strongly advise setting this only when needed +# Make sure you read the link below to understand the consequences and to secure the token +# https://github.com/dani-garcia/vaultwarden/wiki/Enabling-admin-page +# ADMIN_TOKEN=secure-token + +# Check https://github.com/dani-garcia/vaultwarden/blob/e7f083dee9743bfe4937f5c8149fa9d8383edb96/.env.template#L261-L267 +ORG_CREATION_USERS=admin@gmail.com + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Hardening-Guide#disable-password-hint-display +SHOW_PASSWORD_HINT=false + +# Check https://github.com/dani-garcia/vaultwarden/wiki/Enabling-WebSocket-notifications +# You don't need to do any additional configuration, as the template +# already uses Caddy to configure the websocket proxy +WEBSOCKET_ENABLED=true diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 0000000..105e18f --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,54 @@ +name: Docker + +on: + workflow_dispatch: {} + schedule: + - cron: '25 2 * * *' + push: + tags: [ '*' ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build: + + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + id-token: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log into registry ${{ env.REGISTRY }} + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw, value=latest, enable={{is_default_branch}} + type=semver, pattern={{version}} + type=ref, event=branch + type=ref, event=tag + type=ref, event=pr + + - name: Build and push Docker image + id: build-and-push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..daa0595 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +FROM vaultwarden/server:1.30.3-alpine + +WORKDIR / + +ARG SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.29/supercronic-linux-amd64 \ + OVERMIND_URL=https://github.com/DarthSim/overmind/releases/download/v2.4.0/overmind-v2.4.0-linux-amd64.gz + +ENV OVERMIND_CAN_DIE=crontab \ + OVERMIND_PROCFILE=/Procfile \ + + ROCKET_PORT=8080 \ + + SMTP_HOST=smtp.gmail.com \ + SMTP_PORT=587 \ + SMTP_USERNAME=88888888@gmail.com \ + SMTP_PASSWORD=88888888 \ + SMTP_FROM=88888888@gmail.com \ + SMTP_TO= \ + + RESTIC_REPOSITORY=s3://88888888.r2.cloudflarestorage.com/vaultwarden \ + RESTIC_PASSWORD= \ + AWS_ACCESS_KEY_ID= \ + AWS_SECRET_ACCESS_KEY= + +COPY config/crontab \ + config/Procfile \ + config/Caddyfile \ + scripts/restic.sh \ + / + +RUN apk add --no-cache \ + curl \ + caddy \ + restic \ + ca-certificates \ + openssl \ + tzdata \ + iptables \ + ip6tables \ + iputils-ping \ + tmux \ + sqlite \ + msmtp \ + mailx \ + + && rm -rf /var/cache/apk/* \ + && curl -fsSL "$SUPERCRONIC_URL" -o /usr/local/bin/supercronic \ + && curl -fsSL "$OVERMIND_URL" | gunzip -c - > /usr/local/bin/overmind \ + + && ln -sf /usr/bin/msmtp /usr/bin/sendmail \ + && ln -sf /usr/bin/msmtp /usr/sbin/sendmail \ + + && chmod +x /usr/local/bin/supercronic \ + && chmod +x /usr/local/bin/overmind \ + && chmod +x /restic.sh + +CMD ["overmind", "start"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f536130 --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +``` +fly auth login +fly apps create vaultwarden +cat .env | fly secrets import +fly volumes create app_data --size 1 +fly deploy +fly ssh console +``` \ No newline at end of file diff --git a/config/Caddyfile b/config/Caddyfile new file mode 100644 index 0000000..32c1cf9 --- /dev/null +++ b/config/Caddyfile @@ -0,0 +1,46 @@ +{ + # HTTPS/TLS is handled by Fly or on your domain (eg: Cloudflare) + auto_https off + admin off + persist_config off + + log { + output stdout + format console + } +} + +{$DOMAIN_NAME}:80 { + encode gzip + + header / { + # Enable HTTP Strict Transport Security (HSTS) + Strict-Transport-Security "max-age=31536000;" + # Enable cross-site filter (XSS) and tell browser to block detected attacks + X-XSS-Protection "1; mode=block" + # Disallow the site to be rendered within a frame (clickjacking protection) + X-Frame-Options "DENY" + # Prevent search engines from indexing + X-Robots-Tag "noindex, nofollow" + # Disallow sniffing of X-Content-Type-Options + X-Content-Type-Options "nosniff" + # Server name removing + -Server + # Remove X-Powered-By though this shouldn't be an issue, better opsec to remove + -X-Powered-By + # Remove Last-Modified because etag is the same and is as effective + -Last-Modified + } + + # The negotiation endpoint is also proxied to Rocket + reverse_proxy /notifications/hub/negotiate localhost:8080 + + # Notifications redirected to the websockets server + reverse_proxy /notifications/hub localhost:3012 + + # Proxy everything else to Rocket + reverse_proxy localhost:8080 { + # Send the true remote IP to Rocket, so that vaultwarden can put this in the log + header_up X-Real-IP {remote_host} + } +} \ No newline at end of file diff --git a/config/Procfile b/config/Procfile new file mode 100644 index 0000000..cf197d0 --- /dev/null +++ b/config/Procfile @@ -0,0 +1,3 @@ +vaultwarden: /start.sh +caddy: caddy run --config /Caddyfile +crontab: supercronic /crontab \ No newline at end of file diff --git a/config/crontab b/config/crontab new file mode 100644 index 0000000..34af202 --- /dev/null +++ b/config/crontab @@ -0,0 +1,2 @@ +@hourly /restic.sh +@hourly /usr/bin/find /var/log/restic/ -name "*.log" -type f -mmin +600 -exec rm -f {} \; \ No newline at end of file diff --git a/fly.toml b/fly.toml new file mode 100644 index 0000000..1d5f25c --- /dev/null +++ b/fly.toml @@ -0,0 +1,26 @@ +app = "vaultwarden" +primary_region = "hkg" +kill_signal = "SIGINT" +kill_timeout = 5 + +[build] + image = "ghcr.io/webees/vaultwarden" + +[mounts] + source = "app_data" + destination = "/data" + +[http_service] + internal_port = 80 + force_https = true + min_machines_running = 1 + +[checks] + [checks.health] + grace_period = "30s" + interval = "15s" + method = "get" + path = "/alive" + port = 80 + timeout = "10s" + type = "http" diff --git a/scripts/restic.sh b/scripts/restic.sh new file mode 100644 index 0000000..baa17d7 --- /dev/null +++ b/scripts/restic.sh @@ -0,0 +1,120 @@ +#!/bin/sh + +if [ -z "$RESTIC_PASSWORD" ]; then + exit 1 +fi + +if [ -n "$SMTP_TO" ]; then +cat << EOF > /etc/msmtprc +defaults +auth on +tls on +tls_trust_file /etc/ssl/certs/ca-certificates.crt +logfile /var/log/msmtp.log + +account default +host $SMTP_HOST +port $SMTP_PORT +from $SMTP_FROM +user $SMTP_USERNAME +password $SMTP_PASSWORD +EOF +fi + +# catch the error in case first pipe command fails (but second succeeds) +set -o pipefail +# turn on traces, useful while debugging but commented out by default +# set -o xtrace + +EMAIL_SUBJECT_PREFIX="[Restic]" +LOG="/var/log/restic/$(date +\%Y\%m\%d_\%H\%M\%S).log" + +# create log dir +mkdir -p /var/log/restic/ + +# e-mail notification +function notify() { + if [ -n "$SMTP_TO" ]; then + sed -e 's/\x1b\[[0-9;]*m//g' "${LOG}" | mail -s "${EMAIL_SUBJECT_PREFIX} ${1}" ${SMTP_TO} + fi +} + +function log() { + "$@" 2>&1 | tee -a "$LOG" +} + +function run_silently() { + "$@" >/dev/null 2>&1 +} + +# ############################################################################### +# colorized echo helpers # +# taken from: https://github.com/atomantic/dotfiles/blob/master/lib_sh/echos.sh # +# ############################################################################### + +ESC_SEQ="\x1b[" +COL_RED=$ESC_SEQ"31;01m" +COL_BLUE=$ESC_SEQ"34;01m" +COL_GREEN=$ESC_SEQ"32;01m" +COL_YELLOW=$ESC_SEQ"33;01m" +COL_RESET=$ESC_SEQ"39;49;00m" + +function ok() { + log echo -e "$COL_GREEN[ok]$COL_RESET $1" +} + +function running() { + log echo -en "$COL_BLUE ⇒ $COL_RESET $1..." +} + +function warn() { + log echo -e "$COL_YELLOW[warning]$COL_RESET $1" +} + +function error() { + log echo -e "$COL_RED[error]$COL_RESET $1" + log echo -e "$2" +} + +function notify_and_exit_on_error() { + output=$(eval $1 2>&1) + + if [ $? -ne 0 ]; then + error "$2" "$output" + notify "$2" + exit 2 + fi +} + +# ############## +# backup steps # +# ############## +restic unlock + +running "checking restic config" + +run_silently restic cat config + +if [ $? -ne 0 ]; then + warn "restic repo either not initialized or erroring out" + running "trying to initialize it" + notify_and_exit_on_error "restic init" "Repo init failed" +fi + +ok + +running "backing up sqlite database" +notify_and_exit_on_error "sqlite3 /data/db.sqlite3 '.backup /data/backup.bak'" "SQLite backup failed" +ok + +running "restic backup" +notify_and_exit_on_error "restic backup --verbose --exclude='db.*' /data" "Restic backup failed" +ok + +running "checking consistency of restic repository" +notify_and_exit_on_error "restic check" "Restic check failed" +ok + +running "removing outdated snapshots" +notify_and_exit_on_error "restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 3 --keep-yearly 3 --prune" "Restic forget failed" +ok