Fix Nextcloud DNS resolution and implement systemd cron for background jobs

- Enable IP forwarding in security playbook (net.ipv4.ip_forward = 1)
- Add podman network firewall rules to fix container DNS/HTTPS access
- Implement systemd timer for reliable Nextcloud background job execution
- Add database optimization tasks (indices, bigint conversion, mimetypes)
- Configure maintenance window (04:00 UTC) and phone region (NO)
- Add security headers (X-Robots-Tag, X-Permitted-Cross-Domain-Policies)
- Create Nextcloud removal playbook for clean uninstall
- Fix nftables interface matching (podman0 vs podman+)

Root cause: nftables FORWARD chain blocked container egress traffic
Solution: Explicit firewall rules for podman0 bridge interface
This commit is contained in:
2025-12-20 19:51:26 +01:00
parent 90bbcd97b1
commit 846ab74f87
14 changed files with 484 additions and 11 deletions

View File

@@ -5,6 +5,7 @@
- [ ] What gets served on jnss.me?
- [ ] Backups
- [x] Titan email provider support. For smtp access to hello@jnss.me
- [ ] Vaultvarden
@@ -13,10 +14,12 @@
- [ ] Settings
- [ ] Contacts and calendars
- [ ] Storage bucket integration?
- [x] SMTP setup for email sending
- [x] Gitea
- [x] SSH passthrough setup
- [x] Figure out how to disable registration and local password
- [x] SMTP setup for email sending
- [ ] Authentik Invitations for users?

View File

@@ -0,0 +1,212 @@
---
# =================================================================
# Nextcloud Removal Playbook
# =================================================================
# Rick-Infra - Clean removal of Nextcloud installation
#
# This playbook removes all Nextcloud components:
# - Systemd services and timers
# - Container and images
# - Data directories
# - Database and user
# - Caddy configuration
# - System user and groups
#
# Usage: ansible-playbook playbooks/remove-nextcloud.yml -i inventory/hosts.yml
- name: Remove Nextcloud Installation
hosts: arch-vps
become: yes
gather_facts: yes
vars:
nextcloud_user: nextcloud
nextcloud_group: nextcloud
nextcloud_home: /opt/nextcloud
nextcloud_db_name: nextcloud
nextcloud_db_user: nextcloud
caddy_sites_enabled_dir: /etc/caddy/sites-enabled
tasks:
# ============================================
# Stop and Disable Services
# ============================================
- name: Stop and disable nextcloud-cron timer
systemd:
name: nextcloud-cron.timer
state: stopped
enabled: no
failed_when: false
- name: Stop and disable nextcloud-cron service
systemd:
name: nextcloud-cron.service
state: stopped
enabled: no
failed_when: false
- name: Stop and disable nextcloud service
systemd:
name: nextcloud.service
state: stopped
enabled: no
failed_when: false
# ============================================
# Remove Container and Images
# ============================================
- name: Remove nextcloud container (if running)
command: podman rm -f nextcloud
register: container_remove
changed_when: container_remove.rc == 0
failed_when: false
- name: Remove nextcloud images
command: podman rmi -f {{ item }}
loop:
- docker.io/library/nextcloud:stable-fpm
- docker.io/library/nextcloud
register: image_remove
changed_when: image_remove.rc == 0
failed_when: false
# ============================================
# Remove Systemd Units
# ============================================
- name: Remove nextcloud-cron systemd units
file:
path: "{{ item }}"
state: absent
loop:
- /etc/systemd/system/nextcloud-cron.timer
- /etc/systemd/system/nextcloud-cron.service
- name: Remove nextcloud quadlet file
file:
path: /etc/containers/systemd/nextcloud.container
state: absent
- name: Reload systemd daemon
systemd:
daemon_reload: yes
# ============================================
# Remove Database
# ============================================
- name: Drop nextcloud database
become_user: postgres
postgresql_db:
name: "{{ nextcloud_db_name }}"
state: absent
failed_when: false
- name: Drop nextcloud database user
become_user: postgres
postgresql_user:
name: "{{ nextcloud_db_user }}"
state: absent
failed_when: false
# ============================================
# Remove Caddy Configuration
# ============================================
- name: Remove nextcloud Caddy configuration
file:
path: "{{ caddy_sites_enabled_dir }}/nextcloud.caddy"
state: absent
notify: reload caddy
# ============================================
# Remove Data Directories
# ============================================
- name: Remove nextcloud home directory (including all data)
file:
path: "{{ nextcloud_home }}"
state: absent
# ============================================
# Remove User and Groups
# ============================================
- name: Remove nextcloud user
user:
name: "{{ nextcloud_user }}"
state: absent
remove: yes
force: yes
- name: Remove nextcloud group
group:
name: "{{ nextcloud_group }}"
state: absent
# ============================================
# Clean Up Remaining Files
# ============================================
- name: Find nextcloud-related files in /tmp
find:
paths: /tmp
patterns: "nextcloud*,nc_*"
file_type: any
register: tmp_files
- name: Remove nextcloud temp files
file:
path: "{{ item.path }}"
state: absent
loop: "{{ tmp_files.files }}"
when: tmp_files.files | length > 0
failed_when: false
- name: Remove caddy logs for nextcloud
file:
path: /var/log/caddy/nextcloud.log
state: absent
failed_when: false
# ============================================
# Verification
# ============================================
- name: Verify nextcloud service is removed
command: systemctl list-units --all nextcloud*
register: units_check
changed_when: false
- name: Verify nextcloud container is removed
command: podman ps -a --filter name=nextcloud
register: container_check
changed_when: false
- name: Display removal status
debug:
msg: |
✅ Nextcloud removal complete!
Removed components:
- ⏹️ Nextcloud service and cron timer
- 🐳 Container: {{ 'Removed' if container_remove.rc == 0 else 'Not found' }}
- 🗄️ Database: {{ nextcloud_db_name }}
- 📁 Data directory: {{ nextcloud_home }}
- 👤 System user: {{ nextcloud_user }}
- 🌐 Caddy configuration
Remaining services:
{{ units_check.stdout }}
Containers:
{{ container_check.stdout }}
handlers:
- name: reload caddy
systemd:
name: caddy
state: reloaded
failed_when: false

