- name: Temporary workaround for sudo-rs
hosts: all
gather_facts: false
become: false
tasks:
- name: Detect sudo variant
ansible.builtin.raw: command -v sudo.ws || true
register: sudo_ws_check
changed_when: false
- name: Set become_exe fact based on sudo variant present
ansible.builtin.set_fact:
ansible_become_exe: "{{ 'sudo.ws' if sudo_ws_check.stdout | trim | length > 0 else 'sudo' }}"
# ================================================================
# PLAY 0 — Bootstrap: install acl BEFORE any become_user is used.
# ================================================================
- name: Bootstrap – ensure acl is installed
hosts: nextcloud
gather_facts: true
become: true
tasks:
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
- name: Install acl (required for Ansible become_user on Linux)
ansible.builtin.apt:
name: acl
state: present
# ================================================================
# PLAY 1 — Main Nextcloud installation
# ================================================================
- name: Install and configure Nextcloud
hosts: nextcloud
gather_facts: true
become: true
vars_files:
- vars/shared_vars.yml
# ---------------------------------------------------------------
# 1. System packages
# ---------------------------------------------------------------
tasks:
- name: Install system packages
ansible.builtin.apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
- python3-pymysql
- openssl
- unzip
- bzip2
state: present
# ---------------------------------------------------------------
# 2. Apache
# ---------------------------------------------------------------
- name: Install Apache2
ansible.builtin.apt:
name: apache2
state: present
- name: Enable required Apache modules
community.general.apache2_module:
name: "{{ item }}"
state: present
loop:
- rewrite
- headers
- env
- dir
- mime
- ssl
notify: Restart Apache
- name: Enable Apache service
ansible.builtin.systemd:
name: apache2
enabled: true
state: started
# ---------------------------------------------------------------
# 3. Self-signed TLS certificate
# ---------------------------------------------------------------
- name: Generate self-signed TLS certificate (10-year validity)
ansible.builtin.command:
cmd: >
openssl req -x509 -nodes -days 3650
-newkey rsa:4096
-keyout {{ nc_ssl_key }}
-out {{ nc_ssl_cert }}
-subj "/CN={{ nextcloud_domain }}/O=Nextcloud/C=DE"
-addext "subjectAltName=DNS:{{ nextcloud_domain }}"
creates: "{{ nc_ssl_cert }}"
- name: Restrict private key permissions
ansible.builtin.file:
path: "{{ nc_ssl_key }}"
owner: root
group: root
mode: "0600"
# ---------------------------------------------------------------
# 4. PHP 8.3 + extensions
# ---------------------------------------------------------------
- name: Install PHP {{ nc_php_version }} and extensions
ansible.builtin.apt:
name:
- "php{{ nc_php_version }}"
- "php{{ nc_php_version }}-cli"
- "php{{ nc_php_version }}-common"
- "php{{ nc_php_version }}-curl"
- "php{{ nc_php_version }}-gd"
- "php{{ nc_php_version }}-gmp"
- "php{{ nc_php_version }}-imagick"
- "php{{ nc_php_version }}-intl"
- "php{{ nc_php_version }}-mbstring"
- "php{{ nc_php_version }}-mysql"
- "php{{ nc_php_version }}-opcache"
- "php{{ nc_php_version }}-readline"
- "php{{ nc_php_version }}-redis"
- "php{{ nc_php_version }}-xml"
- "php{{ nc_php_version }}-zip"
- "php{{ nc_php_version }}-bcmath"
- "php{{ nc_php_version }}-apcu"
state: present
notify: Restart Apache
- name: Configure PHP for Nextcloud (php.ini tweaks)
ansible.builtin.lineinfile:
path: "/etc/php/{{ nc_php_version }}/apache2/php.ini"
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
backup: true
loop:
- { regexp: '^memory_limit', line: 'memory_limit = 2048M' }
- { regexp: '^upload_max_filesize', line: 'upload_max_filesize = 16G' }
- { regexp: '^post_max_size', line: 'post_max_size = 16G' }
- { regexp: '^max_execution_time', line: 'max_execution_time = 300' }
- { regexp: '^max_input_time', line: 'max_input_time = 300' }
- { regexp: '^output_buffering', line: 'output_buffering = Off' }
notify: Restart Apache
- name: Enable OPcache settings
ansible.builtin.blockinfile:
path: "/etc/php/{{ nc_php_version }}/apache2/conf.d/10-opcache.ini"
block: |
opcache.enable=1
opcache.interned_strings_buffer=32
opcache.max_accelerated_files=10000
opcache.memory_consumption=128
opcache.save_comments=1
opcache.revalidate_freq=1
marker: "; {mark} ANSIBLE MANAGED BLOCK"
notify: Restart Apache
# ---------------------------------------------------------------
# 5. MariaDB
# ---------------------------------------------------------------
- name: Install MariaDB server
ansible.builtin.apt:
name:
- mariadb-server
- mariadb-client
state: present
- name: Enable and start MariaDB
ansible.builtin.systemd:
name: mariadb
enabled: true
state: started
- name: Create Nextcloud database
community.mysql.mysql_db:
name: "{{ nc_db_name }}"
collation: utf8mb4_general_ci
encoding: utf8mb4
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
- name: Create Nextcloud database user
community.mysql.mysql_user:
name: "{{ nc_db_user }}"
password: "{{ nc_db_password }}"
priv: "{{ nc_db_name }}.*:ALL"
host: localhost
state: present
login_unix_socket: /var/run/mysqld/mysqld.sock
# ---------------------------------------------------------------
# 6. Download & extract Nextcloud
# ---------------------------------------------------------------
- name: Check if Nextcloud is already present
ansible.builtin.stat:
path: "{{ nextcloud_install_dir }}/index.php"
register: nextcloud_stat
- name: Download Nextcloud {{ nextcloud_version }}
ansible.builtin.get_url:
url: "https://download.nextcloud.com/server/releases/nextcloud-{{ nextcloud_version }}.zip"
dest: "/tmp/nextcloud-{{ nextcloud_version }}.zip"
mode: "0644"
when: not nextcloud_stat.stat.exists
- name: Extract Nextcloud archive
ansible.builtin.unarchive:
src: "/tmp/nextcloud-{{ nextcloud_version }}.zip"
dest: /etc/
remote_src: true
owner: www-data
group: www-data
when: not nextcloud_stat.stat.exists
- name: Create Nextcloud data directory
ansible.builtin.file:
path: "{{ nextcloud_data_dir }}"
state: directory
owner: www-data
group: www-data
mode: "0750"
- name: Set correct ownership on Nextcloud install directory
ansible.builtin.file:
path: "{{ nextcloud_install_dir }}"
recurse: true
owner: www-data
group: www-data
# ---------------------------------------------------------------
# 7. Apache virtual host — HTTP redirects to HTTPS, HTTPS serves NC
# ---------------------------------------------------------------
- name: Deploy Nextcloud Apache virtual host (HTTP redirect + HTTPS)
ansible.builtin.copy:
dest: /etc/apache2/sites-available/nextcloud.conf
mode: "0644"
content: |
# ── HTTP: redirect all traffic to HTTPS ──────────────────────
ServerName {{ nextcloud_domain }}
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
# ── HTTPS: serve Nextcloud ────────────────────────────────────
ServerName {{ nextcloud_domain }}
DocumentRoot {{ nextcloud_install_dir }}
SSLEngine on
SSLCertificateFile {{ nc_ssl_cert }}
SSLCertificateKeyFile {{ nc_ssl_key }}
# Recommended TLS hardening
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:!aNULL:!MD5:!DSS
SSLHonorCipherOrder on
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
Dav off
# Security headers
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
Header always set Referrer-Policy "no-referrer"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Permitted-Cross-Domain-Policies "none"
Header always set X-Robots-Tag "noindex, nofollow"
Header always set X-XSS-Protection "1; mode=block"
ErrorLog ${APACHE_LOG_DIR}/nextcloud_error.log
CustomLog ${APACHE_LOG_DIR}/nextcloud_access.log combined
notify: Restart Apache
- name: Disable default Apache site
ansible.builtin.command: a2dissite 000-default
args:
removes: /etc/apache2/sites-enabled/000-default.conf
notify: Restart Apache
- name: Enable Nextcloud Apache site
ansible.builtin.command: a2ensite nextcloud
args:
creates: /etc/apache2/sites-enabled/nextcloud.conf
notify: Restart Apache
# ---------------------------------------------------------------
# 8. Nextcloud occ installer
# ---------------------------------------------------------------
- name: Check if Nextcloud is already installed
ansible.builtin.stat:
path: "{{ nextcloud_install_dir }}/config/config.php"
register: nc_config
- name: Run Nextcloud installation via occ
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ maintenance:install \
--database "mysql" \
--database-name "{{ nc_db_name }}" \
--database-user "{{ nc_db_user }}" \
--database-pass "{{ nc_db_password }}" \
--admin-user "{{ nc_admin_user }}" \
--admin-pass "{{ nc_admin_password }}" \
--data-dir "{{ nextcloud_data_dir }}"'
args:
creates: "{{ nextcloud_install_dir }}/config/config.php"
register: occ_install
- name: Add trusted domain to Nextcloud config
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ config:system:set \
trusted_domains 0 --value="{{ nextcloud_domain }}"'
when: occ_install.changed
- name: Set overwrite.cli.url to HTTPS
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ config:system:set \
overwrite.cli.url --value="https://{{ nextcloud_domain }}"'
when: occ_install.changed
- name: Force HTTPS protocol in Nextcloud
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ config:system:set \
overwriteprotocol --value="https"'
when: occ_install.changed
- name: Set default phone region
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ \
config:system:set default_phone_region --value=DE'
when: occ_install.changed
- name: Set maintenance window start hour (UTC)
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ config:system:set \
nc_maintenance_window_start --type=integer --value="{{ nc_maintenance_window_start }}"'
when: occ_install.changed
# ---------------------------------------------------------------
# 9. Cron job for background tasks
# ---------------------------------------------------------------
- name: Configure Nextcloud background job to use cron
ansible.builtin.shell: |
su -s /bin/bash www-data -c \
'php {{ nextcloud_install_dir }}/occ background:cron'
- name: Add cron job for Nextcloud
ansible.builtin.cron:
name: "Nextcloud background tasks"
user: www-data
minute: "*/5"
job: "php -f {{ nextcloud_install_dir }}/cron.php > /dev/null 2>&1"
# ---------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------
handlers:
- name: Restart Apache
ansible.builtin.systemd:
name: apache2
state: restarted
# ---------------------------------------------------------------
# Play 3 - Nextcloud: Hardening
# ---------------------------------------------------------------
- name: Harden Nextcloud install
hosts: nextcloud
gather_facts: true
become: true
vars_files:
- vars/shared_vars.yml
tasks:
- name: Set correct open_basedir restriction
ansible.builtin.lineinfile:
path: "/etc/php/{{ nc_php_version }}/apache2/php.ini"
regexp: '^;?open_basedir'
line: "open_basedir = {{ nextcloud_install_dir }}:{{ nextcloud_data_dir }}:/tmp:/dev/urandom"
notify: Restart Apache
# ---------------------------------------------------------------
# Setup fail2ban for Nextcloud
# ---------------------------------------------------------------
- name: Install fail2ban
ansible.builtin.apt:
name:
- fail2ban
state: present
- name: Alter Nextcloud log settings for fail2ban
ansible.builtin.lineinfile:
path: "{{ nextcloud_install_dir }}/config/config.php"
regexp: "{{ item.regexp }}"
line: " {{ item.line }}"
insertbefore: '^\);'
backup: true
loop:
- { regexp: "^\\s*'log_type'\\s*=>", line: "'log_type' => 'file'," }
- { regexp: "^\\s*'logfile'\\s*=>", line: "'logfile' => '{{ nextcloud_data_dir }}/nextcloud.log'," }
- { regexp: "^\\s*'loglevel'\\s*=>", line: "'loglevel' => 3," }
- name: Create fail2ban filter for Nextcloud
ansible.builtin.copy:
content: |
{% raw %}
[Definition]
_groupsre = (?:(?:,?\s*"\w+":(?:"[^"]+"|\w+))*)
failregex = ^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Login failed:
^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Two-factor challenge failed:
^\{%(_groupsre)s,?\s*"remoteAddr":""%(_groupsre)s,?\s*"message":"Trusted domain error.
datepattern = ,?\s*"time"\s*:\s*"%%Y-%%m-%%d[T ]%%H:%%M:%%S(%%z)?"
{% endraw %}
dest: "{{ fail2ban_dir }}/filter.d/nextcloud.conf"
mode: "0644"
- name: Create fail2ban jail for Nextcloud
ansible.builtin.copy:
dest: "{{ fail2ban_dir }}/jail.d/nextcloud.local"
mode: "0644"
content: |
[nextcloud]
backend = auto
enabled = true
port = 80,443
protocol = tcp
filter = nextcloud
maxretry = 3
bantime = 86400
findtime = 43200
logpath = {{ nextcloud_data_dir }}/nextcloud.log
notify: Restart Apache
# ---------------------------------------------------------------
# Handlers
# ---------------------------------------------------------------
handlers:
- name: Restart Apache
ansible.builtin.systemd:
name: apache2
state: restarted
# This file was written by Ebbe Baß (umpi) - ebbe@ping-mee.de