OS Password Apps: Vaultwarden and TeamPass

Published: May 16, 2024 by Isaac Johnson

Teampass has been around a long time. Its current “Teampass 3” version comes from Nils Laumaille.

Vaultwarden used to be Bitwarden_rs, the open-source Rust version of Bitwarden, but they changed names to avoid confusion.

Vaultwarden

There is a Linux version of Bitwarden but I’ll start with the Rust version; Vaultwarden.

Installation

Let’s start simple and just launch in Docker as the README suggests

We first pull the container image

builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ docker pull vaultwarden/server:latest
latest: Pulling from vaultwarden/server
e1caac4eb9d2: Pull complete
64e9ba731906: Pull complete
41f741e60b13: Pull complete
bed54ec34ad8: Pull complete                                                                                                                            127b9165754a: Pull complete
ce64d427e15f: Pull complete
Digest: sha256:edb8e2bab9cbca22e555638294db9b3657ffbb6e5d149a29d7ccdb243e3c71e0
Status: Downloaded newer image for vaultwarden/server:latest
docker.io/vaultwarden/server:latest

Then we can make a data dir and run

builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ mkdir data
builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ pwd
/home/builder/Workspaces/vaultwarden
builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ docker run -d --name vaultwarden -v /home/builder/Workspaces/vaultwarden/data/:/data/ --restart unless-stopped -p 8885:80 vaultwarden/server:latest
25a1c639e09c9ddb80be11552dcace7ffded0b4dc478f91ffdfe7751d51fd08c

/content/images/2024/05/vaultwarden-01.png

I don’t know why I got hung up on this, but it seems obvious now; the first time through make sure to “create account”

/content/images/2024/05/vaultwarden-02.png

I can now login and start to create passwords

/content/images/2024/05/vaultwarden-03.png

I’ll create one secret

/content/images/2024/05/vaultwarden-04.png

I saved a couple just so I could verify the volume is what is holding our data

builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ docker run -d --name vaultwarden -v /home/builder/Workspaces/vaultwarden/data/:/data/ --restart unless-stopped -p 8885:80 vaultwarden/server:latest
25a1c639e09c9ddb80be11552dcace7ffded0b4dc478f91ffdfe7751d51fd08c
builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ ls -ltra ./data/
total 360
drwxr-xr-x 3 builder builder   4096 May  8 07:20 ..
-rw-r--r-- 1 root    root      1679 May  8 07:21 rsa_key.pem
drwxr-xr-x 2 root    root      4096 May  8 07:21 tmp
drwxr-xr-x 2 root    root      4096 May  8 07:21 sends
-rw-r--r-- 1 root    root       451 May  8 07:21 rsa_key.pub.pem
drwxr-xr-x 2 root    root      4096 May  8 07:21 attachments
-rw-r--r-- 1 root    root    249856 May  8 07:21 db.sqlite3
drwxr-xr-x 6 builder builder   4096 May  8 07:21 .
-rw-r--r-- 1 root    root     49472 May  8 07:30 db.sqlite3-wal
-rw-r--r-- 1 root    root     32768 May  8 07:30 db.sqlite3-shm
drwxr-xr-x 2 root    root      4096 May  8 07:30 icon_cache
builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ ls -ltra ./data/
total 380
drwxr-xr-x 3 builder builder   4096 May  8 07:20 ..
-rw-r--r-- 1 root    root      1679 May  8 07:21 rsa_key.pem
drwxr-xr-x 2 root    root      4096 May  8 07:21 tmp
drwxr-xr-x 2 root    root      4096 May  8 07:21 sends
-rw-r--r-- 1 root    root       451 May  8 07:21 rsa_key.pub.pem
drwxr-xr-x 2 root    root      4096 May  8 07:21 attachments
-rw-r--r-- 1 root    root    249856 May  8 07:21 db.sqlite3
drwxr-xr-x 6 builder builder   4096 May  8 07:21 .
drwxr-xr-x 2 root    root      4096 May  8 07:30 icon_cache
-rw-r--r-- 1 root    root     70072 May  8 07:30 db.sqlite3-wal
-rw-r--r-- 1 root    root     32768 May  8 07:30 db.sqlite3-shm

