Bitwarden: an organization’s password manager self-hosted version installation on an AWS EC2

By | 05/01/2019

We consider Bitwarden as a passwords keeper for our project with the main goal to have an ability to have separated access to secrets by user roles and/or ACLs.

I.e. Pass or KeePass are good for self-usage by one person but they have no main things – a normal web-interface and role-based access to data. There are 1Password/LastPass of course, but they keep data on their own servers which is not too good for me.

Bitwarden is Opensource and can be used as a Cloud-based version or can be installed on your own server.

It has personal Free-version and paid with additional features.

Besides the personal usage, it can be used for Business with user roles – will try it later.

The home page is here>>>.

The main things I did like in Bitwarden:

  • has desktop applications for для Linux, macOS, Windows
  • all browsers extensions
  • applications for Android and iOS
  • RESTful API (in Enterprise version), i.e. theoretically can be used from Jenkins to populate its secrets
  • has CLI utilities
  • MFA authorization
  • roles/groups based access (Enterprise version)
  • File Storage
  • data import from other passwords managers (Chrome, KeePass, 1Password etc)

Quick installation documentation available here here>>> and full – here>>>.

In this post, Bitwarden will be installed on an AWS EC2 instance with additional EBS volume mounted to /bitwarden where Bitwarden will store its data and which will be backed up by  AWS Data Lifecycle Manager.

On the EC2 will have NGINX running as a frontend and SSL sessions with a certificate from Let’s Encrypt will be terminated here.

Although Bitwarden is running in a Docker Compose stack with its own NGINX and Let’s Encrypt certificates support – I’ll do it in a more traditional way, i.e. NGINX on a host will proxy requests to the NGINX in the Bitwarden’s stack, and this NGINX in its turn will proxy requests to its internal services.


Creating EC2

Will use Debian here. AMIs can be found here>>>.

At first, I started t3.nano but this wasn’t enough – an instance just hangs up after starting Bitwarden which is not surprising knowing that fact that it uses MSSQL and has 9 containers running. And Bitwarden itself is  Bitwarden is .NET application written in C#.

Run an EC2 with the t3.medium type:

aws --profile bm-backend  ec2 run-instances --region eu-west-1 --image-id ami-01820e22b83de8d0d --key-name setevoy-testing --instance-type t3.medium --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=bitwarden-dev}]'

Get its Availability Zone:

aws --profile bm-backend ec2 describe-instances --region eu-west-1 --filters "Name=tag:Name,Values=bitwarden-dev" --query "Reservations[*].Instances[*].[Placement.AvailabilityZone]" --output text

Creating EBS

Create an EBS volume (by default the standard i.e. HDD will be used, if you want to have SSD – add --volume-type gp2):

aws --profile bm-backend ec2 create-volume --region eu-west-1 --availability-zone eu-west-1a --size 5 --tag-specifications 'ResourceType=volume,Tags=[{Key=Name,Value=bitwarden-dev-ebs}]'

Here we set the same region  (--region eu-west-1) and the same Availability Zone (--availability-zone eu-west-1a) where the ЕС2 was started and the disk size is 5 GiB.

Get this EBS ID:

aws --profile bm-backend ec2 describe-volumes --region eu-west-1  --filters "Name=tag:Name,Values=bitwarden-dev-ebs" --query "Volumes[*].VolumeId" --output text

And ID of the EC2:

aws --profile bm-backend ec2 describe-instances --region eu-west-1 --filters "Name=tag:Name,Values=bitwarden-dev" --query "Reservations[*].Instances[*].InstanceId" --output text

Attache this EBS to the EC2:

aws --profile bm-backend ec2 attach-volume --region eu-west-1 --volume-id vol-0621e68897eb2a3d8 --instance-id i-0ac18e298768e2c4b --device xvdb

Security Group

Create a Security Group – here via WebUI to make it quickly.

During creating this SG pay attention on a VPC ID – must be the same as used for your EC2:

Allow access to the 80 port from anywhere to make Let’s Encrypt authorization working.

443 and 22 ports allowed from our office only.

Attache this SG to the EC2 – Networking > Change Security Group:

Mounting EBS

Log in to the server:

ssh admin@ -i setevoy-testing-eu-west-1.pem

Check disks:

admin@ip-172-31-36-249:~$ lsblk
nvme0n1     259:0    0   8G  0 disk
└─nvme0n1p1 259:1    0   8G  0 part /
nvme1n1     259:2    0   5G  0 disk

nvme1n1 is our EBS.

Create /bitwarden directory:

admin@ip-172-31-36-249:~$ sudo -s
root@ip-172-31-36-249:/home/admin# mkdir /bitwarden

Create a partition on the /dev/nvme1n1:

root@ip-172-31-36-249:/home/admin# sgdisk -n 1 /dev/nvme1n1
Creating new GPT entries.
The operation has completed successfully.

