diff --git a/linux/ubuntu-server/deploy_basics.yml b/linux/ubuntu-server/deploy_basics.yml
new file mode 100644
index 0000000..874c6d9
--- /dev/null
+++ b/linux/ubuntu-server/deploy_basics.yml
@@ -0,0 +1,27 @@
+- name: Install basic features
+ hosts: ansible_master
+ become: true
+
+ tasks:
+ - name: Update apt
+ ansible.builtin.apt:
+ update_cache: true
+ cache_valid_time: 3600
+
+ - name: Install net-tools
+ ansible.builtin.apt:
+ name: net-tools
+ state: present
+
+ - name: Install neofetch or fastfetch
+ block:
+ - name: Install neofetch
+ ansible.builtin.apt:
+ name: neofetch
+ state: present
+
+ rescue:
+ - name: If neofetch is depricated install fastfetch
+ ansible.builtin.apt:
+ name: fastfetch
+ state: present
\ No newline at end of file
diff --git a/linux/ubuntu-server/deploy_hardened_nextcloud.yml b/linux/ubuntu-server/deploy_hardened_nextcloud.yml
new file mode 100644
index 0000000..d616270
--- /dev/null
+++ b/linux/ubuntu-server/deploy_hardened_nextcloud.yml
@@ -0,0 +1,485 @@
+---
+# Nextcloud Installation Playbook
+# Target: Ubuntu 24.04
+# Stack: Apache2 + PHP 8.3 + MariaDB + Nextcloud 34.0.0
+# HTTPS via self-signed certificate
+#
+# Usage:
+# ansible-playbook nextcloud.yml -i inventory.ini
+
+# ================================================================
+# 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 ──────────────────────
+
+ 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"
+ 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":""%(_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
\ No newline at end of file