View File

@@ -322,8 +322,8 @@
sysctl_file: /etc/sysctl.d/99-security.conf
reload: yes
loop:
# Disable IP forwarding
- { name: 'net.ipv4.ip_forward', value: '0' }
# Enable IP forwarding (required for container networking)
- { name: 'net.ipv4.ip_forward', value: '1' }
- { name: 'net.ipv6.conf.all.forwarding', value: '0' }
# Disable source routing

View File

@@ -25,13 +25,13 @@
# name: authentik
# tags: ['authentik', 'sso', 'auth']
- name: Deploy Gitea
include_role:
name: gitea
tags: ['gitea', 'git', 'development']
# - name: Deploy Nextcloud
# - name: Deploy Gitea
# include_role:
# name: nextcloud
# tags: ['nextcloud', 'cloud', 'storage']
# name: gitea
# tags: ['gitea', 'git', 'development']
- name: Deploy Nextcloud
include_role:
name: nextcloud
tags: ['nextcloud', 'cloud', 'storage']

View File

@@ -67,6 +67,20 @@ nextcloud_overwriteprotocol: "https"
nextcloud_php_memory_limit: "512M"
nextcloud_php_upload_limit: "512M"
# =================================================================
# Background Jobs Configuration
# =================================================================
nextcloud_background_jobs_mode: "cron" # Options: ajax, webcron, cron
nextcloud_cron_interval: "5min" # How often cron runs (systemd timer)
# =================================================================
# Maintenance Configuration
# =================================================================
nextcloud_maintenance_window_start: 4 # Start hour (UTC) for maintenance window
nextcloud_default_phone_region: "NO" # Default phone region code (ISO 3166-1 alpha-2)
# =================================================================
# Caddy Integration
# =================================================================

View File