I think I want to move on to hosting in a Dockerhost or Kubernetes so I’ll stop this local instance

builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ docker ps
CONTAINER ID   IMAGE                          COMMAND                  CREATED          STATUS                    PORTS                              NAMES
25a1c639e09c   vaultwarden/server:latest      "/start.sh"              10 minutes ago   Up 10 minutes (healthy)   3012/tcp, 0.0.0.0:8885->80/tcp     vaultwarden
4b36604209fe   harness/gitness                "/app/gitness server"    7 months ago     Up 12 days                0.0.0.0:3000->3000/tcp, 3001/tcp   gitness
a613dc3843bd   ghcr.io/ajnart/homarr:latest   "docker-entrypoint.s…"   9 months ago     Up 12 days                0.0.0.0:7575->7575/tcp             homarr
cead637ea0a2   louislam/uptime-kuma:1         "/usr/bin/dumb-init …"   2 years ago      Up 12 days (healthy)      0.0.0.0:3001->3001/tcp             uptime-kuma
builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ docker stop 25a1c639e09c
25a1c639e09c

I’m mostly curious if everything needed is self-contained in that data dir.

I’ll tgz that dir

builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ tar -czvf vault-backup.tgz ./data/
./data/
./data/rsa_key.pem
./data/db.sqlite3-shm
./data/rsa_key.pub.pem
./data/sends/
./data/db.sqlite3-wal
./data/attachments/
./data/tmp/
./data/db.sqlite3
./data/icon_cache/
./data/icon_cache/example.com.png.miss

I’ll now copy it out to my dockerhost

builder@DESKTOP-QADGF36:~/Workspaces/vaultwarden$ sftp builder@192.168.1.100
Connected to 192.168.1.100.
sftp> put vault-backup.tgz
Uploading vault-backup.tgz to /home/builder/vault-backup.tgz
vault-backup.tgz                                                                                                     100%   13KB  12.2MB/s   00:00
sftp> exit

I’ll ssh over there and expand the archive into a folder

builder@builder-T100:~$ mkdir vaultwarden
builder@builder-T100:~$ cd vaultwarden/
builder@builder-T100:~/vaultwarden$ mv ../vault-backup.tgz ./
builder@builder-T100:~/vaultwarden$ tar -xzvf vault-backup.tgz
./data/
./data/rsa_key.pem
./data/db.sqlite3-shm
./data/rsa_key.pub.pem
./data/sends/
./data/db.sqlite3-wal
./data/attachments/
./data/tmp/
./data/db.sqlite3
./data/icon_cache/
./data/icon_cache/example.com.png.miss
builder@builder-T100:~/vaultwarden$ ls
data  vault-backup.tgz

I’ll now fire it up on the dockerhost with the same data dir. Will it preserve my logins and passwords?

builder@builder-T100:~/vaultwarden$ docker run -d -v /home/builder/vaultwarden/data/:/data/ -p 8885:80 --restart unless-stopped vaultwarden/server:latest
Unable to find image 'vaultwarden/server:latest' locally
latest: Pulling from vaultwarden/server
e1caac4eb9d2: Pull complete
64e9ba731906: Pull complete
41f741e60b13: Pull complete
bed54ec34ad8: Pull complete
127b9165754a: Pull complete
ce64d427e15f: Pull complete
Digest: sha256:edb8e2bab9cbca22e555638294db9b3657ffbb6e5d149a29d7ccdb243e3c71e0
Status: Downloaded newer image for vaultwarden/server:latest
7b4e266119d8180d18c935b24f4818cf294ef15d0985724de39f3d9fd92efbea

I tried to login but got errors

/content/images/2024/05/vaultwarden-05.png

I tried Edge, Chrome, Firefox and Opera so I think this is more an App setting than actual browser requirement

I might have to move forward with exposing via K8s next just to solve this requirement as a bit of searching doesn’t find a way to disable this.

