Making the Vaultwarden ADMIN_TOKEN secure with an Argon2 hash
Introduction
When you open the Vaultwarden admin page, the following warning is sometimes displayed.
You are using a plain text `ADMIN_TOKEN` which is insecure.
Please generate a secure Argon2 PHC string by using `vaultwarden hash` or `argon2`.Since Vaultwarden 1.28.0, hashing ADMIN_TOKEN with Argon2 is recommended. In this article I explain how to configure ADMIN_TOKEN securely in a Docker Compose environment.
Configuration before the change
My environment uses Traefik
docker-compose.yml (before)
version: "3.6"
services:
bitwarden:
environment:
- DOMAIN=https://vault.example.com
- TZ=Asia/Tokyo
- SIGNUPS_ALLOWED=false
- INVITATIONS_ALLOWED=false
- ADMIN_TOKEN=your_plain_text_admin_token_here
- WEBSOCKET_ENABLED=true
image: vaultwarden/server:latest
restart: always
volumes:
- ./bw_data:/data
networks:
- traefik-network
networks:
traefik-network:
external: trueWith this setup, ADMIN_TOKEN is written directly in docker-compose.yml as plain text, which is a security problem.
Configuration after the change
1. docker-compose.yml (after)
Change it so that the environment variables are managed in a .env file.
version: "3.6"
services:
bitwarden:
env_file:
- .env
image: vaultwarden/server:latest
restart: always
volumes:
- ./bw_data:/data
networks:
- traefik-network
networks:
traefik-network:
external: true2. Creating the .env file
# Vaultwarden environment variables
DOMAIN=https://vault.example.com
TZ=Asia/Tokyo
SIGNUPS_ALLOWED=false
INVITATIONS_ALLOWED=false
ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$$YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY
DISABLE_ADMIN_TOKEN=false
WEBSOCKET_ENABLED=trueImportant: In a Docker Compose .env file, $ has to be escaped as $$.
3. Setting up .gitignore
The .env file contains sensitive information, so configure Git not to track it.
*
!docker-compose.yml
!.envHow to generate the Argon2 hash
Method 1: Use the vaultwarden hash command
# Run it inside the running container
docker exec -it bitwarden-bitwarden-1 /vaultwarden hash
# Or run it in a temporary container
docker run --rm -it vaultwarden/server:latest /vaultwarden hashWhen you run the command, a prompt asks you to enter the password twice.
Generate an Argon2id PHC string using the 'bitwarden' preset:
Password:
Confirm password:
ADMIN_TOKEN='$argon2id$v=19$m=65540,t=3,p=4$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY'Method 2: Use the OWASP recommended settings
To generate a stronger hash, use the --preset owasp option.
docker exec -it bitwarden-bitwarden-1 /vaultwarden hash --preset owaspWriting it into the .env file
When you put the generated PHC string into the .env file, replace $ with $$.
Generated hash (example):
$argon2id$v=19$m=65540,t=3,p=4$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYWhat you write in the .env file:
ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$$YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYChecking config.json
When you save settings from the Vaultwarden admin page, bw_data/config.json is generated. If admin_token exists in that file it takes precedence over the environment variable, so this needs attention.
Removing admin_token from config.json
# Check the current setting
cat bw_data/config.json | jq '.admin_token // "not set"'
# Remove admin_token
jq 'del(.admin_token)' bw_data/config.json > config.json.tmp && mv config.json.tmp bw_data/config.jsonRestarting the container
Important: When you change environment variables, docker compose restart may not apply them. You need to recreate the container.
# Recreate the container (recommended)
docker compose down && docker compose up -d
# Check that the environment variable is set correctly
docker compose exec bitwarden env | grep ADMIN_TOKENIf you forget the password, or want to reset it
If you forget the admin page password or want to set it again, you can deal with it in the following steps.
Step 1: Temporarily change the .env file back to plain text
# Edit the .env file
ADMIN_TOKEN=new_temporary_password_hereStep 2: Remove admin_token from config.json
If an old hash remains in config.json it takes precedence over the environment variable, so remove it.
jq 'del(.admin_token)' bw_data/config.json > config.json.tmp && mv config.json.tmp bw_data/config.jsonStep 3: Recreate the container
docker compose down && docker compose up -dStep 4: Log in to the admin page with the new password
Open the admin page (https://vault.example.com/admin) in a browser and log in with the password you set in step 1.
Step 5: Generate a new hash
docker exec -it bitwarden-bitwarden-1 /vaultwarden hashEnter the new password and copy the generated PHC string.
Step 6: Update the .env file
Write the generated hash into the .env file (replacing $ with $$).
ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$$YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYStep 7: Recreate the container
docker compose down && docker compose up -dThat sets an ADMIN_TOKEN hashed from the new password.
Troubleshooting
If the warning does not disappear
- Check config.json: check whether
admin_tokenis still there in plain text - Check the escaping: check whether
$is escaped as$$in the.envfile - Recreate the container: run
docker compose down && docker compose up -d
If you cannot log in
Check the environment variable:
docker compose exec bitwarden env | grep ADMIN_TOKENRecreate the container:
docker compose down && docker compose up -dCheck the logs:
docker compose logs bitwarden --tail 30 | grep -i "admin\|token"
Summary
| Item | Before | After |
|---|---|---|
| Managing environment variables | Written directly in docker-compose.yml | Managed in a .env file |
| ADMIN_TOKEN | Plain text | Argon2 hash |
| Security | Low | High |
With the Argon2 hash, even if ADMIN_TOKEN were to leak, recovering the original password becomes difficult.
An operational best practice: enable the admin page only when you need it
The Admin Page allows powerful configuration and operational actions, but by the same token it is a place attackers are likely to target. If you do not normally do admin work, I recommend disabling the Admin page and enabling it temporarily only when you really need it.
Flow for temporarily enabling the admin page
- Set
ADMIN_TOKENtemporarily in the.envfile - Once the admin work is finished, comment out
ADMIN_TOKENor leave it empty to disable it - Apply it with
docker compose down && docker compose up -dif necessary
Example: a .env that temporarily enables the admin page
# Enabled only while doing admin work
ADMIN_TOKEN=$$argon2id$$v=19$$m=65540,t=3,p=4$$XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX$$YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYAfter the admin work (disabled)
# ADMIN_TOKEN= ← comment it out or leave it blankPoints to keep in mind
- If you remove
ADMIN_TOKENor leave it blank and restart, the admin page becomes inaccessible - Enabling it only when you need it keeps the risk to a minimum
- For real operation, managing the permissions of the
.envfile itself and controlling access to it are also strongly recommended
This kind of “enable it only when you need it” practice is an effective security measure for all sorts of services, not just Vaultwarden.