Files
ping-mee-ansible-library/playbooks/linux/ubuntu-server/deploy_hardened_nextcloud.yml
T
2026-07-14 09:02:52 +02:00

476 lines
17 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ================================================================
# 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"
nextcloud_domain: "192.168.124.80"
nextcloud_data_dir: "/etc/nextcloud/data"
nextcloud_install_dir: "/etc/nextcloud"
db_name: "nextcloud"
db_user: "nextcloud"
db_password: "Start2026!" # ÄNDERN
admin_user: "admin"
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 ──────────────────────
<VirtualHost *:80>
ServerName {{ nextcloud_domain }}
RewriteEngine On
RewriteRule ^(.*)$ https://%{HTTP_HOST}$1 [R=301,L]
</VirtualHost>
# ── HTTPS: serve Nextcloud ────────────────────────────────────
<VirtualHost *:443>
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
<IfModule mod_headers.c>
Header always set Strict-Transport-Security "max-age=15552000; includeSubDomains"
</IfModule>
<Directory {{ nextcloud_install_dir }}>
Require all granted
AllowOverride All
Options FollowSymLinks MultiViews
<IfModule mod_dav.c>
Dav off
</IfModule>
</Directory>
# 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
</VirtualHost>
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"
nextcloud_domain: "cloud.test.local"
nextcloud_data_dir: "/etc/nextcloud/data"
nextcloud_install_dir: "/etc/nextcloud"
fail2ban_dir: "/etc/fail2ban"
php_version: "8.3"
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":"<HOST>"%(_groupsre)s,?\s*"message":"Login failed:
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_groupsre)s,?\s*"message":"Two-factor challenge failed:
^\{%(_groupsre)s,?\s*"remoteAddr":"<HOST>"%(_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