To do that, I’ll need to add an A record which I can do in Azure DNS:

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n vaultwarden
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "e2adec5f-0f71-4a21-9d85-f0aa4b9b68cf",
  "fqdn": "vaultwarden.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/vaultwarden",
  "name": "vaultwarden",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "type": "Microsoft.Network/dnszones/A"
}

Next, to forward traffic, I can use an Endpoint, EndpointSlice or ExternalName service paired with an Ingress object using that DNS name

---
apiVersion: v1
kind: Service
metadata:
  annotations:
  name: vaultwarden-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: vaultwarden
    port: 80
    protocol: TCP
    targetPort: 8885
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
  name: vaultwarden-external-ip
subsets:
- addresses:
  - ip: 192.168.1.100
  ports:
  - name: vaultwarden
    port: 8885
    protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  name: vaultwarden-ingress
  namespace: default
spec:
  rules:
  - host: vaultwarden.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: vaultwarden-external-ip
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - vaultwarden.tpk.pw
    secretName: vaultwarden-tls

Which we can apply

$ kubectl apply -f ./k8s.yaml
service/vaultwarden-external-ip created
endpoints/vaultwarden-external-ip created
ingress.networking.k8s.io/vaultwarden-ingress created

Success!

/content/images/2024/05/vaultwarden-06.png

I can see it worked as well by decrypting the sample password I set earlier

/content/images/2024/05/vaultwarden-07.png

Backups

This is great, but I sure would love to know I could survive a Dockerhost crash

I have an NFS mounto my NAS on the Docker host I can use with a crontab entry

45 3 * * * tar -zcvf /mnt/filestation/vaultbackups.$(date '+\%Y-\%m-\%d_\%Hh\%Mm').tgz /home/builder/vaultwarden/data

I can see the results of an example run:

$ ls -ltrah /mnt/filestation | grep vaultbackups
-rwxrwxrwx 1 1024 users  13K May  9 18:48 vaultbackups.2024-05-09_18h48m.tgz

Which I can verify on the newer NAS

/content/images/2024/05/vaultwarden-08.png

TeamPass

I can launch with Docker

builder@builder-T100:~$ docker run --name teampass.tpk.pw --restart always --publish-all -p 828:80 -p 428:443 --hostname=teampass.tpk.pw -e VIRTUAL_HOST=teampass.tpk.pw -e LETSENCRYPT_EMAIL=isaac.johnson@gmail.com -e LETSENC
RYPT_HOST=teampass.tpk.pw -d valterseu/teampass
Unable to find image 'valterseu/teampass:latest' locally
latest: Pulling from valterseu/teampass
bccd10f490ab: Pull complete
57e0c9a8808f: Pull complete
831c963e883d: Pull complete
b239094bad30: Pull complete
23135f025a42: Pull complete
f3583531b47a: Pull complete
66c986ff0e6f: Pull complete
1ffc013fd825: Pull complete
4e2d7a1e68c5: Pull complete
7eb43968caba: Pull complete
5af4aff5e32b: Pull complete
4ebd599c73d1: Pull complete
bb1b755643d9: Pull complete
a67508c36c32: Pull complete
df06b420e338: Pull complete
5c7037a78132: Pull complete
071e52a8d8db: Pull complete
f963415304ef: Pull complete
131a851cddfb: Pull complete
ae95c1871556: Pull complete
5f91f03eacb0: Pull complete
5a5797c446cb: Pull complete
Digest: sha256:6813bd83470314942f660325b05eb76b1cb5128a26382fdc068fa4939f747225
Status: Downloaded newer image for valterseu/teampass:latest
4eeefb21214f2530806c429735a6aa9050f96f7b94c11632f589a8d0561912ac

I can then move to installation

/content/images/2024/05/vaultwarden-09.png

On to the next step

/content/images/2024/05/vaultwarden-10.png

Our next step is to create a MaryiaDB/MySql database. In my case, I’m using a MariaDB 10 instance on my NAS.

ijohnson@sirnasilot:~$ mysql -u root -p
Enter password:
Welcome to the MariaDB monitor.  Commands end with ; or \g.
Your MariaDB connection id is 3686403
Server version: 10.11.2-MariaDB Source distribution