@@ -0,0 +1,72 @@
---
# =================================================================
# Nextcloud Background Jobs Configuration
# =================================================================
# Rick-Infra - Nextcloud Role
#
# Configures systemd timer for reliable background job execution
# instead of Ajax-based cron (which requires user activity)
- name: Create nextcloud cron service
copy:
content: |
[Unit]
Description=Nextcloud Background Jobs (cron.php)
Documentation=https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/background_jobs_configuration.html
After=nextcloud.service
Requires=nextcloud.service
[Service]
Type=oneshot
ExecStart=/usr/bin/podman exec --user www-data nextcloud php -f /var/www/html/cron.php
# Logging
StandardOutput=journal
StandardError=journal
SyslogIdentifier=nextcloud-cron
dest: /etc/systemd/system/nextcloud-cron.service
mode: '0644'
backup: yes
notify: reload systemd
- name: Create nextcloud cron timer
copy:
content: |
[Unit]
Description=Nextcloud Background Jobs Timer
Documentation=https://docs.nextcloud.com/server/latest/admin_manual/configuration_server/background_jobs_configuration.html
[Timer]
OnBootSec=5min
OnUnitActiveSec={{ nextcloud_cron_interval }}
Unit=nextcloud-cron.service
[Install]
WantedBy=timers.target
dest: /etc/systemd/system/nextcloud-cron.timer
mode: '0644'
backup: yes
notify: reload systemd
- name: Enable and start nextcloud cron timer
systemd:
name: nextcloud-cron.timer
enabled: yes
state: started
daemon_reload: yes
- name: Configure Nextcloud to use cron for background jobs
command: >
podman exec --user www-data nextcloud
php occ background:cron
register: nextcloud_cron_mode
changed_when: "'background jobs mode changed' in nextcloud_cron_mode.stdout or 'Set mode for background jobs to' in nextcloud_cron_mode.stdout"
failed_when:
- nextcloud_cron_mode.rc != 0
- "'mode for background jobs is currently' not in nextcloud_cron_mode.stdout"
- name: Verify cron timer is active
command: systemctl is-active nextcloud-cron.timer
register: timer_status
changed_when: false
failed_when: timer_status.stdout != "active"

View File

@@ -138,6 +138,21 @@
notify: restart nextcloud
tags: [config, redis]
- name: Truncate nextcloud.log to prevent bloat
shell: |
podman exec nextcloud truncate -s 0 /var/www/html/data/nextcloud.log || true
changed_when: false
failed_when: false
tags: [maintenance, cleanup]
- name: Configure background jobs (cron)
include_tasks: cron.yml
tags: [cron, background-jobs]
- name: Optimize database and apply configuration
include_tasks: optimization.yml
tags: [optimization, database]
- name: Display Nextcloud deployment status
debug:
msg: |

View File

@@ -0,0 +1,64 @@
---
# =================================================================
# Nextcloud Database Optimization
# =================================================================
# Rick-Infra - Nextcloud Role
#
# Performs database maintenance tasks to optimize performance
# and resolve setup warnings about missing indices and migrations
- name: Add missing database indices
command: >
podman exec --user www-data nextcloud
php occ db:add-missing-indices
register: nextcloud_indices
changed_when: "'indices added' in nextcloud_indices.stdout or 'Check indices' in nextcloud_indices.stdout"
failed_when:
- nextcloud_indices.rc != 0
- "'already exists' not in nextcloud_indices.stderr"
- name: Convert filecache bigint columns
command: >
podman exec --user www-data nextcloud
php occ db:convert-filecache-bigint --no-interaction
register: nextcloud_bigint
changed_when: "'converted' in nextcloud_bigint.stdout"
failed_when:
- nextcloud_bigint.rc != 0
- "'already' not in nextcloud_bigint.stdout"
timeout: 300 # 5 minutes for large databases
- name: Update mimetype database mappings
command: >
podman exec --user www-data nextcloud
php occ maintenance:repair --include-expensive
register: nextcloud_repair
changed_when: "'updated' in nextcloud_repair.stdout or 'repaired' in nextcloud_repair.stdout"
failed_when: nextcloud_repair.rc != 0
timeout: 600 # 10 minutes for expensive repairs
- name: Configure maintenance window
command: >
podman exec --user www-data nextcloud
php occ config:system:set maintenance_window_start --value={{ nextcloud_maintenance_window_start }} --type=integer
register: nextcloud_maintenance_window
changed_when: "'set' in nextcloud_maintenance_window.stdout"
failed_when: nextcloud_maintenance_window.rc != 0
- name: Configure default phone region
command: >
podman exec --user www-data nextcloud
php occ config:system:set default_phone_region --value={{ nextcloud_default_phone_region }}
register: nextcloud_phone_region
changed_when: "'set' in nextcloud_phone_region.stdout"
failed_when: nextcloud_phone_region.rc != 0
- name: Display optimization results
debug:
msg: |
Database optimization complete:
- Indices: {{ 'Added' if 'indices added' in nextcloud_indices.stdout else 'Already optimized' }}
- BigInt: {{ 'Converted' if 'converted' in nextcloud_bigint.stdout else 'Already converted' }}
- Mimetypes: {{ 'Updated' if 'updated' in nextcloud_repair.stdout else 'Up to date' }}
- Maintenance window: {{ nextcloud_maintenance_window_start }}:00 UTC
- Phone region: {{ nextcloud_default_phone_region }}

