diff --git a/dev/host_vars/vps.yml b/dev/host_vars/vps.yml index 8deb737..a89dff2 100644 --- a/dev/host_vars/vps.yml +++ b/dev/host_vars/vps.yml @@ -17,11 +17,21 @@ users: gid: 1001 home: true system: true + bitwarden: + uid: 1002 + gid: 1002 + home: true + system: true root_gpgkeys: - name: kris@lamoureux.io id: 42A3A92C5DA0F3E5F71A3710105B748C1362EB96 +fail2ban_ignoreip: + - ::1 + - 127.0.0.1/8 + - 10.89.0.0/16 + ############### #### proxy #### ############### @@ -29,6 +39,8 @@ base_domain: local.krislamo.org proxy: servers: + - domain: "vault.{{ base_domain }}" + proxy_pass: "http://127.0.0.1:9080" - domain: "git.{{ base_domain }}" proxy_pass: "http://127.0.0.1:3000" @@ -36,6 +48,10 @@ proxy: #### podman #### ################ podman_compose: + bitwarden: + root: /srv/bitwarden + api_keepalive: true + compose: [] git: root: /opt/oci trusted_keys: @@ -58,6 +74,14 @@ podman_compose: DB_USER: "{{ gitea.DB_USER }}" DB_PASSWD: "{{ gitea.DB_PASSWD }}" +################### +#### bitwarden #### +################### +bitwarden: + domain: "vault.{{ base_domain }}" + install_id: 4ea840a3-532e-4cb6-a472-abd900728b23 + install_key: 1yB3Z2gRI0KnnH90C6p + ############### #### gitea #### ############### diff --git a/dev/vps.yml b/dev/vps.yml index 47fce69..5959179 100644 --- a/dev/vps.yml +++ b/dev/vps.yml @@ -8,4 +8,5 @@ - proxy - mariadb - podman + - bitwarden - gitea diff --git a/roles/bitwarden/defaults/main.yml b/roles/bitwarden/defaults/main.yml index 8e03a1c..474327b 100644 --- a/roles/bitwarden/defaults/main.yml +++ b/roles/bitwarden/defaults/main.yml @@ -1,8 +1,36 @@ bitwarden_name: bitwarden -bitwarden_root: "/var/lib/{{ bitwarden_name }}" -bitwarden_logs_identity: "{{ bitwarden_root }}/bwdata/logs/identity/Identity" -bitwarden_logs_identity_date: "{{ ansible_date_time.year }}{{ ansible_date_time.month }}{{ ansible_date_time.day }}" +bitwarden_user: "{{ bitwarden_name }}" +bitwarden_domain: "{{ bitwarden.domain }}" +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_realips: "172.16.0.0/12" +bitwarden_realips: "10.89.0.0/16" bitwarden_standalone: 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" diff --git a/roles/bitwarden/files/bitwarden.te b/roles/bitwarden/files/bitwarden.te new file mode 100644 index 0000000..c41ffb7 --- /dev/null +++ b/roles/bitwarden/files/bitwarden.te @@ -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 }; diff --git a/roles/bitwarden/handlers/main.yml b/roles/bitwarden/handlers/main.yml index 38d39dd..0c093c6 100644 --- a/roles/bitwarden/handlers/main.yml +++ b/roles/bitwarden/handlers/main.yml @@ -1,28 +1,43 @@ - name: Stop Bitwarden for rebuild - ansible.builtin.service: + ansible.builtin.systemd: name: "{{ bitwarden_name }}" state: stopped + scope: user + become: true + become_user: "{{ bitwarden_user }}" + environment: "{{ bitwarden_env }}" listen: 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 - name: Reload systemd manager configuration ansible.builtin.systemd: daemon_reload: true + scope: user + become: true + become_user: "{{ bitwarden_user }}" + environment: "{{ bitwarden_env }}" listen: rebuild_bitwarden - name: Start Bitwarden after rebuild - ansible.builtin.service: + ansible.builtin.systemd: name: "{{ bitwarden_name }}" state: started enabled: true + scope: user + become: true + become_user: "{{ bitwarden_user }}" + environment: "{{ bitwarden_env }}" 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 diff --git a/roles/bitwarden/tasks/main.yml b/roles/bitwarden/tasks/main.yml index 83855cf..53f82fa 100644 --- a/roles/bitwarden/tasks/main.yml +++ b/roles/bitwarden/tasks/main.yml @@ -3,57 +3,206 @@ name: expect 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 ansible.builtin.file: path: "{{ bitwarden_root }}" 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 ansible.builtin.get_url: url: "https://raw.githubusercontent.com/\ - bitwarden/self-host/master/bitwarden.sh" - dest: "{{ bitwarden_root }}" - mode: u+x + bitwarden/self-host/master/bitwarden.sh" + dest: "{{ bitwarden_script }}" + 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 ansible.builtin.template: src: bw_wrapper.j2 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 ansible.builtin.command: "{{ bitwarden_root }}/bw_wrapper" 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 ansible.builtin.template: src: compose.override.yml.j2 - dest: "{{ bitwarden_root }}/bwdata/docker/docker-compose.override.yml" - mode: "644" - when: bitwarden_override | default(true) + dest: "{{ bitwarden_data }}/docker/docker-compose.override.yml" + owner: "{{ bitwarden_user }}" + 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 - name: Disable bitwarden-nginx HTTP on 80 ansible.builtin.replace: - path: "{{ bitwarden_root }}/bwdata/config.yml" + path: "{{ bitwarden_data }}/config.yml" 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 notify: rebuild_bitwarden - name: Disable bitwarden-nginx HTTPS on 443 ansible.builtin.replace: - path: "{{ bitwarden_root }}/bwdata/config.yml" + path: "{{ bitwarden_data }}/config.yml" 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 notify: rebuild_bitwarden - name: Disable Bitwarden managed Lets Encrypt ansible.builtin.replace: - path: "{{ bitwarden_root }}/bwdata/config.yml" + path: "{{ bitwarden_data }}/config.yml" regexp: "^ssl_managed_lets_encrypt: true$" replace: "ssl_managed_lets_encrypt: false" when: not bitwarden_standalone or not bitwarden_production @@ -61,7 +210,7 @@ - name: Disable Bitwarden managed SSL ansible.builtin.replace: - path: "{{ bitwarden_root }}/bwdata/config.yml" + path: "{{ bitwarden_data }}/config.yml" regexp: "^ssl: true$" replace: "ssl: false" when: not bitwarden_standalone @@ -69,25 +218,60 @@ - name: Define reverse proxy servers ansible.builtin.lineinfile: - path: "{{ bitwarden_root }}/bwdata/config.yml" + path: "{{ bitwarden_data }}/config.yml" line: "- {{ bitwarden_realips }}" insertafter: "^real_ips" 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 ansible.builtin.template: src: bitwarden.service.j2 - dest: "/etc/systemd/system/{{ bitwarden_name }}.service" - mode: "644" - register: bitwarden_systemd + dest: "/home/{{ bitwarden_user }}/.config/systemd/user/{{ bitwarden_name }}.service" + owner: "{{ bitwarden_user }}" + group: "{{ bitwarden_user }}" + mode: "0644" notify: rebuild_bitwarden - name: Create Bitwarden's initial logging directory ansible.builtin.file: path: "{{ bitwarden_logs_identity }}" state: directory - mode: "755" - notify: touch_bitwarden + owner: "{{ bitwarden_user }}" + 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 ansible.builtin.template: diff --git a/roles/bitwarden/templates/bitwarden.service.j2 b/roles/bitwarden/templates/bitwarden.service.j2 index fab45ce..3938283 100644 --- a/roles/bitwarden/templates/bitwarden.service.j2 +++ b/roles/bitwarden/templates/bitwarden.service.j2 @@ -1,13 +1,19 @@ [Unit] Description=Bitwarden Password Manager Server -PartOf=docker.service -After=docker.service +Requires=podman.socket +After=podman.socket +StartLimitIntervalSec=300 +StartLimitBurst=5 [Service] -Type=oneshot +Type=exec RemainAfterExit=true -ExecStart={{ bitwarden_root }}/bitwarden.sh start -ExecStop={{ bitwarden_root }}/bitwarden.sh stop +WorkingDirectory={{ bitwarden_root }} +Environment=DOCKER_HOST=unix://%t/podman/podman.sock +ExecStart={{ bitwarden_script }} start +ExecStop={{ bitwarden_script }} stop +Restart=on-failure +RestartSec=10 [Install] -WantedBy=multi-user.target +WantedBy=default.target diff --git a/roles/bitwarden/templates/compose.override.yml.j2 b/roles/bitwarden/templates/compose.override.yml.j2 index f7bffe1..1b80827 100644 --- a/roles/bitwarden/templates/compose.override.yml.j2 +++ b/roles/bitwarden/templates/compose.override.yml.j2 @@ -1,16 +1,16 @@ -version: '3' - +# {{ ansible_managed }} services: - nginx: - networks: - - traefik - labels: - traefik.http.routers.bitwarden.rule: "Host(`{{ bitwarden_domain }}`)" - traefik.http.routers.bitwarden.entrypoints: {{ bitwarden_entrypoint | default('web') }} - traefik.http.routers.bitwarden.tls: {{ bitwarden_traefik_tls | default('false') }} - traefik.http.services.bitwarden.loadbalancer.server.port: 8080 - traefik.docker.network: traefik - traefik.enable: "true" -networks: - traefik: - external: true +{% for service in bitwarden_services %} + {{ service }}: +{% if service == 'nginx' %} + user: "0:0" +{% endif %} +{% if service == 'identity' %} + environment: + globalSettings__knownNetworks: "{{ bitwarden_realips }}" + volumes: + - {{ bitwarden_logs }}/identity:/etc/bitwarden/logs +{% endif %} + security_opt: + - label=level:{{ bitwarden_se_level }} +{% endfor %} diff --git a/roles/bitwarden/templates/fail2ban-filter.conf.j2 b/roles/bitwarden/templates/fail2ban-filter.conf.j2 new file mode 100644 index 0000000..e56dbc2 --- /dev/null +++ b/roles/bitwarden/templates/fail2ban-filter.conf.j2 @@ -0,0 +1,4 @@ +# {{ ansible_managed }} +[Definition] +failregex = Failed login attempt.*IpAddress: "" +ignoreregex = diff --git a/roles/bitwarden/templates/fail2ban-jail.conf.j2 b/roles/bitwarden/templates/fail2ban-jail.conf.j2 index 6e85e00..7f35a63 100644 --- a/roles/bitwarden/templates/fail2ban-jail.conf.j2 +++ b/roles/bitwarden/templates/fail2ban-jail.conf.j2 @@ -2,7 +2,7 @@ [bitwarden] enabled = true filter = bitwarden -logpath = {{ bitwarden_root }}/bwdata/logs/identity/Identity/* +logpath = {{ bitwarden_logs_identity }}/* maxretry = 10 findtime = 3600 bantime = 900 diff --git a/roles/podman/tasks/deploy.yml b/roles/podman/tasks/deploy.yml index dd9335e..a1feac0 100644 --- a/roles/podman/tasks/deploy.yml +++ b/roles/podman/tasks/deploy.yml @@ -30,6 +30,30 @@ group: "{{ podman_user }}" 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 ansible.builtin.template: src: compose.service.j2 @@ -76,7 +100,11 @@ group: "{{ podman_user }}" mode: "0600" 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 - name: Import trusted GPG keys for docker compose (podman) projects diff --git a/roles/podman/tasks/main.yml b/roles/podman/tasks/main.yml index a0ccd94..dcb2b24 100644 --- a/roles/podman/tasks/main.yml +++ b/roles/podman/tasks/main.yml @@ -3,6 +3,15 @@ name: ["podman", "docker-cli", "docker-compose"] 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 ansible.posix.seboolean: name: container_use_userns_all_caps