Copyright (c) 2000, 2018, Oracle, MariaDB Corporation Ab and others.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

MariaDB [(none)]> CREATE DATABASE teampassdb CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_unicode_ci';
Query OK, 1 row affected (0.001 sec)

MariaDB [(none)]> create user 'teampass'@'%' identified by 'teampass';
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements: [Minimal password length 10, Include mixed case, Include numeric characters, Include special characters, Exclude name of user from password]
MariaDB [(none)]> create user 'teampass'@'%' identified by 'Te49__00@';
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements: [Minimal password length 10]
MariaDB [(none)]> create user 'teampass'@'%' identified by 'Te49__00@44444';
Query OK, 0 rows affected (0.384 sec)

MariaDB [(none)]> grant all privileges on teampassdb.* to 'teampass'@'%';
Query OK, 0 rows affected (0.331 sec)

MariaDB [(none)]> FLUSH PRIVILEGES;
Query OK, 0 rows affected (0.045 sec)

MariaDB [(none)]> \q
Bye

Which works when tested

/content/images/2024/05/vaultwarden-11.png

I’ll move on to creating a password

/content/images/2024/05/vaultwarden-12.png

The next step prepares the database

/content/images/2024/05/vaultwarden-13.png

The “Preparing database” step takes quite a while. I had to tell Chrome to wait a few times

/content/images/2024/05/vaultwarden-14.png

Our last step is Finalization

/content/images/2024/05/vaultwarden-15.png

We can then finish and go to the home page

/content/images/2024/05/vaultwarden-16.png

We return to a home page

/content/images/2024/05/vaultwarden-17.png

We can login with ‘admin’ and the passwords we used in the setup

/content/images/2024/05/vaultwarden-18.png

I can go to users and add a user with “+New”

/content/images/2024/05/vaultwarden-19.png

I’ll then create an admin user for myself

/content/images/2024/05/vaultwarden-20.png

Once created I can set the login password for the user with the gear dropdown

/content/images/2024/05/vaultwarden-21.png

which popped up a warning. Overall this is pretty slow

/content/images/2024/05/vaultwarden-22.png

When it does work, I can see a Reset page

/content/images/2024/05/vaultwarden-23.png

It will display it when done

/content/images/2024/05/vaultwarden-24.png

I could use this to login

/content/images/2024/05/vaultwarden-25.png

Much as before, I’ll go ahead and create an A Record in Azure

$ az account set --subscription "Pay-As-You-Go" && az network dns record-set a add-record -g idjdnsrg -z tpk.pw -a 75.73.224.240 -n teampass
{
  "ARecords": [
    {
      "ipv4Address": "75.73.224.240"
    }
  ],
  "TTL": 3600,
  "etag": "d6781cc8-fe87-4f63-822d-dcd6f4908be2",
  "fqdn": "teampass.tpk.pw.",
  "id": "/subscriptions/d955c0ba-13dc-44cf-a29a-8fed74cbb22d/resourceGroups/idjdnsrg/providers/Microsoft.Network/dnszones/tpk.pw/A/teampass",
  "name": "teampass",
  "provisioningState": "Succeeded",
  "resourceGroup": "idjdnsrg",
  "targetResource": {},
  "type": "Microsoft.Network/dnszones/A"
}

I can now fire up an ingress, service and endpoint

$ cat ./k8s-teampass.yaml
---
apiVersion: v1
kind: Service
metadata:
  annotations:
  name: teampass-external-ip
spec:
  clusterIP: None
  clusterIPs:
  - None
  internalTrafficPolicy: Cluster
  ipFamilies:
  - IPv4
  - IPv6
  ipFamilyPolicy: RequireDualStack
  ports:
  - name: teampass
    port: 80
    protocol: TCP
    targetPort: 828
  sessionAffinity: None
  type: ClusterIP
---
apiVersion: v1
kind: Endpoints
metadata:
  annotations:
  name: teampass-external-ip
