diff --git a/README.md b/README.md index c15642c..cadb677 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,9 @@ concise area of concern. - [`openproject`](roles/openproject/README.md): Deploys an [openproject.org](https://www.openproject.org) installation using the upstream provided docker-compose setup. +- [`vaultwarden`](roles/vaultwarden/README.md): Deploy [vaultwarden](https://github.com/dani-garcia/vaultwarden/), + an open-source implementation of the Bitwarden Server (formerly Bitwarden\_RS). + - [`vouch_proxy`](roles/vouch_proxy/README.md): Deploys [vouch-proxy](https://github.com/vouch/vouch-proxy), an authorization proxy for arbitrary webapps working with `nginx`s' `auth_request` module. diff --git a/galaxy.yml b/galaxy.yml index 54511d7..246b29d 100644 --- a/galaxy.yml +++ b/galaxy.yml @@ -18,4 +18,5 @@ tags: - gitea - hedgedoc - jellyfin + - vaultwarden - docker diff --git a/playbooks/vaultwarden.yml b/playbooks/vaultwarden.yml new file mode 100644 index 0000000..24ac3e5 --- /dev/null +++ b/playbooks/vaultwarden.yml @@ -0,0 +1,6 @@ +--- +- name: Install and configure vaultwarden + hosts: "{{ vaultwarden_hosts | default('vaultwarden') }}" + become: "{{ vaultwarden_become | default(true, false) }}" + roles: + - role: finallycoffee.services.vaultwarden diff --git a/roles/vaultwarden/README.md b/roles/vaultwarden/README.md new file mode 100644 index 0000000..158af82 --- /dev/null +++ b/roles/vaultwarden/README.md @@ -0,0 +1,34 @@ +# `finallycoffee.services.vaultwarden` ansible role + +## Configuration + +To use this role, the following variables need to be populated: + +- `vaultwarden_config_domain` - always. Changing this will lead to two-factor not working for two-factor methods registered in the past. +- `vaultwarden_config_admin_token` - if `vaultwarden_config_disable_admin_token` is `false`. + +### Email + +Configure mailing by first enabling SMTP using `vaultwarden_config_enable_smtp: true`, then configure your email server like this: +```yaml +vaultwarden_config: + smtp_host: "mail.example.com" + smtp_explicit_tls: true + smtp_port: 465 + smtp_from: "noreply+vaultwarden@example.com" + smtp_from_name: "'Example.com Vaultwarden instance' " + smtp_username: vaultwarden@example.com + smtp_password: i_hope_i_will_be_a_strong_one! + helo_name: "{{ vaultwarden_config_domain }}" +``` + +### 2FA via email + +To enable email-based two-factor-authentication, set `vaultwarden_config_enable_email_2fa: true` and optionally set the following configuration: +```yaml +vaultwarden_config: + email_token_size: 8 + email_expiration_time: 300 # 300 seconds = 5min + email_attempts_limit: 3 +``` + diff --git a/roles/vaultwarden/defaults/main/config.yml b/roles/vaultwarden/defaults/main/config.yml new file mode 100644 index 0000000..3d48824 --- /dev/null +++ b/roles/vaultwarden/defaults/main/config.yml @@ -0,0 +1,68 @@ +--- +# Required configuration +vaultwarden_config_domain: ~ +vaultwarden_config_admin_token: ~ +# Invitations and signups +vaultwarden_config_invitations_allowed: false +vaultwarden_config_invitation_org_name: ~ +vaultwarden_config_signups_allowed: false +vaultwarden_config_signups_verify: true +vaultwarden_config_signups_verify_resend_time: 3600 +vaultwarden_config_signups_verify_resend_limit: 5 +# Entry preview icons +vaultwarden_config_disable_icon_download: true +vaultwarden_config_icon_cache_ttl: 604800 # 7 days +vaultwarden_config_icon_cache_negttl: 259200 # 3 days +vaultwarden_config_icon_download_timeout: 30 # seconds +vaultwarden_config_icon_blacklist_non_global_ips: true +# Features +vaultwarden_config_sends_allowed: true +vaultwarden_config_enable_yubico: false +vaultwarden_config_enable_duo: false +vaultwarden_config_enable_smtp: false +vaultwarden_config_enable_email_2fa: false +# Security +vaultwarden_config_password_iterations: 100000 +vaultwarden_config_show_password_hint: false +vaultwarden_config_disable_2fa_remember: false +vaultwarden_config_disable_admin_token: true +vaultwarden_config_require_device_email: false +vaultwarden_config_authenticator_disable_time_drift: true +# Other +vaultwarden_config_log_timestamp_format: "%Y-%m-%d %H:%M:%S.%3f" +vaultwarden_config_ip_header: "X-Real-IP" +vaultwarden_config_reload_templates: false + +vaultwarden_base_config: + domain: "{{ vaultwarden_config_domain }}" + admin_token: "{{ vaultwarden_config_admin_token }}" + invitations_allowed: "{{ vaultwarden_config_invitations_allowed }}" + invitation_org_name: "{{ vaultwarden_config_invitation_org_name | default('', true) }}" + signups_allowed: "{{ vaultwarden_config_signups_allowed }}" + signups_verify: "{{ vaultwarden_config_signups_verify }}" + signups_verify_resend_time: "{{ vaultwarden_config_signups_verify_resend_time }}" + signups_verify_resend_limit: "{{ vaultwarden_config_signups_verify_resend_limit }}" + disable_icon_download: "{{ vaultwarden_config_disable_icon_download }}" + icon_cache_ttl: "{{ vaultwarden_config_icon_cache_ttl }}" + icon_cache_negttl: "{{ vaultwarden_config_icon_cache_negttl }}" + icon_download_timeout: "{{ vaultwarden_config_icon_download_timeout }}" + icon_blacklist_non_global_ips: "{{ vaultwarden_config_icon_blacklist_non_global_ips }}" + password_iterations: "{{ vaultwarden_config_password_iterations }}" + show_password_hint: "{{ vaultwarden_config_show_password_hint }}" + disable_2fa_remember: "{{ vaultwarden_config_disable_2fa_remember }}" + disable_admin_token: "{{ vaultwarden_config_disable_admin_token }}" + require_device_email: "{{ vaultwarden_config_require_device_email }}" + authenticator_disable_time_drift: "{{ vaultwarden_config_authenticator_disable_time_drift }}" + ip_header: "{{ vaultwarden_config_ip_header }}" + log_timestamp_format: "{{ vaultwarden_config_log_timestamp_format }}" + reload_templates: "{{ vaultwarden_config_reload_templates }}" + sends_allowed: "{{ vaultwarden_config_sends_allowed }}" + _enable_yubico: "{{ vaultwarden_config_enable_yubico }}" + _enable_duo: "{{ vaultwarden_config_enable_duo }}" + _enable_smtp: "{{ vaultwarden_config_enable_smtp }}" + _enable_email_2fa: "{{ vaultwarden_config_enable_email_2fa }}" + +vaultwarden_config: ~ +vaultwarden_merged_config: >-2 + {{ vaultwarden_base_config | default({}, true) + | combine(vaultwarden_config | default({}, true), recursive=true) }} diff --git a/roles/vaultwarden/defaults/main/container.yml b/roles/vaultwarden/defaults/main/container.yml new file mode 100644 index 0000000..efaa879 --- /dev/null +++ b/roles/vaultwarden/defaults/main/container.yml @@ -0,0 +1,50 @@ +--- +vaultwarden_container_image_registry: docker.io +vaultwarden_container_image_namespace: vaultwarden +vaultwarden_container_image_name: server +vaultwarden_container_image_tag: ~ +vaultwarden_container_image_flavour: alpine +vaultwarden_container_image_source: pull +vaultwarden_container_image_force_source: >-2 + {{ vaultwarden_container_image_tag | default(false, true) | bool }} +vaultwarden_container_image: >-2 + {{ + ([ + vaultwarden_container_image_registry | default([], true), + vaultwarden_container_image_namespace | default([], true), + vaultwarden_container_image_name, + ] | flatten | join('/')) + + ':' + + (vaultwarden_container_image_tag | default( + vaultwarden_version + ( + ((vaultwarden_container_image_flavour is string) + and (vaultwarden_container_image_flavour | length > 0)) + | ternary( + '-' + vaultwarden_container_image_flavour | default('', true), + '' + ) + ), + true + )) + }} + +vaultwarden_container_name: vaultwarden +vaultwarden_container_env: ~ +vaultwarden_container_user: >-2 + {{ vaultwarden_run_user_id }}:{{ vaultwarden_run_group_id }} +vaultwarden_container_ports: ~ +vaultwarden_container_labels: ~ +vaultwarden_container_networks: ~ +vaultwarden_container_etc_hosts: ~ +vaultwarden_container_dns_servers: ~ +vaultwarden_container_restart_policy: >-2 + {{ (vaultwarden_deployment_method == 'docker') | ternary( + 'unless-stopped', + 'on-failure', + ) + }} +vaultwarden_container_state: >-2 + {{ (vaultwarden_state == 'present') | ternary('started', 'absent') }} +vaultwarden_container_volumes: + - "{{ vaultwarden_data_directory }}:/data:rw" + - "{{ vaultwarden_config_file }}:/data/config.json:ro" diff --git a/roles/vaultwarden/defaults/main/main.yml b/roles/vaultwarden/defaults/main/main.yml new file mode 100644 index 0000000..98eb134 --- /dev/null +++ b/roles/vaultwarden/defaults/main/main.yml @@ -0,0 +1,10 @@ +--- +vaultwarden_user: vaultwarden +vaultwarden_version: "1.32.2" + +vaultwarden_config_file: "/etc/vaultwarden/config.json" +vaultwarden_config_directory: "{{ vaultwarden_config_file | dirname }}" +vaultwarden_data_directory: "/var/lib/vaultwarden" + +vaultwarden_state: present +vaultwarden_deployment_method: docker diff --git a/roles/vaultwarden/defaults/main/user.yml b/roles/vaultwarden/defaults/main/user.yml new file mode 100644 index 0000000..27a4743 --- /dev/null +++ b/roles/vaultwarden/defaults/main/user.yml @@ -0,0 +1,5 @@ +--- +vaultwarden_run_user_id: >-2 + {{ vaultwarden_user_info.uid | default(vaultwarden_user, true) }} +vaultwarden_run_group_id: >-2 + {{ vaultwarden_user_info.group | default(vaultwarden_user, true) }} diff --git a/roles/vaultwarden/handlers/main.yml b/roles/vaultwarden/handlers/main.yml new file mode 100644 index 0000000..9715963 --- /dev/null +++ b/roles/vaultwarden/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Ensure vaultwarden container '{{ vaultwarden_container_name }}' is restarted + community.docker.docker_container: + name: "{{ vaultwarden_container_name }}" + state: "{{ vaultwarden_container_state }}" + restart: true + listen: vaultwarden-restart + when: vaultwarden_deployment_method == 'docker' + ignore_errors: "{{ ansible_check_mode }}" diff --git a/roles/vaultwarden/meta/main.yml b/roles/vaultwarden/meta/main.yml new file mode 100644 index 0000000..fe8fa01 --- /dev/null +++ b/roles/vaultwarden/meta/main.yml @@ -0,0 +1,12 @@ +--- +allow_duplicates: true +dependencies: [] +galaxy_info: + role_name: vaultwarden + description: >-2 + Deploy vaultwarden, a bitwarden-compatible server backend + galaxy_tags: + - vaultwarden + - bitwarden + - passwordstore + - docker diff --git a/roles/vaultwarden/tasks/deploy-docker.yml b/roles/vaultwarden/tasks/deploy-docker.yml new file mode 100644 index 0000000..5b0d5e7 --- /dev/null +++ b/roles/vaultwarden/tasks/deploy-docker.yml @@ -0,0 +1,22 @@ +--- +- name: Ensure container image '{{ vaultwarden_container_image }}' is {{ vaultwarden_state }} + community.docker.docker_image: + name: "{{ vaultwarden_container_image }}" + state: "{{ vaultwarden_state }}" + source: "{{ vaultwarden_container_image_source }}" + force_source: "{{ vaultwarden_container_image_force_source }}" + +- name: Ensure container '{{ vaultwarden_container_name }}' is {{ vaultwarden_container_state }} + community.docker.docker_container: + name: "{{ vaultwarden_container_name }}" + image: "{{ vaultwarden_container_image }}" + env: "{{ vaultwarden_container_env | default(omit, true) }}" + user: "{{ vaultwarden_container_user | default(omit, true) }}" + ports: "{{ vaultwarden_container_ports | default(omit, true) }}" + labels: "{{ vaultwarden_container_labels | default(omit, true) }}" + volumes: "{{ vaultwarden_container_volumes }}" + networks: "{{ vaultwarden_container_networks | default(omit, true) }}" + etc_hosts: "{{ vaultwarden_container_etc_hosts | default(omit, true) }}" + dns_servers: "{{ vaultwarden_container_dns_servers | default(omit, true) }}" + restart_policy: "{{ vaultwarden_container_restart_policy | default(omit, true) }}" + state: "{{ vaultwarden_container_state | default(omit, true) }}" diff --git a/roles/vaultwarden/tasks/main.yml b/roles/vaultwarden/tasks/main.yml new file mode 100644 index 0000000..e34a03e --- /dev/null +++ b/roles/vaultwarden/tasks/main.yml @@ -0,0 +1,78 @@ +--- +- name: Ensure state is valid + ansible.builtin.fail: + msg: >-2 + Unsupported state '{{ vaultwarden_state }}'! + Supported states are {{ vaultwarden_states | join(', ') }}. + when: vaultwarden_state not in vaultwarden_states + +- name: Ensure deployment method is valid + ansible.builtin.fail: + msg: >-2 + Unsupported deployment method '{{ vaultwarden_deployment_method }}'! + Supported are {{ vaultwarden_deployment_methods | join(', ') }}. + when: vaultwarden_deployment_method not in vaultwarden_deployment_methods + +- name: Ensure required variables are given + ansible.builtin.fail: + msg: "Required variable '{{ var }}' is undefined!" + loop: "{{ vaultwarden_required_variables }}" + loop_control: + loop_var: var + when: >-2 + var not in hostvars[inventory_hostname] + or hostvars[inventory_hostname][var] | length == 0 + +- name: Ensure required variables are given + ansible.builtin.fail: + msg: "Required variable '{{ var.name }}' is undefined!" + loop: "{{ vaultwarden_conditionally_required_variables }}" + loop_control: + loop_var: var + label: "{{ var.name }}" + when: >-2 + var.when and ( + var.name not in hostvars[inventory_hostname] + or hostvars[inventory_hostname][var.name] | length == 0) + +- name: Ensure vaultwarden user '{{ vaultwarden_user }}' is {{ vaultwarden_state }} + ansible.builtin.user: + name: "{{ vaultwarden_user }}" + state: "{{ vaultwarden_state }}" + system: "{{ vaultwarden_user_system | default(true, true) }}" + create_home: "{{ vaultwarden_user_create_home | default(false, true) }}" + groups: "{{ vaultwarden_user_groups | default(omit, true) }}" + append: >-2 + {{ vaultwarden_user_append_groups | default( + (vaultwarden_user_groups | default([], true) | length > 0), + true, + ) }} + register: vaultwarden_user_info + +- name: Ensure base paths are {{ vaultwarden_state }} + ansible.builtin.file: + path: "{{ mount.path }}" + state: "{{ (vaultwarden_state == 'present') | ternary('directory', 'absent') }}" + owner: "{{ mount.owner | default(vaultwarden_run_user_id) }}" + group: "{{ mount.group | default(vaultwarden_run_group_id) }}" + mode: "{{ mount.mode | default('0755', true) }}" + loop: + - path: "{{ vaultwarden_config_directory }}" + - path: "{{ vaultwarden_data_directory }}" + loop_control: + loop_var: mount + label: "{{ mount.path }}" + +- name: Ensure vaultwarden config file '{{ vaultwarden_config_file }}' is {{ vaultwarden_state }} + ansible.builtin.copy: + content: "{{ vaultwarden_merged_config | to_nice_json(indent=4) }}" + dest: "{{ vaultwarden_config_file }}" + owner: "{{ vaultwarden_run_user_id }}" + group: "{{ vaultwarden_run_group_id }}" + mode: "0640" + when: vaultwarden_state == 'present' + notify: vaultwarden-restart + +- name: Deploy vaultwarden using {{ vaultwarden_deployment_method }} + ansible.builtin.include_tasks: + file: "deploy-{{ vaultwarden_deployment_method }}.yml" diff --git a/roles/vaultwarden/vars/main.yml b/roles/vaultwarden/vars/main.yml new file mode 100644 index 0000000..989d2d4 --- /dev/null +++ b/roles/vaultwarden/vars/main.yml @@ -0,0 +1,11 @@ +--- +vaultwarden_states: + - present + - absent +vaultwarden_deployment_methods: + - docker +vaultwarden_required_variables: + - vaultwarden_config_domain +vaultwarden_conditionally_required_variables: + - name: vaultwarden_config_admin_token + when: "{{ vaultwarden_config_disable_admin_token | default(true, true) | bool }}"