Create a file system:

root@ip-172-31-36-249:/home/admin# mkfs.ext4 /dev/nvme1n1p1

Check partitions now:

root@ip-172-31-36-249:/home/admin# fdisk /dev/nvme1n1
Device         Start      End  Sectors Size Type
/dev/nvme1n1p1  2048 10485726 10483679   5G Linux filesystem

Mount it to the /bitwarden:

root@ip-172-31-36-249:/home/admin# mount /dev/nvme1n1p1 /bitwarden/
root@ip-172-31-36-249:/home/admin# ls -l /bitwarden/
total 16
drwx------ 2 root root 16384 Apr 30 10:15 lost+found

Get partition’s ID:

root@ip-172-31-36-249:/home/admin# blkid /dev/nvme1n1p1
/dev/nvme1n1p1: UUID="5e3972d4-c742-4224-80d6-8239e5201ae1" TYPE="ext4" PARTUUID="929f264c-ac03-4f9f-9071-056c1511de0e"

Using this UUID add a new mount point to the /etc/fstab with the --nofail option:

root@ip-172-31-36-249:/home/admin# cat /etc/fstab
UUID=3866caa4-0449-4478-899b-60eb6f71dd26       /       ext4    rw,discard,errors=remount-ro    0       1
UUID="5e3972d4-c742-4224-80d6-8239e5201ae1" /bitwarden ext4 nofail 0 0

Unmount partition mounted manually:

root@ip-172-31-36-249:/home/admin# umount /bitwarden/

And mount it back using fstab:

root@ip-172-31-36-249:/home/admin# mount -a


root@ip-172-31-36-249:/home/admin# findmnt /bitwarden/
/bitwarden /dev/nvme1n1p1 ext4   rw,relatime,data=ordered

Can reboot instance now to check mount works properly now.

Also, install all updates:

root@ip-172-31-36-249:/home/admin# apt update && apt -y upgrade && reboot


Create a domain name to be used:

The host’s set up

Let’s Encrypt

Install client:

root@ip-172-31-36-249:/home/admin# apt install -y git
root@ip-172-31-36-249:/home/admin# git clone /opt/letsencrypt

Get a certificate using standalone authenticator:

root@ip-172-31-36-249:/home/admin# /opt/letsencrypt/letsencrypt-auto certonly -d
Saving debug log to /var/log/letsencrypt/letsencrypt.log
How would you like to authenticate with the ACME CA?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: Spin up a temporary webserver (standalone)
2: Place files in webroot directory (webroot)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 1
Plugins selected: Authenticator standalone, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at You must
agree in order to register with the ACME server at
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for
Waiting for verification...
Cleaning up challenges
- Congratulations! Your certificate and chain have been saved at:
Your key file has been saved at:


Install NGINX:

root@ip-172-31-36-249:/home/admin# apt -y install nginx

Generate a key for the SSL:

root@ip-172-31-36-249:/home/admin# openssl dhparam -out /etc/nginx/dhparams.pem 2048

Add a virtual host’s config – /etc/nginx/conf.d/

