Migrate Bitwarden to rootless Podman with SELinux

This commit is contained in:
2026-09-06 16:10:48 -04:00
parent a1ef6e76a5
commit adda06628b
12 changed files with 374 additions and 58 deletions
+24
View File
@@ -17,11 +17,21 @@ users:
gid: 1001 gid: 1001
home: true home: true
system: true system: true
bitwarden:
uid: 1002
gid: 1002
home: true
system: true
root_gpgkeys: root_gpgkeys:
- name: kris@lamoureux.io - name: kris@lamoureux.io
id: 42A3A92C5DA0F3E5F71A3710105B748C1362EB96 id: 42A3A92C5DA0F3E5F71A3710105B748C1362EB96
fail2ban_ignoreip:
- ::1
- 127.0.0.1/8
- 10.89.0.0/16
############### ###############
#### proxy #### #### proxy ####
############### ###############
@@ -29,6 +39,8 @@ base_domain: local.krislamo.org
proxy: proxy:
servers: servers:
- domain: "vault.{{ base_domain }}"
proxy_pass: "http://127.0.0.1:9080"
- domain: "git.{{ base_domain }}" - domain: "git.{{ base_domain }}"
proxy_pass: "http://127.0.0.1:3000" proxy_pass: "http://127.0.0.1:3000"
@@ -36,6 +48,10 @@ proxy:
#### podman #### #### podman ####
################ ################
podman_compose: podman_compose:
bitwarden:
root: /srv/bitwarden
api_keepalive: true
compose: []
git: git:
root: /opt/oci root: /opt/oci
trusted_keys: trusted_keys:
@@ -58,6 +74,14 @@ podman_compose:
DB_USER: "{{ gitea.DB_USER }}" DB_USER: "{{ gitea.DB_USER }}"
DB_PASSWD: "{{ gitea.DB_PASSWD }}" DB_PASSWD: "{{ gitea.DB_PASSWD }}"
###################
#### bitwarden ####
###################
bitwarden:
domain: "vault.{{ base_domain }}"
install_id: 4ea840a3-532e-4cb6-a472-abd900728b23
install_key: 1yB3Z2gRI0KnnH90C6p
############### ###############
#### gitea #### #### gitea ####
############### ###############
+1
View File
@@ -8,4 +8,5 @@
- proxy - proxy
- mariadb - mariadb
- podman - podman
- bitwarden
- gitea - gitea
+32 -4
View File
@@ -1,8 +1,36 @@
bitwarden_name: bitwarden bitwarden_name: bitwarden
bitwarden_root: "/var/lib/{{ bitwarden_name }}" bitwarden_user: "{{ bitwarden_name }}"
bitwarden_logs_identity: "{{ bitwarden_root }}/bwdata/logs/identity/Identity" bitwarden_domain: "{{ bitwarden.domain }}"
bitwarden_logs_identity_date: "{{ ansible_date_time.year }}{{ ansible_date_time.month }}{{ ansible_date_time.day }}" bitwarden_install_id: "{{ bitwarden.install_id }}"
bitwarden_install_key: "{{ bitwarden.install_key }}"
bitwarden_puid: "{{ users[bitwarden_user].uid }}"
bitwarden_root: "{{ podman_compose[bitwarden_user].root }}"
bitwarden_data: "{{ bitwarden_root }}/bwdata"
bitwarden_script: "{{ bitwarden_root }}/bitwarden.sh"
bitwarden_logs: "{{ podman_log_root }}/{{ bitwarden_name }}"
bitwarden_logs_identity: "{{ bitwarden_logs }}/identity/Identity"
bitwarden_logs_identity_date: "{{ ansible_facts['date_time']['date'] | replace('-', '') }}"
bitwarden_database: "{{ bitwarden_name }}" bitwarden_database: "{{ bitwarden_name }}"
bitwarden_realips: "172.16.0.0/12" bitwarden_realips: "10.89.0.0/16"
bitwarden_standalone: false bitwarden_standalone: false
bitwarden_production: false bitwarden_production: false
bitwarden_http_port: "127.0.0.1:9080"
bitwarden_https_port: "127.0.0.1:9443"
bitwarden_se_level: "s0:c33,c333"
bitwarden_se_opt: "label=level:{{ bitwarden_se_level }}"
bitwarden_services:
- mssql
- web
- attachments
- api
- identity
- sso
- admin
- icons
- notifications
- events
- nginx
bitwarden_env:
XDG_RUNTIME_DIR: "/run/user/{{ bitwarden_puid }}"
DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ bitwarden_puid }}/bus"
DOCKER_HOST: "unix:///run/user/{{ bitwarden_puid }}/podman/podman.sock"
+17
View File
@@ -0,0 +1,17 @@
module bitwarden 1.0;
require {
type container_t;
type container_ro_file_t;
type oci_log_t;
class process ptrace;
class file { read getattr open lock ioctl write append create unlink setattr rename link map execmod };
class dir { read getattr search open lock ioctl write add_name remove_name create rmdir setattr rename reparent };
class lnk_file { read getattr create unlink rename };
}
allow container_t self:process ptrace;
allow container_t container_ro_file_t:file execmod;
allow container_t oci_log_t:dir { read getattr search open lock ioctl write add_name remove_name create rmdir setattr rename reparent };
allow container_t oci_log_t:file { read getattr open lock ioctl write append create unlink setattr rename link map };
allow container_t oci_log_t:lnk_file { read getattr create unlink rename };
+25 -10
View File
@@ -1,28 +1,43 @@
- name: Stop Bitwarden for rebuild - name: Stop Bitwarden for rebuild
ansible.builtin.service: ansible.builtin.systemd:
name: "{{ bitwarden_name }}" name: "{{ bitwarden_name }}"
state: stopped state: stopped
scope: user
become: true
become_user: "{{ bitwarden_user }}"
environment: "{{ bitwarden_env }}"
listen: rebuild_bitwarden listen: rebuild_bitwarden
- name: Rebuild Bitwarden - name: Rebuild Bitwarden
ansible.builtin.command: "{{ bitwarden_root }}/bitwarden.sh rebuild" ansible.builtin.command: "{{ bitwarden_script }} rebuild"
become: true
become_user: "{{ bitwarden_user }}"
environment: "{{ bitwarden_env }}"
changed_when: true
listen: rebuild_bitwarden
- name: Relabel Bitwarden's data directory
ansible.builtin.command: "restorecon -RF {{ bitwarden_root }}"
changed_when: true
when: selinux is defined and selinux is not false
listen: rebuild_bitwarden listen: rebuild_bitwarden
- name: Reload systemd manager configuration - name: Reload systemd manager configuration
ansible.builtin.systemd: ansible.builtin.systemd:
daemon_reload: true daemon_reload: true
scope: user
become: true
become_user: "{{ bitwarden_user }}"
environment: "{{ bitwarden_env }}"
listen: rebuild_bitwarden listen: rebuild_bitwarden
- name: Start Bitwarden after rebuild - name: Start Bitwarden after rebuild
ansible.builtin.service: ansible.builtin.systemd:
name: "{{ bitwarden_name }}" name: "{{ bitwarden_name }}"
state: started state: started
enabled: true enabled: true
scope: user
become: true
become_user: "{{ bitwarden_user }}"
environment: "{{ bitwarden_env }}"
listen: rebuild_bitwarden listen: rebuild_bitwarden
- name: Create Bitwarden's initial log file
ansible.builtin.file:
path: "{{ bitwarden_logs_identity }}/{{ bitwarden_logs_identity_date }}.txt"
state: touch
mode: "644"
listen: touch_bitwarden
+205 -21
View File
@@ -3,57 +3,206 @@
name: expect name: expect
state: present state: present
- name: Create Bitwarden's containers config directory
ansible.builtin.file:
path: "/home/{{ bitwarden_user }}/.config/containers"
state: directory
owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0755"
- name: Configure keep-id user namespace for Bitwarden's containers
ansible.builtin.copy:
content: |
[containers]
userns = "keep-id"
dest: "/home/{{ bitwarden_user }}/.config/containers/containers.conf"
owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0644"
notify: rebuild_bitwarden
- name: Set SELinux context on Bitwarden's root directory
community.general.sefcontext:
target: "{{ bitwarden_root }}(/.*)?"
setype: container_file_t
selevel: "{{ bitwarden_se_level }}"
state: present
when: selinux is defined and selinux is not false
- name: Set SELinux context on Bitwarden's logging directory
community.general.sefcontext:
target: "{{ bitwarden_logs }}(/.*)?"
setype: oci_log_t
selevel: "{{ bitwarden_se_level }}"
state: present
when: selinux is defined and selinux is not false
- name: Install Bitwarden SELinux policy source
ansible.builtin.copy:
src: bitwarden.te
dest: "{{ podman_selinux_dir }}/bitwarden.te"
owner: root
group: root
mode: "0644"
register: bitwarden_selinux_src
when: selinux is defined and selinux is not false
- name: Clear stale Bitwarden SELinux artifacts
ansible.builtin.file:
path: "{{ podman_selinux_dir }}/bitwarden.{{ item }}"
state: absent
loop: [mod, pp]
when:
- selinux is defined and selinux is not false
- bitwarden_selinux_src is changed
- name: Compile Bitwarden SELinux policy module
ansible.builtin.command:
cmd: >-
checkmodule -M -m -o {{ podman_selinux_dir }}/bitwarden.mod
{{ podman_selinux_dir }}/bitwarden.te
creates: "{{ podman_selinux_dir }}/bitwarden.mod"
when: selinux is defined and selinux is not false
- name: Package Bitwarden SELinux policy module
ansible.builtin.command:
cmd: >-
semodule_package -o {{ podman_selinux_dir }}/bitwarden.pp
-m {{ podman_selinux_dir }}/bitwarden.mod
creates: "{{ podman_selinux_dir }}/bitwarden.pp"
when: selinux is defined and selinux is not false
- name: List loaded SELinux policy modules for Bitwarden
ansible.builtin.command:
cmd: semodule -l
register: bitwarden_semodule_list
changed_when: false
when: selinux is defined and selinux is not false
- name: Load Bitwarden SELinux policy module
ansible.builtin.command:
cmd: semodule -i {{ podman_selinux_dir }}/bitwarden.pp
register: bitwarden_semodule
changed_when: bitwarden_semodule.rc == 0
when:
- selinux is defined and selinux is not false
- bitwarden_selinux_src is changed or "bitwarden" not in bitwarden_semodule_list.stdout_lines
notify: rebuild_bitwarden
- name: Create Bitwarden directory - name: Create Bitwarden directory
ansible.builtin.file: ansible.builtin.file:
path: "{{ bitwarden_root }}" path: "{{ bitwarden_root }}"
state: directory state: directory
mode: "755" owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0700"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ bitwarden_se_level }}"
- name: Check whether the Bitwarden script is already installed
ansible.builtin.stat:
path: "{{ bitwarden_script }}"
register: bitwarden_script_stat
- name: Download Bitwarden script - name: Download Bitwarden script
ansible.builtin.get_url: ansible.builtin.get_url:
url: "https://raw.githubusercontent.com/\ url: "https://raw.githubusercontent.com/\
bitwarden/self-host/master/bitwarden.sh" bitwarden/self-host/master/bitwarden.sh"
dest: "{{ bitwarden_root }}" dest: "{{ bitwarden_script }}"
mode: u+x force: false
owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0750"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ bitwarden_se_level }}"
when: not bitwarden_script_stat.stat.exists
- name: Install Bitwarden script wrapper - name: Install Bitwarden script wrapper
ansible.builtin.template: ansible.builtin.template:
src: bw_wrapper.j2 src: bw_wrapper.j2
dest: "{{ bitwarden_root }}/bw_wrapper" dest: "{{ bitwarden_root }}/bw_wrapper"
mode: u+x owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0750"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ bitwarden_se_level }}"
- name: Pin the SELinux level on newly downloaded run scripts
ansible.builtin.blockinfile:
path: "{{ bitwarden_script }}"
insertafter: '^\s*mv \$tmp_script \$SCRIPTS_DIR/run\.sh$'
marker: " # {mark} Ansible Managed: Add Bitwarden SELinux MCS level"
block: |2
sed -i \
"s|--name setup |&--security-opt {{ bitwarden_se_opt }} |g" \
"$SCRIPTS_DIR/run.sh"
when: selinux is defined and selinux is not false
- name: Give Bitwarden's setup container a writable working directory
ansible.builtin.blockinfile:
path: "{{ bitwarden_script }}"
insertafter: '^\s*mv \$tmp_script \$SCRIPTS_DIR/run\.sh$'
marker: " # {mark} Ansible Managed: Set Bitwarden's setup directory"
block: |2
sed -i \
"s|-it --rm --name setup |&-w /bitwarden |" \
"$SCRIPTS_DIR/run.sh"
- name: Run Bitwarden installation script - name: Run Bitwarden installation script
ansible.builtin.command: "{{ bitwarden_root }}/bw_wrapper" ansible.builtin.command: "{{ bitwarden_root }}/bw_wrapper"
args: args:
creates: "{{ bitwarden_root }}/bwdata/config.yml" creates: "{{ bitwarden_data }}/config.yml"
become: true
become_user: "{{ bitwarden_user }}"
environment: "{{ bitwarden_env }}"
- name: Pin the SELinux level on Bitwarden's setup container
ansible.builtin.replace:
path: "{{ bitwarden_data }}/scripts/run.sh"
regexp: "--name setup(?! --security-opt)"
replace: "--name setup --security-opt label=level:{{ bitwarden_se_level }}"
when: selinux is defined and selinux is not false
notify: rebuild_bitwarden
- name: Install compose override - name: Install compose override
ansible.builtin.template: ansible.builtin.template:
src: compose.override.yml.j2 src: compose.override.yml.j2
dest: "{{ bitwarden_root }}/bwdata/docker/docker-compose.override.yml" dest: "{{ bitwarden_data }}/docker/docker-compose.override.yml"
mode: "644" owner: "{{ bitwarden_user }}"
when: bitwarden_override | default(true) group: "{{ bitwarden_user }}"
mode: "0640"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('container_file_t', omit) }}
selevel: "{{ bitwarden_se_level }}"
notify: rebuild_bitwarden notify: rebuild_bitwarden
- name: Disable bitwarden-nginx HTTP on 80 - name: Disable bitwarden-nginx HTTP on 80
ansible.builtin.replace: ansible.builtin.replace:
path: "{{ bitwarden_root }}/bwdata/config.yml" path: "{{ bitwarden_data }}/config.yml"
regexp: "^http_port: 80$" regexp: "^http_port: 80$"
replace: "http_port: {{ bitwarden_http_port | default('127.0.0.1:9080') }}" replace: "http_port: {{ bitwarden_http_port }}"
when: not bitwarden_standalone when: not bitwarden_standalone
notify: rebuild_bitwarden notify: rebuild_bitwarden
- name: Disable bitwarden-nginx HTTPS on 443 - name: Disable bitwarden-nginx HTTPS on 443
ansible.builtin.replace: ansible.builtin.replace:
path: "{{ bitwarden_root }}/bwdata/config.yml" path: "{{ bitwarden_data }}/config.yml"
regexp: "^https_port: 443$" regexp: "^https_port: 443$"
replace: "https_port: {{ bitwarden_https_port | default('127.0.0.1:9443') }}" replace: "https_port: {{ bitwarden_https_port }}"
when: not bitwarden_standalone when: not bitwarden_standalone
notify: rebuild_bitwarden notify: rebuild_bitwarden
- name: Disable Bitwarden managed Lets Encrypt - name: Disable Bitwarden managed Lets Encrypt
ansible.builtin.replace: ansible.builtin.replace:
path: "{{ bitwarden_root }}/bwdata/config.yml" path: "{{ bitwarden_data }}/config.yml"
regexp: "^ssl_managed_lets_encrypt: true$" regexp: "^ssl_managed_lets_encrypt: true$"
replace: "ssl_managed_lets_encrypt: false" replace: "ssl_managed_lets_encrypt: false"
when: not bitwarden_standalone or not bitwarden_production when: not bitwarden_standalone or not bitwarden_production
@@ -61,7 +210,7 @@
- name: Disable Bitwarden managed SSL - name: Disable Bitwarden managed SSL
ansible.builtin.replace: ansible.builtin.replace:
path: "{{ bitwarden_root }}/bwdata/config.yml" path: "{{ bitwarden_data }}/config.yml"
regexp: "^ssl: true$" regexp: "^ssl: true$"
replace: "ssl: false" replace: "ssl: false"
when: not bitwarden_standalone when: not bitwarden_standalone
@@ -69,25 +218,60 @@
- name: Define reverse proxy servers - name: Define reverse proxy servers
ansible.builtin.lineinfile: ansible.builtin.lineinfile:
path: "{{ bitwarden_root }}/bwdata/config.yml" path: "{{ bitwarden_data }}/config.yml"
line: "- {{ bitwarden_realips }}" line: "- {{ bitwarden_realips }}"
insertafter: "^real_ips" insertafter: "^real_ips"
notify: rebuild_bitwarden notify: rebuild_bitwarden
- name: Create user systemd directory for Bitwarden
ansible.builtin.file:
path: "/home/{{ bitwarden_user }}/.config/systemd/user"
state: directory
owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0755"
- name: Install Bitwarden systemd service - name: Install Bitwarden systemd service
ansible.builtin.template: ansible.builtin.template:
src: bitwarden.service.j2 src: bitwarden.service.j2
dest: "/etc/systemd/system/{{ bitwarden_name }}.service" dest: "/home/{{ bitwarden_user }}/.config/systemd/user/{{ bitwarden_name }}.service"
mode: "644" owner: "{{ bitwarden_user }}"
register: bitwarden_systemd group: "{{ bitwarden_user }}"
mode: "0644"
notify: rebuild_bitwarden notify: rebuild_bitwarden
- name: Create Bitwarden's initial logging directory - name: Create Bitwarden's initial logging directory
ansible.builtin.file: ansible.builtin.file:
path: "{{ bitwarden_logs_identity }}" path: "{{ bitwarden_logs_identity }}"
state: directory state: directory
mode: "755" owner: "{{ bitwarden_user }}"
notify: touch_bitwarden group: "{{ bitwarden_user }}"
mode: "0755"
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('oci_log_t', omit) }}
selevel: "{{ bitwarden_se_level }}"
- name: Create Bitwarden's log file for Fail2ban
ansible.builtin.file:
path: "{{ bitwarden_logs_identity }}/{{ bitwarden_logs_identity_date }}.txt"
state: touch
owner: "{{ bitwarden_user }}"
group: "{{ bitwarden_user }}"
mode: "0644"
modification_time: preserve
access_time: preserve
setype: >-
{{ (selinux is defined and selinux is not false)
| ternary('oci_log_t', omit) }}
selevel: "{{ bitwarden_se_level }}"
- name: Install Bitwarden's Fail2ban filter
ansible.builtin.template:
src: fail2ban-filter.conf.j2
dest: /etc/fail2ban/filter.d/bitwarden.conf
mode: "0644"
notify: restart_fail2ban
- name: Install Bitwarden's Fail2ban jail - name: Install Bitwarden's Fail2ban jail
ansible.builtin.template: ansible.builtin.template:
+12 -6
View File
@@ -1,13 +1,19 @@
[Unit] [Unit]
Description=Bitwarden Password Manager Server Description=Bitwarden Password Manager Server
PartOf=docker.service Requires=podman.socket
After=docker.service After=podman.socket
StartLimitIntervalSec=300
StartLimitBurst=5
[Service] [Service]
Type=oneshot Type=exec
RemainAfterExit=true RemainAfterExit=true
ExecStart={{ bitwarden_root }}/bitwarden.sh start WorkingDirectory={{ bitwarden_root }}
ExecStop={{ bitwarden_root }}/bitwarden.sh stop Environment=DOCKER_HOST=unix://%t/podman/podman.sock
ExecStart={{ bitwarden_script }} start
ExecStop={{ bitwarden_script }} stop
Restart=on-failure
RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=default.target
@@ -1,16 +1,16 @@
version: '3' # {{ ansible_managed }}
services: services:
nginx: {% for service in bitwarden_services %}
networks: {{ service }}:
- traefik {% if service == 'nginx' %}
labels: user: "0:0"
traefik.http.routers.bitwarden.rule: "Host(`{{ bitwarden_domain }}`)" {% endif %}
traefik.http.routers.bitwarden.entrypoints: {{ bitwarden_entrypoint | default('web') }} {% if service == 'identity' %}
traefik.http.routers.bitwarden.tls: {{ bitwarden_traefik_tls | default('false') }} environment:
traefik.http.services.bitwarden.loadbalancer.server.port: 8080 globalSettings__knownNetworks: "{{ bitwarden_realips }}"
traefik.docker.network: traefik volumes:
traefik.enable: "true" - {{ bitwarden_logs }}/identity:/etc/bitwarden/logs
networks: {% endif %}
traefik: security_opt:
external: true - label=level:{{ bitwarden_se_level }}
{% endfor %}
@@ -0,0 +1,4 @@
# {{ ansible_managed }}
[Definition]
failregex = Failed login attempt.*IpAddress: "<ADDR>"
ignoreregex =
@@ -2,7 +2,7 @@
[bitwarden] [bitwarden]
enabled = true enabled = true
filter = bitwarden filter = bitwarden
logpath = {{ bitwarden_root }}/bwdata/logs/identity/Identity/* logpath = {{ bitwarden_logs_identity }}/*
maxretry = 10 maxretry = 10
findtime = 3600 findtime = 3600
bantime = 900 bantime = 900
+29 -1
View File
@@ -30,6 +30,30 @@
group: "{{ podman_user }}" group: "{{ podman_user }}"
mode: "0755" mode: "0755"
- name: Create podman API service override directory for user
ansible.builtin.file:
path: "/home/{{ podman_user }}/.config/systemd/user/podman.service.d"
state: directory
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: "0755"
when: podman_compose_config.api_keepalive | default(false)
- name: Keep the podman API service alive for slow compose runs
ansible.builtin.copy:
content: |
# {{ lookup('ansible.builtin.config', 'DEFAULT_MANAGED_STR') }}
[Service]
ExecStart=
ExecStart=/usr/bin/podman $LOGGING system service --time=0
dest: >-
/home/{{ podman_user }}/.config/systemd/user/podman.service.d/timeout.conf
owner: "{{ podman_user }}"
group: "{{ podman_user }}"
mode: "0644"
when: podman_compose_config.api_keepalive | default(false)
notify: podman_compose_systemd
- name: Install docker compose (podman) systemd service for user - name: Install docker compose (podman) systemd service for user
ansible.builtin.template: ansible.builtin.template:
src: compose.service.j2 src: compose.service.j2
@@ -76,7 +100,11 @@
group: "{{ podman_user }}" group: "{{ podman_user }}"
mode: "0600" mode: "0600"
state: present state: present
when: podman_project is defined become: true
become_user: "{{ podman_user }}"
when:
- podman_project is defined
- podman_project | length > 0
tags: podman_compose tags: podman_compose
- name: Import trusted GPG keys for docker compose (podman) projects - name: Import trusted GPG keys for docker compose (podman) projects
+9
View File
@@ -3,6 +3,15 @@
name: ["podman", "docker-cli", "docker-compose"] name: ["podman", "docker-cli", "docker-compose"]
state: present state: present
- name: Configure NSS subid lookups for rootless containers
ansible.builtin.lineinfile:
path: /etc/nsswitch.conf
line: "subid: files"
regexp: "^subid:"
owner: root
group: root
mode: "0644"
- name: Allow rootless containers to use capabilities in their user namespace - name: Allow rootless containers to use capabilities in their user namespace
ansible.posix.seboolean: ansible.posix.seboolean:
name: container_use_userns_all_caps name: container_use_userns_all_caps