# ================================================================ # PLAY 0 — Bootstrap: install acl BEFORE any become_user is used. # ================================================================ - name: Bootstrap – ensure acl is installed hosts: nextcloud become: true gather_facts: false 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 become: true vars: nextcloud_version: "34.0.0" #EDIT based on your needs nextcloud_domain: "cloud.test.local" #EDIT based on your needs nextcloud_data_dir: "/etc/nextcloud/data" nextcloud_install_dir: "/etc/nextcloud" db_name: "nextcloud" #EDIT based on your needs db_user: "nextcloud" #EDIT based on your needs db_password: "Start2026!" # ÄNDERN admin_user: "admin" #EDIT based on your needs admin_password: "Start2026!" # ÄNDERN php_version: "8.3" # TLS certificate paths (self-signed, generated by this playbook) ssl_cert: "/etc/ssl/certs/nextcloud-selfsigned.crt" ssl_key: "/etc/ssl/private/nextcloud-selfsigned.key" # Maintenance window (UTC): tasks run between start and start+4h # 1 = 01:00 UTC → adjust to your timezone offset as needed maintenance_window_start: 1 # --------------------------------------------------------------- # 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 {{ ssl_key }} -out {{ ssl_cert }} -subj "/CN={{ nextcloud_domain }}/O=Nextcloud/C=DE" -addext "subjectAltName=DNS:{{ nextcloud_domain }}" creates: "{{ ssl_cert }}" - name: Restrict private key permissions ansible.builtin.file: path: "{{ ssl_key }}" owner: root group: root mode: "0600" # --------------------------------------------------------------- # 4. PHP 8.3 + extensions # --------------------------------------------------------------- - name: Install PHP {{ php_version }} and extensions ansible.builtin.apt: name: - "php{{ php_version }}" - "php{{ php_version }}-cli" - "php{{ php_version }}-common" - "php{{ php_version }}-curl" - "php{{ php_version }}-gd" - "php{{ php_version }}-gmp" - "php{{ php_version }}-imagick" - "php{{ php_version }}-intl" - "php{{ php_version }}-mbstring" - "php{{ php_version }}-mysql" - "php{{ php_version }}-opcache" - "php{{ php_version }}-readline" - "php{{ php_version }}-redis" - "php{{ php_version }}-xml" - "php{{ php_version }}-zip" - "php{{ php_version }}-bcmath" - "php{{ php_version }}-apcu" state: present notify: Restart Apache - name: Configure PHP for Nextcloud (php.ini tweaks) ansible.builtin.lineinfile: path: "/etc/php/{{ 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/{{ 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: "{{ 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: "{{ db_user }}" password: "{{ db_password }}" priv: "{{ 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 {{ ssl_cert }} SSLCertificateKeyFile {{ 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 "{{ db_name }}" \ --database-user "{{ db_user }}" \ --database-pass "{{ db_password }}" \ --admin-user "{{ admin_user }}" \ --admin-pass "{{ 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 \ maintenance_window_start --type=integer --value="{{ 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 become: true vars: nextcloud_version: "34.0.0" #EDIT based on your needs nextcloud_domain: "cloud.test.local" #EDIT based on your needs nextcloud_data_dir: "/etc/nextcloud/data" #EDIT based on your needs nextcloud_install_dir: "/etc/nextcloud" #EDIT based on your needs fail2ban_dir: "/etc/fail2ban" #EDIT based on your needs php_version: "8.3" #EDIT based on your needs tasks: - name: Set correct open_basedir restriction ansible.builtin.lineinfile: path: "/etc/php/{{ 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