subsets:
- addresses:
  - ip: 192.168.1.100
  ports:
  - name: teampass
    port: 828
    protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  annotations:
    cert-manager.io/cluster-issuer: azuredns-tpkpw
    ingress.kubernetes.io/ssl-redirect: "true"
    kubernetes.io/ingress.class: nginx
    kubernetes.io/tls-acme: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
  name: teampass-ingress
  namespace: default
spec:
  rules:
  - host: teampass.tpk.pw
    http:
      paths:
      - backend:
          service:
            name: teampass-external-ip
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - teampass.tpk.pw
    secretName: teampass-tls

$ kubectl apply -f ./k8s-teampass.yaml
service/teampass-external-ip created
endpoints/teampass-external-ip created
ingress.networking.k8s.io/teampass-ingress created

When I can see the cert is created

$ kubectl get cert teampass-tls
NAME           READY   SECRET         AGE
teampass-tls   True    teampass-tls   116s

I can go to the URL https://teampass.tpk.pw/

/content/images/2024/05/vaultwarden-26.png

And login

/content/images/2024/05/vaultwarden-27.png

The Backups page doesnt work

/content/images/2024/05/vaultwarden-28.png

I can then update some of the settings like the server URL

/content/images/2024/05/vaultwarden-29.png

Also, to disable the “Maintenance Mode”, we can do that in settings now too

/content/images/2024/05/vaultwarden-30.png

The UI is a bit odd here. After it saves, it is disabled. You won’t see that immediately, but if you refresh the page, then it shows “No”

/content/images/2024/05/vaultwarden-31.png

Perhaps you are noticing, “Hey, where do we set passwords?” We don’t really see anything to that end on the left

/content/images/2024/05/vaultwarden-32.png

That’s because “Admins” are just for administering the site. We have to use a “User” account to actually to CRUD passwords.

I’ll create a new user ‘isaac.fb’ and also indicate I want a top level folder as well

/content/images/2024/05/vaultwarden-33.png

I can set a password, logout and then try and login with the user

/content/images/2024/05/vaultwarden-34.png

The “user” level has far fewer options on the left. However, we now see a section on the right to add “New Items”

/content/images/2024/05/vaultwarden-35.png

However, I still need to wait to access a personal folder

/content/images/2024/05/vaultwarden-36.png

While I wait for folders to get created, I can see an alert. I’m prompted to download my recovery keys

/content/images/2024/05/vaultwarden-37.png

When I click download, I get a warning

/content/images/2024/05/vaultwarden-38.png

But it does download the keys

/content/images/2024/05/vaultwarden-39.png

I grew impatient waiting on my personal folder so I logged in as admin in a different window and created an “OurKeys” folder under /Root

/content/images/2024/05/vaultwarden-40.png

By default, however, the “Default” role cannot view any newly created folder.

I’ll make two more quick changes, I’ll edit the role to be able to edit visible items

/content/images/2024/05/vaultwarden-41.png

Next, I’ll edit the OurKeys folder to change from the default “No Access” to “Write”

/content/images/2024/05/vaultwarden-42.png

I can now see that is exposed to our Default role

/content/images/2024/05/vaultwarden-43.png

As the user I’ll now also add a folder under the OurKeys called “MyFolder”

/content/images/2024/05/vaultwarden-44.png

I had to log out and in again as the user to get the permissions to work to add to the main “OurKeys” folder.

Let’s create a password and view it:

Summary

We looked at VaultWarden and TeamPass today. We set them up with Docker before pivoting on to Kubernetes. In both cases, I opted to keep the hosting in a Dockerhost and forward traffic with an Endpoint. In the case of VaultWarden, we also looked at creating a crontab entry to backup the data dir and save it to durable storage using an NFS Mount to a NAS.

Both tools could solve the needs for storing and sharing passwords with a team. I think I lean a bit towards the VaultWarden app because not only was it easy to backup and restore passwords (which I tested), but it was also quite fast having been rewritten in Rust.

Vaultwarden Bitwarden Teampass OpenSource Containers Kubernetes

Have something to add? Feedback? You can use the feedback form

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes