self hosting vaultwarden

update: i have since moved this setup to be run off a raspberry pi in my basement, still using tailscale so that i can connect to it while away from home

this is the topic that originally motivated me to setup this site: i really wanted to talk about it but whenever i did it would cause people visible discomfort with how boring it was.

the gist of this setup is putting vaultwarden, a web server for the bitwarden password manager on [[fly.io]] but not exposing this sucker to the public internet. keeping it off the public internet is a lot less risky when it comes to security.[^1]

that’s where tailscale comes in: i sign into tailscale with my github account, and then i can magically visit this machine by putting it’s name into my browser’s url bar. pretty cool! no dns entries needed. no one else can do that since the machine doesn’t have any ports exposed in it’s fly configuration

this all seemed to be adequate until i learned that vaultwarden doesn’t really work without https. and the web server they ship with doesn’t do https that well, so after reading that caddy

  • is Brad Fitzpatrick approved
  • is recommended in the vaultwarden wiki
  • will automatically get me the https certs i need from letsencrypt

sold! the way caddy fits in is that my computer and phone connect to the caddy server via wireguard by way of tailscale and their https support. caddy proxies the request to the vaultwarden server through fly.io’s internal wireguard network.

for a variety of reasons this was a huge pain in the ass to get working so i wanted to save some poor schmuck like me a bit of time by publishing my dockerfiles. this should be obvious but please note that i don’t know what i’m doing.

vaultwarden collapsed:: true

  • Dockerfile
FROM vaultwarden/server:alpine
  • docker-compose.yml
version: '3'

services:
  vaultwarden:
    image: localvault
    container_name: vaultwardenlocal
    restart: always
    environment:
      WEBSOCKET_ENABLED: "true"  # Enable WebSocket notifications.
    volumes:
      - ./vw-data:/data
    ports:
      - "8082:80"
  • fly.toml
app = "vaultwarden-machine"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]
DATA_FOLDER = "vw-data"
ROCKET_ADDRESS = "::"
WEBSOCKET_ENABLED = "true"
WEBSOCKET_ADDRESS = "::"
WEBSOCKET_PORT = 3012

[experimental]
allowed_public_ports = []
auto_rollback = true

[mounts]
source = "your_fly_volume"
destination = "/vw-data"

caddy

Dockerfile

FROM caddy:2-alpine as builder
WORKDIR /app
COPY . ./
COPY ./Caddyfile /etc/caddy/Caddyfile

FROM alpine:latest as tailscale
WORKDIR /app
COPY . ./
ENV TSFILE=tailscale_1.28.0_amd64.tgz
RUN wget https://pkgs.tailscale.com/stable/${TSFILE} && tar xzf ${TSFILE} --strip-components=1
COPY . ./

# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM caddy:2-alpine
RUN apk update && apk add ca-certificates iptables ip6tables && rm -rf /var/cache/apk/*

# Copy binary to production image
COPY --from=builder /app/start.sh /app/start.sh
COPY --from=builder /etc/caddy/Caddyfile /etc/caddy/Caddyfile
COPY --from=tailscale /app/tailscaled /app/tailscaled
COPY --from=tailscale /app/tailscale /app/tailscale
RUN mkdir -p /var/run/tailscale /var/cache/tailscale /var/lib/tailscale

#caddy is running on 80
EXPOSE 80

# Run on container startup.
CMD ["/app/start.sh"]

start.sh

#!/bin/sh

echo "starting tailscaled"
/app/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/var/run/tailscale/tailscaled.sock &
echo "tailscale up"
/app/tailscale up --authkey=${TAILSCALE_AUTHKEY} --hostname=schpet-caddyvault

echo "Executing: caddy start -config /etc/caddy/Caddyfile"
caddy run -config /etc/caddy/Caddyfile

caddyfile

# https://github.com/dani-garcia/vaultwarden/wiki/Proxy-examples
vaultwarden-machine.random-whatever.ts.net {
    log {
        level INFO
        output stdout
    }
    encode gzip

    reverse_proxy /notifications/hub/negotiate vaultwarden-machine.internal:80
    reverse_proxy /notifications/hub vaultwarden-machine.internal:3012 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
    reverse_proxy vaultwarden-machine.internal:80 {
        header_up X-Real-IP {remote_host}
    }
}

fly.toml

app = "caddy-machine"
kill_signal = "SIGINT"
kill_timeout = 5
processes = []

[env]

[experimental]
  allowed_public_ports = []
  auto_rollback = true

[[services]]
  http_checks = []
  internal_port = 80
  processes = ["app"]
  protocol = "tcp"
  script_checks = []
  [services.concurrency]
    hard_limit = 25
    soft_limit = 20
    type = "connections"

  # uncomment the following if you want to expose this to the internet

  # [[services.ports]]
  #   force_https = true
  #   handlers = ["http"]
  #   port = 80

  # [[services.ports]]
  #   handlers = ["tls", "http"]
  #   port = 443

  [[services.tcp_checks]]
    grace_period = "1s"
    interval = "15s"
    restart_limit = 0
    timeout = "2s"

notes

  • tailscale on fly.io
  • scale-to-zero would be particularly nice for stuff like this so i hope fly adds that like they say they will
  • i had a problem where every time i deployed caddy i’d need to make a new tailscale key, and haven’t bothered figuring it out given i haven’t deployed this since i initially got it working a few months ago

^1: i was totally happy using 1password for years until they decided the only way i can sync my passwords from my computer to my phone was through their website

© 2024 peter schilling