server {
    listen 80;
    # Lets Encrypt Webroot
    location ~ /.well-known {
        root /var/www/html;
        allow all;
    location / {
        # office1
        allow 194.***.***.24/29;
        # office2
        allow 91.***.***.78/32;
        # arseny home
        allow 188.***.***.48/32;
        deny  all;
        return 301;

server {

    listen       443 ssl;

    root /var/www/html;

    access_log  /var/log/nginx/;
    error_log /var/log/nginx/ warn;
    # office1
    allow 194.***.***.24/29;
    # office2
    allow 91.***.***.78/32;
    # arseny home
    allow 188.***.***.48/32;
    deny  all;
    ssl_certificate /etc/letsencrypt/live/;
    ssl_certificate_key /etc/letsencrypt/live/;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_prefer_server_ciphers on;
    ssl_dhparam /etc/nginx/dhparams.pem;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;

    location / {
        proxy_pass http://localhost:8000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        client_max_body_size 0;
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
        add_header Referrer-Policy "same-origin";

Check its syntax and reload configs:

root@ip-172-31-36-249:/home/admin# nginx  -t && systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful


root@ip-172-31-36-249:/home/admin# curl -vL
* Connected to ( port 80 (#0)
< HTTP/1.1 301 Moved Permanently
< Location:
curl: (7) Failed to connect to port 443: Connection timed out

All good.

Connection timed out – as we have no backend running yet.

Docker and Docker Compose

To run Bitwarden need to have Docker and Docker Compose – install them.


root@ip-172-31-36-249:/home/admin# curl -L | bash


root@ip-172-31-36-249:/home/admin# docker -v
Docker version 18.09.5, build e8ff056dbc

Add the admin user to the docker group:

root@ip-172-31-36-249:/home/admin# usermod -aG docker admin

Install Docker Compose:

root@ip-172-31-36-249:/home/admin# curl -L "$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
root@ip-172-31-36-249:/home/admin# chmod +x /usr/local/bin/docker-compose
root@ip-172-31-36-249:/home/admin# docker-compose -v
docker-compose version 1.24.0, build 0aa59064

Bitwarden installation

Go to the and get keys:

Each Bitwarden needs to have own set of those keys.

Download installation, configuration, and management script:

root@ip-172-31-36-249:/home/admin# cd /bitwarden/
root@ip-172-31-36-249:/bitwarden# curl -s -o
root@ip-172-31-36-249:/bitwarden# chmod +x

This script will download files from the repository and then will call the script with the install option.

All options for the

Command Description
install Start the installer.
start Start all containers.
restart Restart all containers (same as start).
stop Stop all containers.
updatedb Update/initialize the database.
update Update all containers and the database.
updateself Update this main script.
rebuild Rebuild generated installation assets from config.yml.

Start installation:

root@ip-172-31-36-249:/bitwarden# ./ install
_     _ _                         _
| |__ (_) |___      ____ _ _ __ __| | ___ _ __
| '_ \| | __\ \ /\ / / _` | '__/ _` |/ _ \ '_ \
| |_) | | |_ \ V  V / (_| | | | (_| |  __/ | | |
|_.__/|_|\__| \_/\_/ \__,_|_|  \__,_|\___|_| |_|
(!) Enter the domain name for your Bitwarden instance (ex.
(!) Do you want to use Let's Encrypt to generate a free SSL certificate? (y/n): n
1.30.1: Pulling from bitwarden/setup
Status: Downloaded newer image for bitwarden/setup:1.30.1
(!) Enter your installation id (get at 46ec2f0b-***-***-aa3f00b8ab41
(!) Enter your installation key: OJ0***fDD
(!) Do you have a SSL certificate to use? (y/n): y
(!) Is this a trusted SSL certificate (requires ca.crt, see docs)? (y/n): y
Generating key for IdentityServer.
Generating a RSA private key
writing new private key to 'identity.key'
Building nginx config.
Building docker environment files.
Building docker environment override files.
Building FIDO U2F app id.
Building docker-compose.yml.
Installation complete
Next steps, run:
`./ start`

Bitwarden configuration

The script will save all Bitwarden’s data to the bwdata directory:

root@ip-172-31-36-249:/bitwarden# ll
total 24
-rwxr-xr-x  1 root   root     2535 Apr 30 11:07
drwxr-xr-x 11 nobody nogroup  4096 Apr 30 11:13 bwdata
root@ip-172-31-36-249:/bitwarden# ll bwdata/
total 40
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:10 ca-certificates
-rw-r--r-- 1 nobody nogroup 3323 Apr 30 11:13 config.yml
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:13 docker
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:13 env
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:13 identity
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:10 letsencrypt
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:13 nginx
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:10 scripts
drwxr-xr-x 3 nobody nogroup 4096 Apr 30 11:13 ssl
drwxr-xr-x 2 nobody nogroup 4096 Apr 30 11:13 web

The main configuration file is the bwdata/config.yml.

Also, in the ./bwdata/env/global.override.env file additional variables can be set, will check them a bit later.

Docker Compose stack will be started using the ./bwdata/docker/docker-compose.yml file:

version: '3'

    image: bitwarden/mssql:1.30.1
    container_name: bitwarden-mssql
    restart: always
      - ../mssql/data:/var/opt/mssql/data
      - ../logs/mssql:/var/opt/mssql/log
      - ../mssql/backups:/etc/bitwarden/mssql/backups
      - mssql.env
      - ../env/uid.env
      - ../env/mssql.override.env

    image: bitwarden/web:2.10.0
    container_name: bitwarden-web
    restart: always
      - ../web:/etc/bitwarden/web
      - global.env
      - ../env/uid.env

Update the config.yml file – disable SSL as we have own NGINX with SSL and change HTTP and HTTPS ports:

# Docker compose file port mapping for HTTP. Leave empty to remove the port mapping.
# Learn more:
http_port: 8000
# Docker compose file port mapping for HTTPS. Leave empty to remove the port mapping.
# Learn more:
https_port: 8001
# Configure Nginx for SSL.
ssl: false

Update applications configs:

./ rebuild

Start Bitwarden:

root@ip-172-31-36-249:/bitwarden# ./ start
Bitwarden is up and running!

Check in a browser:

Check containers:

root@ip-172-31-36-249:/bitwarden# docker ps
CONTAINER ID        IMAGE                            COMMAND             CREATED             STATUS              PORTS                                                    NAMES
b196ee0f81ff        bitwarden/nginx:1.30.1           "/"    About an hour ago   Up About an hour    80/tcp,>8080/tcp,>8443/tcp   bitwarden-nginx
ef03f591491d        bitwarden/admin:1.30.1           "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-admin
d4fa88921cce        bitwarden/api:1.30.1             "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-api
408c5f0bd370        bitwarden/notifications:1.30.1   "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-notifications
9bec10bc09d8        bitwarden/icons:1.30.1           "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-icons
f87789cc4da4        bitwarden/mssql:1.30.1           "/"    About an hour ago   Up About an hour    1433/tcp                                                 bitwarden-mssql
143370f979c5        bitwarden/web:2.10.0             "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-web
acdc220a7c29        bitwarden/identity:1.30.1        "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-identity
925d047b6321        bitwarden/attachments:1.30.1     "/"    About an hour ago   Up About an hour    5000/tcp                                                 bitwarden-attachments

In those containers, Bitwarden will mount directories from the host, for example mssql:

root@ip-172-31-36-249:/bitwarden# docker inspect bitwarden-mssql | jq .[].Mounts
"Type": "bind",
"Source": "/bitwarden/bwdata/logs/mssql",
"Destination": "/var/opt/mssql/log",
"Mode": "rw",
"RW": true,
"Propagation": "rprivate"
"Type": "bind",
"Source": "/bitwarden/bwdata/mssql/data",
"Destination": "/var/opt/mssql/data",
"Mode": "rw",
"RW": true,
"Propagation": "rprivate"
"Type": "bind",
"Source": "/bitwarden/bwdata/mssql/backups",
"Destination": "/etc/bitwarden/mssql/backups",
"Mode": "rw",
"RW": true,
"Propagation": "rprivate"

Thus for a backup will be enough just to store the bwdata catalog.

Email configuration

Email settings are set in the bwdata/env/global.override.env file.

We will use AWS SES, update variables:


Restart Bitwarden (rebuild needs to be done only after changes in the config.yml):

root@ip-172-31-36-249:/bitwarden# ./ restart

In case of email sending problems – check logs in the bwdata/logs/api/Api/ directory or from the bitwarden-api container

root@ip-172-31-36-249:/bitwarden# docker logs -f bitwarden-api

Registration in the Bitwarden

Now you can register in your Bitwarden installation.

Click on the Create account:

Click Confirm.

Bitwarden Admin and users

Add an administrator mailbox to the bwdata/env/global.override.env file in the adminSettings__admins= field.

Note, that the documentation says:

These admin email addresses do not need to be registered with an account on your Bitwarden installation

After logging in with this mailbox – you’ll get an email with a link to proceed authorization to the admin page. This link will be valid for 15 minutes:


Restart service:

root@ip-172-31-36-249:/bitwarden# ./ restart

And go to the page:

Log in with the mailbox specified in the adminSettings__admins, get an email, go by the link:

Users settings

Log in using common Log In form and will see usual userspace:

In the Tools you can import data from various passwords managers like KeePass:

Adding passwords manually:

Getting a password:

Working with Bitwarden

Chrome plugin

Install Chrome extension from the Chrome webstore:

Click on the Settings:

Set your server’s URL:

Log in:

And get all your passwords directly from a browser:

Also, it will suggest storing passwords during logins as a usual passwords manager:

Linux desktop

I guess it has clients for any Linux-based systems

In Arch Linux can be installed from AUR:

yaourt -S bitwarden-bin

And log in in the same way as in the Chrome extension:

Import from KeePass

Let’s check how import is working

In your KeePass export data to an XML file:

Then go to the Bitwarden – Tools > Import data:

Ready – even with directories structure:

Export can be done in the same way – you can upload data from Bitwarden in a JSON or CSV file and the CSV can be imported to a local KeePass. Such an additional backup.

Keep in mind that the exported file will have all passwords unencrypted.

Multi-factor authorization

MFA can be configured in My account – Two-step Login, everything in a standard way here:

Backuping and restoring Bitwarden storage

Nobody wants to lose an organization’s all passwords so let’s check how backup and restore will works.

As we have /bitwarden mounted from a dedicated EBS volume then it can be daily snapshotted by AWS Data Lifecycle Manager and then in case of problems – a new volume can be created and mounted to a new EC2 instance with a new Bitwarden installation.

So steps to check are, quickly:

  1. create a snapshot manually
  2. create a new EBS using this snapshot
  3. start a new ЕС2
  4. attach this EBS and mount it to the /bitwarden
  5. obtain a Let’s Encrypt Certificate
  6. install NGINX, set up a virtual host
  7. install Docker, Docker Compose
  8. if a domain was changed – update /bitwarden/bwdata/config.yml, change the url parameter
  9. run ./ rebuild
  10. run ./ start
  11. Profit!

That’s all for now.

When will get a trial license – will play with user’s and roles.