I Deployed Vaultwarden on K3s So I Could Ditch 1Password

I Deployed Vaultwarden on K3s So I Could Ditch 1Password

Intro

My 1Password subscription was up for renewal at the start of this year, which prompted me to make good on one of my early resolutions: switching to a self-hosted Vaultwarden solution instead of paying out for the admittedly not expensive annual 1P sub. Uh, 1P is short for 1Password. If the sub was only 1 pence then I'd probably still be subbed 🤣

Anyway.

I migrated my self-hosted stack from Docker to K3s back in November of last year - I run some of the *arrs, Kavita, Plex, SABnzbd, Gitea etc. You know: lots of research applications. Maybe at some point I'll write a bit more about that particular process but my main motivation was to learn more about Kubernetes, which we've been using extensively in my day job for a couple of years now. I'm also an inveterate tinkerer, so having exhausted my interest in Docker, it seemed like a good idea at the time. Note: I exhausted my interested in Docker - I don't profess to have any actual expertise. 😑

Being an inveterate tinkerer I'm always looking for something new and useful to self-host and with my 1Password subscription renewal looming it seemed like a good time to have a look at Vaultwarden.

What is Vaultwarden?

A quick run-down on Vaultwarden then: it used to be called 'bitwarden-rs', and is an implementation of the Bitwarden backend in Rust. The name was changed to Vaultwarden to avoid implying that the project has any official relationship to Bitwarden proper. Bitwarden is an open-source password management suite - if you're familiar with 1Password or LastPass then you've an idea what Bitwarden entails. Your passwords (credit card details, secure notes, etc.) are saved in an online vault and you use an app - native or web-based - to manage and retrieve them.

You can self-host the Bitwarden server yourself - it is an open-source project and there's no real reason not to do so. Some features are locked behind a paid license - it's a bit hard to get a complete sense of what is and isn't, but from what I can glean if you're not using it in an enterprise setting then you probably won't miss any of the locked features.

Why bother with Vaultwarden then?

Apart from the perennial "why not?" question, Vaultwarden's resource requirements are apparently lighter. I say 'apparently' because I got as far as "why not?" and didn't investigate any further. I'm that shallow.

There are a few other reasons which you may or may not care about - for example, Vaultwarden supports some of the paid features of Bitwarden out of the box, and you can disable user registration in Vaultwarden which is something that you can't do with Bitwarden. You can also have as many users as you want - handy if you want to get your family on board with proper password safety. Again, these may or may not be relevant to your particular requirements.

On the flip side, there are a small number of things that Vaultwarden doesn't support from the official Bitwarden server. It's very enterprise-y - there's a list here. I'm not sure which, if any, of these are premium features of Bitwarden.

What about clients?

It's worth noting at this point that Vaultwarden only provides a backend - a place to centrally store your password vault(s). Other than the built-in web UI, the Vaultwarden project doesn't provide a suite of clients for your computers or smart devices. Luckily the Vaultwarden backend is fully compatible with the Bitwarden clients, so you can just use them.

Is this really a good idea?

There are other considerations. Some self-host advocates would argue that you shouldn't give up control of your sensitive data to companies - you have to trust that the likes of 1Password, LastPass, or even Bitwarden themselves are actually as diligent with there as they should be. There's always the temptation for companies to cut corners to save costs, and LastPass have had a handful of serious breaches over the past few years.

On the other hand, these companies do employ people with security expertise, and they open themselves up to scrutiny by external bodies - 1Password are SOC2 certified and have a bounty program, for example. They also have access to expertise that can help them deal with any breaches that do crop up - small comfort if your data has already been stolen and exposed, but it's not nothing.

A self-hosted vault solution has a much smaller attack surface by virtue of being more anonymous but there's no real value in security through obscurity. If your server is breached, a small number of people - possibly just one - will be impacted but that's not really much comfort if that person is you!

All worth considering before deciding whether or not to migrate away from 1Password (or indeed LastPass, Bitwarden, KeePass, an Excel spreadsheet, Post-It notes) to any self-hosted solution.

How'd I do it?