View File

@@ -58,6 +58,10 @@
Referrer-Policy "no-referrer"
# Disable FLoC tracking
Permissions-Policy "interest-cohort=()"
# Robot indexing policy
X-Robots-Tag "noindex, nofollow"
# Cross-domain policy
X-Permitted-Cross-Domain-Policies "none"
# Remove server header
-Server
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Nextcloud Additional Configuration
* Rick-Infra - Nextcloud Role
*
* This file provides additional configuration for Nextcloud
* that complements the main config.php file.
*
* Applied via: php occ config:system:set
*/
$CONFIG = array(
/**
* Maintenance Window
*
* Defines a maintenance window during which resource-intensive
* operations (like database updates) can be performed.
*
* Format: Hour (0-23, UTC)
*/
'maintenance_window_start' => {{ nextcloud_maintenance_window_start }},
/**
* Default Phone Region
*
* Sets the default country code for phone number validation.
* Used when users enter phone numbers without country prefix.
*
* Format: ISO 3166-1 alpha-2 country code
*/
'default_phone_region' => '{{ nextcloud_default_phone_region }}',
);

View File

@@ -46,6 +46,10 @@ podman_registry_blocked: false
podman_default_network: "bridge"
podman_network_security: true
# Trusted container subnets (allowed through firewall)
podman_trusted_subnets:
- "10.88.0.0/16"
# =================================================================
# Storage Configuration
# =================================================================

View File

@@ -10,4 +10,9 @@
systemd:
name: podman
state: restarted
when: podman_service_enabled | default(true)
when: podman_service_enabled | default(true)
- name: reload nftables
systemd:
name: nftables
state: reloaded

View File

@@ -42,6 +42,22 @@
backup: yes
notify: restart podman
- name: Create default podman network with DNS enabled
command: podman network create podman --subnet 10.88.0.0/16
register: podman_network_create
changed_when: "'podman' in podman_network_create.stdout"
failed_when:
- podman_network_create.rc != 0
- "'already exists' not in podman_network_create.stderr"
- name: Deploy podman firewall rules
template:
src: podman.nft.j2
dest: /etc/nftables.d/10-podman.nft
mode: '0644'
backup: yes
notify: reload nftables
- name: Enable podman system service (if enabled)
systemd:
name: podman

View File

@@ -0,0 +1,32 @@
#!/usr/sbin/nft -f
# =================================================================
# Podman Container Network Firewall Rules
# =================================================================
# Rick-Infra Infrastructure - Podman Role
# Priority: 10 (loaded after base rules, before drop rules)
#
# Purpose:
# - Allow container-to-host communication for services (PostgreSQL, Valkey)
# - Allow container outbound traffic for DNS, package updates, etc.
# - Enable NAT/masquerading for container networks
#
# Security Model:
# - Containers are trusted (they run our own services)
# - All container egress traffic is allowed (simplified management)
# - Container ingress is controlled by application-specific port publishing
#
# Architecture:
# - Containers access host services via Unix sockets or host.containers.internal
# - Caddy reverse proxy handles all external traffic
# - No direct container port exposure to internet
# Add rules to INPUT chain - Allow trusted container subnets
{% for subnet in podman_trusted_subnets %}
add rule inet filter input ip saddr {{ subnet }} accept comment "Podman containers: {{ subnet }}"
{% endfor %}
# Add rules to FORWARD chain - Enable container forwarding
add rule inet filter forward ct state established,related accept comment "Allow established connections"
add rule inet filter forward iifname "podman0" accept comment "Allow outbound from podman bridge"
add rule inet filter forward oifname "podman0" ct state established,related accept comment "Allow inbound to podman bridge (established)"