476 lines
17 KiB
YAML
476 lines
17 KiB
YAML
# ================================================================
|
||
# 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: "cloud.test.local"
|
||
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 |