diff --git a/playbooks/seafile.yml b/playbooks/seafile.yml new file mode 100644 index 0000000..cf17ee0 --- /dev/null +++ b/playbooks/seafile.yml @@ -0,0 +1,9 @@ +--- +- name: Podman Seafile | Deploy and update Seafile instances + hosts: + - seafile + roles: + - role: sr2c.core.baseline + tags: bootstrap + - role: sr2c.apps.podman_seafile + tags: seafile diff --git a/roles/podman_seafile/defaults/main.yml b/roles/podman_seafile/defaults/main.yml new file mode 100644 index 0000000..42d1a51 --- /dev/null +++ b/roles/podman_seafile/defaults/main.yml @@ -0,0 +1,3 @@ +--- +podman_seafile_podman_rootless_user: seafile +# podman_seafile_redis_password: \ No newline at end of file diff --git a/roles/podman_seafile/handlers/main.yml b/roles/podman_seafile/handlers/main.yml new file mode 100644 index 0000000..b90e6b3 --- /dev/null +++ b/roles/podman_seafile/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Restart Seafile + ansible.builtin.systemd_service: + name: seafile.service + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_seafile_podman_rootless_user }}" diff --git a/roles/podman_seafile/tasks/main.yml b/roles/podman_seafile/tasks/main.yml new file mode 100644 index 0000000..70e7518 --- /dev/null +++ b/roles/podman_seafile/tasks/main.yml @@ -0,0 +1,122 @@ +--- +- name: Podman Seafile | PATCH | Install data plate + ansible.builtin.template: + src: etc/motd.d/10-data-plate.txt + dest: /etc/motd.d/10-data-plate.txt + owner: root + group: root + mode: "0444" + become: true + +- name: Podman Seafile | PATCH | Install podman and verify rootless podman user + ansible.builtin.include_role: + role: sr2c.core.podman_host + vars: + podman_host_minimum_unpriv_port: 80 + podman_host_rootless_users: ["{{ podman_seafile_podman_rootless_user }}"] + +- name: Podman Seafile | AUDIT | Get subuid range for user + ansible.builtin.command: + cmd: "getsubids {{ podman_seafile_podman_rootless_user }}" + register: _podman_seafile_user_subuid + changed_when: false + +- name: Podman Seafile | AUDIT | Get subgid range for user + ansible.builtin.command: + cmd: "getsubids -g {{ podman_seafile_podman_rootless_user }}" + register: _podman_seafile_user_subgid + changed_when: false + +- name: Podman Seafile | AUDIT | Parse outputs of getsubids and store results + ansible.builtin.set_fact: + _podman_seafile_user_subuid_start: "{{ (_podman_seafile_user_subuid.stdout_lines[0].split()[2] | int) }}" + _podman_seafile_user_subgid_start: "{{ (_podman_seafile_user_subgid.stdout_lines[0].split()[2] | int) }}" + +# MySQL runs with UID/GID 999 inside the container +- name: Podman Seafile | PATCH | Create data directory for MySQL + ansible.builtin.file: + path: "/home/{{ podman_seafile_podman_rootless_user }}/mysql_data" + owner: "{{ _podman_seafile_user_subuid_start + 998 }}" + group: "{{ _podman_seafile_user_subgid_start + 998 }}" + mode: "0750" + state: "directory" + become: true + +# Seafile runs as root inside the container +- name: Podman Seafile | PATCH | Create data directories for Seafile + ansible.builtin.file: + path: "/home/{{ podman_seafile_podman_rootless_user }}/{{ item }}" + owner: "{{ podman_seafile_podman_rootless_user }}" + group: "{{ podman_seafile_podman_rootless_user }}" + mode: "0755" + state: "directory" + become: true + with_items: + - seafile_data + - seadoc_data + - onlyoffice/logs + - onlyoffice/data + - onlyoffice/lib + +- name: Podman CDR Link | PATCH | Install container quadlets + ansible.builtin.template: + src: "home/podman/config/containers/systemd/{{ item }}" + dest: "/home/{{ podman_seafile_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_seafile_podman_rootless_user }}" + mode: "0400" + with_items: + - mysql.container + - redis.container + - seafile.container + - seadoc.container + - onlyoffice.container + - frontend.network + - seafile.network + become: true + notify: + - Restart Seafile + +- name: Podman Seafile | PATCH | Set up nginx and Let's Encrypt certificate + ansible.builtin.include_role: + name: sr2c.core.podman_nginx + vars: + podman_nginx_frontend_network: frontend + podman_nginx_podman_rootless_user: "{{ podman_seafile_podman_rootless_user }}" + podman_nginx_primary_hostname: "{{ podman_seafile_hostname }}" + +- name: Podman Seafile | PATCH | Install production nginx configuration file + ansible.builtin.template: + src: home/podman/nginx/nginx.conf + dest: "/home/{{ podman_seafile_podman_rootless_user }}/nginx/nginx.conf" + owner: "{{ podman_seafile_podman_rootless_user }}" + group: "{{ podman_seafile_podman_rootless_user }}" + mode: "0644" + become: true + notify: + - Restart nginx + +- name: Podman Seafile | PATCH | Ensure services are running and enabled + ansible.builtin.systemd_service: + name: seafile.service + scope: user + masked: false + state: started + enabled: true + become: true + become_user: "{{ podman_seafile_podman_rootless_user }}" + +- name: Podman Seafile | AUDIT | Wait until the seahub config file is created + ansible.builtin.wait_for: + path: "/home/{{ podman_seafile_podman_rootless_user }}/seafile_data/seafile/conf/seahub_settings.py" + state: present + become: true + +- name: Podman Seafile | PATCH | Append Seafile config block from template for proxy and OAuth + ansible.builtin.blockinfile: + path: "/home/{{ podman_seafile_podman_rootless_user }}/seafile_data/seafile/conf/seahub_settings.py" + block: "{{ lookup('ansible.builtin.template', 'home/podman/seafile_data/seahub_settings.py') }}" + insertafter: EOF + marker: "# {mark} ANSIBLE MANAGED BLOCK (Keycloak OAuth login)" + become: true + notify: + - Restart Seafile diff --git a/roles/podman_seafile/templates/etc/motd.d/10-data-plate.txt b/roles/podman_seafile/templates/etc/motd.d/10-data-plate.txt new file mode 100644 index 0000000..096b7a8 --- /dev/null +++ b/roles/podman_seafile/templates/etc/motd.d/10-data-plate.txt @@ -0,0 +1,8 @@ + ========================================================= + A Seafile instance is hosted on this server. + Podman user: {{ podman_seafile_podman_rootless_user }} + ========================================================= + # Become the podman user + sudo -iu {{ podman_seafile_podman_rootless_user }} + ========================================================= + diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/frontend.network b/roles/podman_seafile/templates/home/podman/config/containers/systemd/frontend.network new file mode 100644 index 0000000..379c059 --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/frontend.network @@ -0,0 +1,2 @@ +[Network] +NetworkName=frontend diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/mysql.container b/roles/podman_seafile/templates/home/podman/config/containers/systemd/mysql.container new file mode 100644 index 0000000..47eccd8 --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/mysql.container @@ -0,0 +1,16 @@ +[Container] +ContainerName=mysql +Environment=MYSQL_ROOT_PASSWORD={{ podman_seafile_mysql_root_password | replace("%", "%%") }} +Environment=MYSQL_LOG_CONSOLE=true +Environment=MARIADB_AUTO_UPGRADE=1 +HealthCmd=["/usr/local/bin/healthcheck.sh","--connect","--mariadbupgrade","--innodb_initialized"] +HealthInterval=20s +HealthRetries=10 +HealthStartPeriod=30s +HealthTimeout=5s +Image=docker.io/mariadb:10.11 +Network=seafile.network +Volume=/home/{{ podman_seafile_podman_rootless_user }}/mysql_data:/var/lib/mysql:rw,Z + +[Service] +Restart=always diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/onlyoffice.container b/roles/podman_seafile/templates/home/podman/config/containers/systemd/onlyoffice.container new file mode 100644 index 0000000..c06285a --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/onlyoffice.container @@ -0,0 +1,12 @@ +[Container] +ContainerName=onlyoffice +Environment=JWT_ENABLED=true +Environment=JWT_SECRET={{ podman_seafile_jwt_private_key | replace("%", "%%") }} +Image=docker.io/onlyoffice/documentserver:8.1.0.1 +Network=frontend.network +Volume=/home/{{ podman_seafile_podman_rootless_user }}/onlyoffice/logs:/var/log/onlyoffice:rw,Z +Volume=/home/{{ podman_seafile_podman_rootless_user }}/onlyoffice/data:/var/www/onlyoffice/Data:rw,Z +Volume=/home/{{ podman_seafile_podman_rootless_user }}/onlyoffice/lib:/var/lib/onlyoffice:rw,Z + +[Service] +Restart=always diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/redis.container b/roles/podman_seafile/templates/home/podman/config/containers/systemd/redis.container new file mode 100644 index 0000000..de1d649 --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/redis.container @@ -0,0 +1,9 @@ +[Container] +ContainerName=redis +Environment=REDIS_PASSWORD={{ podman_seafile_redis_password | replace("%", "%%") }} +Exec=/bin/sh -c 'redis-server --requirepass "$$REDIS_PASSWORD"' +Image=docker.io/redis +Network=seafile.network + +[Service] +Restart=always diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/seadoc.container b/roles/podman_seafile/templates/home/podman/config/containers/systemd/seadoc.container new file mode 100644 index 0000000..9dd55f7 --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/seadoc.container @@ -0,0 +1,22 @@ +[Unit] +Requires=mysql.service +After=mysql.service + +[Container] +ContainerName=seadoc +Environment=DB_HOST=mysql +Environment=DB_PORT=3306 +Environment=DB_USER=seafile +Environment=DB_PASSWORD={{ podman_seafile_mysql_user_password | replace("%", "%%") }} +Environment=DB_NAME=seahub_db +Environment=TIME_ZONE=Etc/UTC +Environment=JWT_PRIVATE_KEY={{ podman_seafile_jwt_private_key | replace("%", "%%") }} +Environment=NON_ROOT=false +Environment=SEAHUB_SERVICE_URL=http://seafile:80 +Image=docker.io/seafileltd/sdoc-server:2.0-latest +Network=seafile.network +Network=frontend.network +Volume=/home/{{ podman_seafile_podman_rootless_user }}/seadoc_data:/shared:rw,Z + +[Service] +Restart=always diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/seafile.container b/roles/podman_seafile/templates/home/podman/config/containers/systemd/seafile.container new file mode 100644 index 0000000..335da64 --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/seafile.container @@ -0,0 +1,41 @@ +[Unit] +Requires=mysql.service redis.service +After=mysql.service redis.service + +[Container] +ContainerName=seafile +Environment=SEAFILE_MYSQL_DB_HOST=mysql +Environment=SEAFILE_MYSQL_DB_PORT=3306 +Environment=SEAFILE_MYSQL_DB_USER=seafile +Environment=SEAFILE_MYSQL_DB_PASSWORD={{ podman_seafile_mysql_user_password | replace("%", "%%") }} +Environment=INIT_SEAFILE_MYSQL_ROOT_PASSWORD={{ podman_seafile_mysql_root_password | replace("%", "%%") }} +Environment=SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db +Environment=SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db +Environment=SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub_db +Environment=TIME_ZONE=Etc/UTC +Environment=INIT_SEAFILE_ADMIN_EMAIL={{ podman_seafile_admin_email | replace("%", "%%") }} +Environment=INIT_SEAFILE_ADMIN_PASSWORD={{ podman_seafile_admin_password | replace("%", "%%") }} +Environment=SEAFILE_SERVER_HOSTNAME={{ podman_seafile_hostname | replace("%", "%%") }} +Environment=SEAFILE_SERVER_PROTOCOL=https +Environment=SITE_ROOT=/ +Environment=NON_ROOT=false +Environment=JWT_PRIVATE_KEY={{ podman_seafile_jwt_private_key | replace("%", "%%") }} +Environment=SEAFILE_LOG_TO_STDOUT=true +Environment=CACHE_PROVIDER=redis +Environment=REDIS_HOST=redis +Environment=REDIS_PORT=6379 +Environment=REDIS_PASSWORD={{ podman_seafile_redis_password | replace("%", "%%") }} +Environment=MEMCACHED_HOST=memcached +Environment=MEMCACHED_PORT=11211 +Environment=ENABLE_NOTIFICATION_SERVER=false +Environment=ENABLE_SEAFILE_AI=false +Environment=MD_FILE_COUNT_LIMIT=100000 +Environment=ENABLE_SEADOC=true +Environment=SEADOC_SERVER_URL=https://{{ podman_seafile_hostname | replace("%", "%%") }}/sdoc-server +Image=docker.io/seafileltd/seafile-mc:13.0-latest +Network=seafile.network +Network=frontend.network +Volume=/home/{{ podman_seafile_podman_rootless_user }}/seafile_data:/shared:rw,Z + +[Service] +Restart=always diff --git a/roles/podman_seafile/templates/home/podman/config/containers/systemd/seafile.network b/roles/podman_seafile/templates/home/podman/config/containers/systemd/seafile.network new file mode 100644 index 0000000..ded2b80 --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/config/containers/systemd/seafile.network @@ -0,0 +1,2 @@ +[Network] +NetworkName=seafile diff --git a/roles/podman_seafile/templates/home/podman/nginx/nginx.conf b/roles/podman_seafile/templates/home/podman/nginx/nginx.conf new file mode 100644 index 0000000..de05bbb --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/nginx/nginx.conf @@ -0,0 +1,124 @@ +# {{ ansible_managed }} + +error_log /dev/stdout info; +access_log /dev/stdout; + +resolver 10.89.0.1 ipv6=off valid=10s; + +gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; + +# Mitigate httpoxy attack +proxy_set_header Proxy ""; + +server { + listen 80; + listen [::]:80; + + server_name {{ podman_seafile_hostname }}; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://{{ podman_seafile_hostname }}$request_uri; + } +} + +upstream seafile { + zone seafile_upstream 64k; + server seafile:80 resolve; +} + +upstream seadoc { + zone seadoc_upstream 64k; + server seadoc:80 resolve; +} + +upstream onlyoffice { + zone onlyoffice_upstream 64k; + server onlyoffice:80 resolve; +} + +map $http_upgrade $proxy_connection { + default upgrade; + "" close; +} + +server { + server_name {{ podman_seafile_hostname }}; + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + server_tokens off; + + ssl_certificate /etc/letsencrypt/live/{{ podman_seafile_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ podman_seafile_hostname }}/privkey.pem; + + add_header Strict-Transport-Security "max-age=31536000" always; + add_header Referrer-Policy origin always; # make sure outgoing links don't show the URL to the instance + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + location ~ ^(/accounts/login)(.*)$ { + return 301 /oauth/login$2; + } + + location / { + proxy_pass http://seafile; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Port 443; + proxy_buffering on; + proxy_buffer_size 8k; + proxy_buffers 2048 8k; + + client_max_body_size 100m; + } + + location /sdoc-server/ { + proxy_pass http://seadoc/; + proxy_redirect off; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $server_name; + + client_max_body_size 100m; + } + + location /socket.io { + proxy_pass http://seadoc; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_redirect off; + + proxy_buffers 8 32k; + proxy_buffer_size 64k; + + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $http_host; + proxy_set_header X-NginX-Proxy true; + } + + location /onlyofficeds/ { + proxy_pass http://onlyoffice/; + proxy_http_version 1.1; + + client_max_body_size 100M; + + proxy_read_timeout 3600s; + proxy_connect_timeout 3600s; + + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $proxy_connection; + proxy_set_header X-Forwarded-Host $host/onlyofficeds; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + +} diff --git a/roles/podman_seafile/templates/home/podman/seafile_data/seahub_settings.py b/roles/podman_seafile/templates/home/podman/seafile_data/seahub_settings.py new file mode 100644 index 0000000..e6557af --- /dev/null +++ b/roles/podman_seafile/templates/home/podman/seafile_data/seahub_settings.py @@ -0,0 +1,32 @@ +SEAFILE_SERVER_HOSTNAME = "{{ podman_seafile_hostname }}" +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +CSRF_TRUSTED_ORIGINS = ["https://{{ podman_seafile_hostname }}"] +FORCE_HTTPS_IN_CONF = True +USE_X_FORWARDED_HOST = True + +ENABLE_OAUTH = True + +OAUTH_CREATE_UNKNOWN_USER = True +OAUTH_ACTIVATE_USER_AFTER_CREATION = True + +OAUTH_CLIENT_ID = "{{ podman_seafile_keycloak_client_id }}" +OAUTH_CLIENT_SECRET = "{{ podman_seafile_keycloak_client_secret }}" +OAUTH_REDIRECT_URL = "https://{{ podman_seafile_hostname }}/oauth/callback/" + +OAUTH_PROVIDER_DOMAIN = '{{ podman_seafile_hostname }}' +OAUTH_AUTHORIZATION_URL = 'https://{{ podman_seafile_keycloak_hostname }}/realms/{{ podman_seafile_keycloak_realm }}/protocol/openid-connect/auth' +OAUTH_TOKEN_URL = 'https://{{ podman_seafile_keycloak_hostname }}/realms/{{ podman_seafile_keycloak_realm }}/protocol/openid-connect/token' +OAUTH_USER_INFO_URL = 'https://{{ podman_seafile_keycloak_hostname }}/realms/{{ podman_seafile_keycloak_realm }}/protocol/openid-connect/userinfo' +OAUTH_SCOPE = ["openid", "profile", "email"] +OAUTH_ATTRIBUTE_MAP = { + "sub": (True, "uid"), + "email": (False, "contact_email"), + "name": (False, "name") +} + +ENABLE_ONLYOFFICE = True +ONLYOFFICE_APIJS_URL = 'https://{{ podman_seafile_hostname }}/onlyofficeds/web-apps/apps/api/documents/api.js' +ONLYOFFICE_JWT_SECRET = '{{ podman_seafile_jwt_private_key }}' +ONLYOFFICE_FILE_EXTENSION = ('doc', 'docx', 'ppt', 'pptx', 'xls', 'xlsx', 'odt', 'fodt', 'odp', 'fodp', 'ods', 'fods', 'ppsx', 'pps', 'csv') +ONLYOFFICE_EDIT_FILE_EXTENSION = ('docx', 'pptx', 'xlsx', 'csv') +OFFICE_PREVIEW_MAX_SIZE = 30 * 1024 * 1024 # preview size, 30 MB