So having made the decision to switch to Vaultwarden and host it on my K3s cluster (of one node 🙄), the broad steps I needed to follow where:

  1. Set up a backend database.
  2. Set up persistent storage.
  3. Create the app, service, and ingress YAML definitions

The Database

Straightforward enough - I had previously followed the Kubernetes documentation on creating a MySQL stateful set, so I already have a little MySQL pod running on my K3s cluster and it made sense to use that. I could have opted to use SQLite instead but wanted the flexibility that comes with MySQL. It didn't have to be a K3s-hosted MySQL server either, I could have opted to use an external (to the cluster) one.

Instructions for creating the Vaultwarden database and user are in the wiki.

Persistent Storage

Since I'm using K3s as my Kubernetes distribution it makes sense to use Longhorn for persistent storage. Longhorn is reasonably easy to set up and use - the only clanger is that I forget to set the RetainPolicy for my persistent volumes to Retain instead of the default Delete. If you're like me then at least try to remember to check the policies occasionally and change any that are Delete to Retain.

It probably doesn't help that I generally just create a PersistentVolumeClaim definition and let that take care of creating the PersistentVolume itself.

pvc.yaml:

---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vaultwarden-data
  namespace: vaultwarden
spec:
  accessModes:
  - ReadWriteOnce
  storageClassName: longhorn
  resources:
    requests:
      storage: 512Mi

Note that I explicitly set the namespace in my definitions. See also my comments re: forgetfulness.

Application, Service, and Ingress

I don't do anything fancy with these either. I use nginx-ingress as a reverse proxy, with cert-manager configured to uh... manage certs. One replica for the app because I only have one node in my cluster at the moment. I can bump this by one each time I add a K3s node, for additional resilience.

Note that I'm allowing signups from my own domain.

app.yaml:

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vaultwarden
  namespace: vaultwarden
  labels:
    app: vaultwarden
    env: production
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vaultwarden
      env: production
  template:
    metadata:
      labels: 
        app: vaultwarden
        env: production
    spec:
      containers:
      - name: vw
        image: "vaultwarden/server:latest"
        imagePullPolicy: Always
        volumeMounts:
        - name: data
          mountPath: /data
        env:
        - name: DOMAIN
          value: "https://lol.cooldomain.org"
        - name: DATABASE_URL
          value: "mysql://vaultwarden:[email protected]/vaultwarden"
        - name: SIGNUPS_DOMAINS_WHITELIST
          value: "cooldomain.org"
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: vaultwarden-data

svc.yaml:

---
apiVersion: v1
kind: Service
metadata:
  name: vaultwarden
  namespace: vaultwarden
spec:
  selector:
    app: vaultwarden
  ports:
  - protocol: TCP
    port: 80

ingress.yaml:

---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: vaultwarden-ingress
  namespace: vaultwarden
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    cert-manager.io/issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - "*.cooldomain.org"
    secretName: letsencrypt-prod
  rules:
  - host: lol.cooldomain.org
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: vaultwarden
            port:
              number: 80

kubectl apply -f these mfers and after a couple of minutes my Vaultwarden deployment is done. The last thing to do is set up a DNS entry for lol.cooldomain.org, pointing at my public IP. Once that was done I was able to go to https://lol.cooldomain.org/ (not the actual URL) and register my user. Awesome.

The actual, like, migration

This bit is hairy because it involves exporting your 1Password vault(s) to disk and then importing them into Vaultwarden. It's an easy process - export from the 1Password client, import through the Vaultwarden web UI (tools -> import data).

The hairy bit is that regardless of which format you choose to output to, your precious passwords are going to be stored in plain-text. It's absolutely critical that you only do this on a secure machine and that you remember to delete the files once you've finished importing and confirmed that everything is present and correct in Vaultwarden. My experience was that the only format that carried over the TOTP data was 1pif and even there I've noticed that I'm missing one or two. Nothing that I can't easily work around, but it's worth checking your most crucial services anyway. You might need to experiment if you're migrating from something other than 1Password.

Anyway, start to finish the whole process took me about an hour. Not bad really.