diff --git a/roles/podman_cleaninsights/defaults/main.yml b/roles/podman_cleaninsights/defaults/main.yml new file mode 100644 index 0000000..eeff222 --- /dev/null +++ b/roles/podman_cleaninsights/defaults/main.yml @@ -0,0 +1,8 @@ +podman_cleaninsights_podman_rootless_user: cleaninsights +podman_cleaninsights_web_hostname: "{{ inventory_hostname }}" +podman_cleaninsights_mysql_database: matomo +# podman_cleaninsights_mysql_password: +# podman_cleaninsights_mysql_root_password: +podman_cleaninsights_php_memory_limit: 2048M +podman_cleaninsights_mysql_user: matomo +podman_cleaninsights_matomo_token: "" \ No newline at end of file diff --git a/roles/podman_link/handlers/main.yml b/roles/podman_cleaninsights/handlers/main.yml similarity index 50% rename from roles/podman_link/handlers/main.yml rename to roles/podman_cleaninsights/handlers/main.yml index 19d7e1d..e0735da 100644 --- a/roles/podman_link/handlers/main.yml +++ b/roles/podman_cleaninsights/handlers/main.yml @@ -1,26 +1,26 @@ -- name: restart zammad-init +- name: restart mysql ansible.builtin.systemd_service: - name: zammad-init + name: mysql state: restarted scope: user daemon_reload: true become: true - become_user: "{{ podman_link_podman_rootless_user }}" + become_user: "{{ podman_cleaninsights_podman_rootless_user }}" -- name: restart zammad-postgresql +- name: restart matomo ansible.builtin.systemd_service: - name: zammad-postgresql + name: matomo state: restarted scope: user daemon_reload: true become: true - become_user: "{{ podman_link_podman_rootless_user }}" + become_user: "{{ podman_cleaninsights_podman_rootless_user }}" -- name: restart link.slice +- name: restart redis ansible.builtin.systemd_service: - name: link.slice + name: redis state: restarted scope: user daemon_reload: true become: true - become_user: "{{ podman_link_podman_rootless_user }}" + become_user: "{{ podman_cleaninsights_podman_rootless_user }}" diff --git a/roles/podman_cleaninsights/tasks/main.yml b/roles/podman_cleaninsights/tasks/main.yml new file mode 100644 index 0000000..7550975 --- /dev/null +++ b/roles/podman_cleaninsights/tasks/main.yml @@ -0,0 +1,113 @@ +--- +- name: create service configuration directories + ansible.builtin.file: + path: "/home/{{ podman_cleaninsights_podman_rootless_user }}/{{ item }}" + state: directory + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + group: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0755" + become: true + with_items: + - mysql + - matomo + + +- name: install cleaninsights script + ansible.builtin.copy: + src: templates/cleaninsights.php + dest: "/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo/cleaninsights.php" + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + group: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0755" + become: true + +- name: install cleaninsights configuration file + ansible.builtin.template: + src: "cleaninsights.ini.j2" + dest: "/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo/cleaninsights.ini" + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + group: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0755" + become: true + +- name: install podman quadlet for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_cleaninsights_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0400" + with_items: + - matomo.container + - mysql.container + - redis.container + notify: + - "restart {{ item | split('.') | first }}" + become: true + + +- name: install network quadlets for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_cleaninsights_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0400" + with_items: + - frontend.network + - cleaninsights.network + become: true + +- name: verify quadlets are correctly defined + ansible.builtin.command: /usr/libexec/podman/quadlet -dryrun -user + register: podman_cleaninsights_quadlet_result + ignore_errors: true + changed_when: false + become: true + become_user: "{{ podman_cleaninsights_podman_rootless_user }}" + +- name: assert that the quadlet verification succeeded + ansible.builtin.assert: + that: + - podman_cleaninsights_quadlet_result.rc == 0 + fail_msg: "'/usr/libexec/podman/quadlet -dryrun -user' failed! Output withheld to prevent leaking secrets." + +- name: set up nginx + ansible.builtin.include_role: + name: irl.wip.podman_nginx + vars: + podman_nginx_frontend_network: frontend + podman_nginx_podman_rootless_user: "{{ podman_cleaninsights_podman_rootless_user }}" + podman_nginx_primary_hostname: "{{ podman_cleaninsights_web_hostname }}" + podman_nginx_systemd_service_slice: cleaninsights.slice + podman_nginx_systemd_service_requires: ["matomo"] + podman_nginx_additional_volumes: + - src: "/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo" + dest: "/var/www/html" + options: "ro" + +- name: create nginx configuration file + ansible.builtin.template: + src: nginx.conf + dest: "/home/{{ podman_cleaninsights_podman_rootless_user }}/nginx/nginx.conf" + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + group: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0644" + become: true + +- name: install services slice for rootless podman user + ansible.builtin.template: + src: "cleaninsights.slice" + dest: "/home/{{ podman_cleaninsights_podman_rootless_user }}/.config/systemd/user/cleaninsights.slice" + owner: "{{ podman_cleaninsights_podman_rootless_user }}" + group: "{{ podman_cleaninsights_podman_rootless_user }}" + mode: "0655" + become: true + +- name: make sure services are started on boot + ansible.builtin.systemd_service: + name: "cleaninsights.slice" + enabled: true + state: started + daemon_reload: true + scope: user + become: true + become_user: "{{ podman_cleaninsights_podman_rootless_user }}" diff --git a/roles/podman_cleaninsights/templates/cleaninsights.ini.j2 b/roles/podman_cleaninsights/templates/cleaninsights.ini.j2 new file mode 100644 index 0000000..01ea4b4 --- /dev/null +++ b/roles/podman_cleaninsights/templates/cleaninsights.ini.j2 @@ -0,0 +1,22 @@ +; A Matomo token for authentication to enable retrograde setting of timestamps. +; Needs to be set for CIMP to work. See https://matomo.org/faq/general/faq_114/ +token_auth = '{{ podman_cleaninsights_matomo_token }}' + +; Allowed CORS scheme/domain/port tuples. Defaults to none allowed. +; cors[] = 'https://example.com:3000' + +; The base URL of your Matomo instance. Only needs to be set if CIMP is +; not installed inside the Matomo directory.ca +matomo_base_url = 'http://nginx' + +; Number of created tracking requests to send to Matomo at once. + chunk_size = 1000 + +; Number of seconds to wait before retry, if Matomo responded with an error. +delay_after_failure = 2 + +; Number of maximum retries on error. +max_attempts = 3 + +; Set to true to have a debug output in 'ciapi.log' in the same directory. +debug = true \ No newline at end of file diff --git a/roles/podman_cleaninsights/templates/cleaninsights.network b/roles/podman_cleaninsights/templates/cleaninsights.network new file mode 100644 index 0000000..f95a29b --- /dev/null +++ b/roles/podman_cleaninsights/templates/cleaninsights.network @@ -0,0 +1,2 @@ +[Network] +NetworkName=cleaninsights diff --git a/roles/podman_cleaninsights/templates/cleaninsights.php b/roles/podman_cleaninsights/templates/cleaninsights.php new file mode 100644 index 0000000..d72a67a --- /dev/null +++ b/roles/podman_cleaninsights/templates/cleaninsights.php @@ -0,0 +1,341 @@ +idsite < 1) { + debug('Malformed body. Last JSON error:' . json_last_error() . ' ' . json_last_error_msg()); + http_response_code(400); + exit(1); +} + + +// Build output for Matomo Tracking API and send in chunks. + +$output = []; + +$reporting_start = time(); +$reporting_end = 0; +$count = 0; +$result = false; + +if (property_exists($data, 'visits') && gettype($data->visits) === 'array') { + foreach ($data->visits as $visit) { + $count += renderVisit($visit); + } +} + +if (property_exists($data, 'events') && gettype($data->events) === 'array') { + foreach ($data->events as $event) { + $count += renderEvent($event); + } +} + +// Send the rest. +send(false); + +debug('result = ' . ($result ? 'success': 'failure') . ", count = $count"); + +if ($result === false) { + debug('There were no events and visits or the Matomo Tracking API refused all requests.'); + http_response_code(400); + exit(1); +} + + +// Instruct Matomo to re-analyse the complete period of this request +// on the next auto-archiving run. + +$start = new DateTime(); +$start->setTimestamp($reporting_start); + +$end = new DateTime(); +$end->setTimestamp($reporting_end); + +$query = http_build_query([ + 'module' => 'API', + 'method' => 'CoreAdminHome.invalidateArchivedReports', + 'idSites' => $data->idsite, + 'period' => 'range', + // Need to use `dates[]` instead of `dates`. Otherwise, some automagic happens in Matomo and breaks the call. + 'dates[]' => $start->format('Y-m-d') . ',' . $end->format('Y-m-d'), + 'token_auth' => $TOKEN_AUTH, +]); + +$result = callMatomo(null, $query); + +if ($result === false) { + debug('Matomo could not be triggered to reprocess data!'); +} +else { + debug('Matomo was triggered to reprocess data!'); +} + +http_response_code(204); + +exit(); + + +// Helper functions. + +function renderVisit(stdClass $visit): int { + $action_name = $visit->action_name; + + return render($visit->period_start, $visit->period_end, $visit->times, + function() use ($action_name) { + return ['action_name' => $action_name]; + }); +} + +function renderEvent(stdClass $event): int { + return render($event->period_start, $event->period_end, $event->times, + function() use ($event) { + $e = ['e_c' => $event->category, 'e_a' => $event->action]; + + if (property_exists($event, 'name') && $event->name) { + $e['e_n'] = $event->name; + } + + if (property_exists($event, 'value') && $event->value) { + $e['e_v'] = $event->value; + } + + return $e; + }); +} + +function render(int $period_start, int $period_end, int $times, callable $callback): int { + global $reporting_start, $reporting_end, $output; + + $reporting_start = min($reporting_start, $period_start); + $reporting_end = max($reporting_end, $period_end); + + $interval = ($period_end - $period_start) / $times; + + // Offset the first record by half an interval, so records are neatly "centered" in the period. + $period_start += $interval / 2; + + for ($i = 0; $i < $times; $i++) { + $request = $callback(); + + $request['cdt'] = $period_start + $interval * $i; + + $output[] = buildRequest($request); + + send(); + } + + return $times; +} + +function buildRequest(array &$request): string { + global $data; + + $request['idsite'] = $data->idsite; + $request['rec'] = 1; + if (property_exists($data, 'lang') && $data->lang) $request['lang'] = $data->lang; + if (property_exists($data, 'ua') && $data->ua) $request['ua'] = $data->ua; + + return '?' . http_build_query($request); +} + +function callMatomo(string $path = null, string $query = null, string $body = null): string { + global $MATOMO_BASE_URL, $MAX_ATTEMPTS, $DELAY_AFTER_FAILURE; + + $options = [ + 'method' => 'POST', + 'header' => 'Content-Type: application/json; charset=UTF-8']; + + if (isset($body)) { + $options['content'] = $body; + } + + $context = stream_context_create(['http' => $options]); + + $url = "$MATOMO_BASE_URL/"; + + if (isset($path)) { + $url .= $path; + } + + if (isset($query)) { + $url .= "?$query"; + } + + $counter = 1; + + debug("Call Matomo at $url"); + + do { + $result = file_get_contents($url, false, $context); + + $counter++; + + if ($result !== false || $counter > $MAX_ATTEMPTS) { + break; + } + + debug("Request error. Sleep $DELAY_AFTER_FAILURE seconds and retry!"); + + sleep($DELAY_AFTER_FAILURE); + + } while (true); + + return $result; +} + +function sendToMatomo(array &$output): bool { + global $TOKEN_AUTH; + + $body = json_encode([ + 'token_auth' => $TOKEN_AUTH, + 'requests' => $output]); + + if ($body === false) { + return false; + } + + $result = callMatomo("matomo.php", null, $body); + + debug("Called tracking API " . (!$result ? 'un' : '') . "successfully with " + . count($output) . " lines of data: " . substr($body, 0, 256)); + + return !!$result; +} + +function send(bool $onlyIfChunkSizeReached = true) { + global $output, $CHUNK_SIZE, $result; + + if (!$onlyIfChunkSizeReached || count($output) >= $CHUNK_SIZE) { + if (sendToMatomo($output)) { + // If at least one request succeeded, we consider this good. + // Otherwise, the client would re-send already processed data. + $result = true; + } + + // Remove already sent requests from memory again. + $output = []; + } +} + +function debug($message) { + global $DEBUG; + if (!$DEBUG) return; + + error_log(print_r($message, true)); +} \ No newline at end of file diff --git a/roles/podman_cleaninsights/templates/cleaninsights.slice b/roles/podman_cleaninsights/templates/cleaninsights.slice new file mode 100644 index 0000000..71c7a82 --- /dev/null +++ b/roles/podman_cleaninsights/templates/cleaninsights.slice @@ -0,0 +1,8 @@ +[Unit] +Description=Cleaninsights Slice +Before=slices.target +Requires=matomo.service +Requires=nginx.service + +[Install] +WantedBy=default.target diff --git a/roles/podman_link/templates/frontend.network b/roles/podman_cleaninsights/templates/frontend.network similarity index 100% rename from roles/podman_link/templates/frontend.network rename to roles/podman_cleaninsights/templates/frontend.network diff --git a/roles/podman_cleaninsights/templates/matomo.container b/roles/podman_cleaninsights/templates/matomo.container new file mode 100644 index 0000000..fd9d168 --- /dev/null +++ b/roles/podman_cleaninsights/templates/matomo.container @@ -0,0 +1,23 @@ +[Unit] +Requires=mysql.service +After=mysql.service +Requires=redis.service +After=redis.service + +[Container] +ContainerName=matomo +Environment=MATOMO_DATABASE_HOST=mysql +Environment=PHP_MEMORY_LIMIT={{ podman_cleaninsights_php_memory_limit }} +Environment=MATOMO_DATABASE_DBNAME={{ podman_cleaninsights_mysql_database }} +Environment=MATOMO_DATABASE_PASSWORD={{ podman_cleaninsights_mysql_password }} +Environment=MATOMO_DATABASE_USERNAME={{ podman_cleaninsights_mysql_user }} +Image=docker.io/matomo:5-fpm +Volume=/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo:/var/www/html +Volume=/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo/cleaninsights.php:/var/www/html/cleaninsights.php:ro +Volume=/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo/cleaninsights.ini:/var/www/html/cleaninsights.ini:ro +Network=cleaninsights.network +Network=frontend.network + +[Service] +Restart=always +Slice=cleaninsights.slice diff --git a/roles/podman_cleaninsights/templates/mysql.container b/roles/podman_cleaninsights/templates/mysql.container new file mode 100644 index 0000000..71e6dd6 --- /dev/null +++ b/roles/podman_cleaninsights/templates/mysql.container @@ -0,0 +1,15 @@ +[Container] +ContainerName=mysql +Environment=MYSQL_ROOT_PASSWORD={{ podman_cleaninsights_mysql_root_password }} +Environment=MYSQL_DATABASE={{ podman_cleaninsights_mysql_database }} +Environment=MYSQL_USER={{ podman_cleaninsights_mysql_user }} +Environment=MYSQL_PASSWORD={{ podman_cleaninsights_mysql_password }} +Image=docker.io/mysql:9 +PublishPort=127.0.0.1:3306:3306 +Volume=/home/{{ podman_cleaninsights_podman_rootless_user }}/mysql:/var/lib/mysql +Network=cleaninsights.network + +[Service] +Restart=always +Slice=cleaninsights.slice + diff --git a/roles/podman_cleaninsights/templates/nginx.conf b/roles/podman_cleaninsights/templates/nginx.conf new file mode 100644 index 0000000..3c20147 --- /dev/null +++ b/roles/podman_cleaninsights/templates/nginx.conf @@ -0,0 +1,103 @@ +# {{ ansible_managed }} + +upstream php-handler { + server matomo:9000; +} + +server { + listen 80; + listen [::]:80; + + server_name {{ podman_cleaninsights_web_hostname }}; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://{{ podman_cleaninsights_web_hostname }}$request_uri; + } +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + http2 on; + + server_name matomo {{ podman_cleaninsights_web_hostname }}; + server_tokens off; + + ssl_certificate /etc/letsencrypt/live/{{ podman_cleaninsights_web_hostname }}/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/{{ podman_cleaninsights_web_hostname }}/privkey.pem; + + add_header Strict-Transport-Security max-age=15768000 always; + add_header Referrer-Policy origin always; # make sure outgoing links don't show the URL to the Matomo instance + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + + root /var/www/html/; # this is the default matomo image path + index index.php; + + location ~ ^/(index|matomo|cleaninsights|piwik|js/index|plugins/HeatmapSessionRecording/configs).php { + # regex to split $uri to $fastcgi_script_name and $fastcgi_path + try_files $fastcgi_script_name =404; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + # Check that the PHP script exists before passing it + #try_files $fastcgi_script_name =404; + + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param PATH_INFO $fastcgi_path_info; + fastcgi_param HTTP_PROXY ""; # prohibit httpoxy: https://httpoxy.org/ + include fastcgi_params; + fastcgi_pass php-handler; + fastcgi_read_timeout 60s; + } + + # deny access to all other .php files + location ~* ^.+\.php$ { + return 403; + } + + location / { + try_files $uri $uri/ =404; + } + + # deny access to all cleaninsights.ini file + location ~ cleaninsights.ini { + return 403; + } + + # disable all access to the following directories + location ~ ^/(config|tmp|core|lang) { + return 403; # replace with 404 to not show these directories exist + } + + location ~ /\.ht { + return 403; + } + + location ~ js/container_.*_preview\.js$ { + expires off; + add_header Cache-Control 'private, no-cache, no-store'; + } + + location ~ \.(gif|ico|jpg|png|svg|js|css|htm|html|mp3|mp4|wav|ogg|avi|ttf|eot|woff|woff2)$ { + allow all; + # Cache images,CSS,JS and webfonts for an hour + # Increasing the duration may improve the load-time, but may cause old files to show after an Matomo upgrade + expires 1h; + add_header Pragma public; + add_header Cache-Control "public"; + } + + location ~ ^/(libs|vendor|plugins|misc|node_modules) { + deny all; + return 403; + } + + # properly display textfiles in root directory + location ~/(.*\.md|LEGALNOTICE|LICENSE) { + default_type text/plain; + } +} \ No newline at end of file diff --git a/roles/podman_cleaninsights/templates/redis.container b/roles/podman_cleaninsights/templates/redis.container new file mode 100644 index 0000000..aa85fb3 --- /dev/null +++ b/roles/podman_cleaninsights/templates/redis.container @@ -0,0 +1,9 @@ +[Container] +ContainerName=redis +ExposeHostPort=6379 +Image=docker.io/redis:7.2-rc1-alpine +Network=cleaninsights.network + +[Service] +Restart=always +Slice=cleaninsights.slice diff --git a/roles/podman_forgejo/templates/forgejo.container b/roles/podman_forgejo/templates/forgejo.container index a213a64..9d8e00c 100644 --- a/roles/podman_forgejo/templates/forgejo.container +++ b/roles/podman_forgejo/templates/forgejo.container @@ -11,23 +11,8 @@ Environment=FORGEJO__database__HOST=mariadb:3306 Environment=FORGEJO__database__NAME={{ podman_forgejo_mariadb_database }} Environment=FORGEJO__database__USER={{ podman_forgejo_mariadb_user }} Environment=FORGEJO__database__PASSWD={{ podman_forgejo_mariadb_password }} -Environment=FORGEJO__email_0X2E_incoming__ENABLED=true -Environment=FORGEJO__email_0X2E_incoming__REPLY_TO_ADDRESS={{ podman_forgejo_reply_to }} -Environment=FORGEJO__email_0X2E_incoming__HOST={{ podman_forgejo_imap_hostname }} -Environment=FORGEJO__email_0X2E_incoming__PORT=993 -Environment=FORGEJO__email_0X2E_incoming__USERNAME={{ podman_forgejo_imap_username }} -Environment=FORGEJO__email_0X2E_incoming__PASSWORD={{ podman_forgejo_imap_password }} -Environment=FORGEJO__email_0X2E_incoming__USE_TLS=true Environment=FORGEJO__oauth2_client__ENABLE_AUTO_REGISTRATION=true Environment=FORGEJO__server__LANDING_PAGE=/explore/repos -Environment=FORGEJO__service__ENABLE_NOTIFY_MAIL=true -Environment=FORGEJO__mailer__ENABLED=true -Environment=FORGEJO__mailer__PROTOCOL=smtps -Environment=FORGEJO__mailer__SMTP_ADDR={{ podman_forgejo_smtp_hostname }} -Environment=FORGEJO__mailer__SMTP_PORT=465 -Environment=FORGEJO__mailer__USER={{ podman_forgejo_smtp_username }} -Environment=FORGEJO__mailer__PASSWD={{ podman_forgejo_smtp_password }} -Environment=FORGEJO__mailer__FROM={{ podman_forgejo_smtp_from }} Image=codeberg.org/forgejo/forgejo:11 Network=frontend.network Network=forgejo.network diff --git a/roles/podman_host/defaults/main.yml b/roles/podman_host/defaults/main.yml new file mode 100644 index 0000000..558c897 --- /dev/null +++ b/roles/podman_host/defaults/main.yml @@ -0,0 +1,3 @@ +--- +podman_host_minimum_unpriv_port: "22" +podman_host_rootless_users: ["podman"] diff --git a/roles/podman_host/tasks/main.yml b/roles/podman_host/tasks/main.yml new file mode 100644 index 0000000..36e5ca9 --- /dev/null +++ b/roles/podman_host/tasks/main.yml @@ -0,0 +1,69 @@ +--- +- name: set unprivileged port minimum + ansible.posix.sysctl: + name: net.ipv4.ip_unprivileged_port_start + value: "{{ podman_host_minimum_unpriv_port }}" + sysctl_set: true + sysctl_file: /etc/sysctl.d/zzz-podman-unpriv-port.conf + reload: true + become: true + +- name: create users for rootless podman + ansible.builtin.user: + name: "{{ item }}" + become: true + with_items: "{{ podman_host_rootless_users }}" + +- name: set XDG_RUNTIME_DIR in .profile for rootless users + ansible.builtin.lineinfile: + path: "/home/{{ item }}/.bashrc" + line: "export XDG_RUNTIME_DIR=/run/user/$(id -u)" + create: false + become: true + become_user: "{{ item }}" + with_items: "{{ podman_host_rootless_users }}" + +- name: enable linger for rootless users + ansible.builtin.command: + argv: + - /usr/bin/loginctl + - enable-linger + - "{{ item }}" + creates: "/var/lib/systemd/linger/{{ item }}" + become: true + with_items: "{{ podman_host_rootless_users }}" + +- name: create /etc/subuid and /etc/subgid + ansible.builtin.template: + dest: "/etc/{{ item }}" + src: subXid.j2 + with_items: + - subuid + - subgid + become: true + +- name: install podman + ansible.builtin.apt: + pkg: podman + state: latest + become: true + +- name: create quadlets directory + ansible.builtin.file: + path: "/home/{{ item }}/.config/containers/systemd" + state: directory + owner: "{{ item }}" + group: "{{ item }}" + mode: "0700" + with_items: "{{ podman_host_rootless_users }}" + become: true + +- name: enable podman auto update timer for rootless users + ansible.builtin.systemd_service: + name: podman-auto-update.timer + scope: user + state: started + enabled: true + become: true + become_user: "{{ item }}" + with_items: "{{ podman_host_rootless_users }}" diff --git a/roles/podman_host/templates/subXid.j2 b/roles/podman_host/templates/subXid.j2 new file mode 100644 index 0000000..a8022c6 --- /dev/null +++ b/roles/podman_host/templates/subXid.j2 @@ -0,0 +1,4 @@ +# {{ ansible_managed }} +{% for username in podman_host_rootless_users %} +{{ username }}:{{ 100000 + ((loop.index - 1) * 65536) }}:65536 +{% endfor %} diff --git a/roles/podman_link/defaults/main.yml b/roles/podman_link/defaults/main.yml deleted file mode 100644 index debbddf..0000000 --- a/roles/podman_link/defaults/main.yml +++ /dev/null @@ -1,25 +0,0 @@ -podman_link_podman_rootless_user: link -podman_link_web_hostname: "{{ inventory_hostname }}" -podman_link_postgres_zammad_user: postgres -podman_link_postgres_zammad_database: zammad_production -podman_link_postgres_link_user: link -podman_link_postgres_link_database: link -# podman_link_postgres_zammad_password: -# podman_link_postgres_link_password: -# podman_link_postgres_root_password: -# podman_link_zammad_redis_password: -# podman_link_opensearch_password: -podman_link_opensearch_memory_limit: 2048 -podman_link_setup_mode: false -podman_link_leafcutter_enabled: false -podman_link_dashboard_url: "" -podman_link_zammad_api_token: "" -# podman_link_nextauth_secret: -# podman_link_google_client_id: -# podman_link_google_client_secret: -# -# the following may seem useless, but unless they are included as variables in the environment file template, -# they automatically get wrapped in quotes by systemd causing zammad to not connect -podman_link_postgres_zammad_postgresql_host: zammad-postgresql -podman_link_postgres_zammad_es_host: opensearch -podman_link_postgres_zammad_memcached_server: zammad-memcached:11211 diff --git a/roles/podman_link/tasks/main.yml b/roles/podman_link/tasks/main.yml deleted file mode 100644 index 642d883..0000000 --- a/roles/podman_link/tasks/main.yml +++ /dev/null @@ -1,229 +0,0 @@ ---- -- name: create service configuration directories - ansible.builtin.file: - path: "/home/{{ podman_link_podman_rootless_user }}/{{ item }}" - state: directory - owner: "{{ podman_link_podman_rootless_user }}" - group: "{{ podman_link_podman_rootless_user }}" - mode: "0755" - become: true - with_items: - - zammad-storage - - zammad-var - - zammad-backup - - zammad-data - - signal-cli-rest-api-data - - bridge-postgresql-data - - bridge-whatsapp-data - - redis-data - - postgresql-data - -- name: create configuration directories where containers need to execute scripts - ansible.builtin.file: - path: "/home/{{ podman_link_podman_rootless_user }}/{{ item }}" - state: directory - owner: "{{ podman_link_podman_rootless_user }}" - group: "{{ podman_link_podman_rootless_user }}" - mode: "0777" - become: true - with_items: - - zammad-config-nginx - - opensearch-data - -- name: install zammad railsserver database configuration file - ansible.builtin.template: - src: "{{ item }}" - dest: "/home/{{ podman_link_podman_rootless_user }}/{{ item }}" - owner: "{{ podman_link_podman_rootless_user }}" - group: "{{ podman_link_podman_rootless_user }}" - mode: "0444" - become: true - with_items: - - zammad-database.yml - -- name: install env configuration files - ansible.builtin.template: - src: "{{ item }}" - dest: "/home/{{ podman_link_podman_rootless_user }}/.config/containers/systemd/{{ item }}" - owner: "{{ podman_link_podman_rootless_user }}" - mode: "0600" - become: true - with_items: - - common-zammad.env - - common-bridge.env - -- name: Set sysctl settings for elasticsearch - sysctl: - name: vm.max_map_count - value: '262144' - state: present - become: true - -- name: Set vm.overcommit_memory for Memcached - sysctl: - name: vm.overcommit_memory - value: '1' - state: present - become: true - -- name: install opensearch config - ansible.builtin.copy: - src: templates/opensearch-config.yml - dest: "/home/{{ podman_link_podman_rootless_user }}/opensearch-config.yml" - mode: "0444" - owner: "{{ podman_link_podman_rootless_user }}" - group: "{{ podman_link_podman_rootless_user }}" - become: true - -- name: install podman quadlet for rootless podman user - ansible.builtin.template: - src: "{{ item }}" - dest: "/home/{{ podman_link_podman_rootless_user }}/.config/containers/systemd/{{ item }}" - owner: "{{ podman_link_podman_rootless_user }}" - mode: "0400" - with_items: - - link.container - - zammad-opensearch.container - - opensearch-dashboards.container - - bridge-worker.container - - bridge-postgresql.container - - bridge-whatsapp.container - - signal-cli-rest-api.container - - zammad-init.container - - zammad-nginx.container - - zammad-railsserver.container - - zammad-scheduler.container - - zammad-postgresql.container - - zammad-websocket.container - - zammad-redis.container - - zammad-memcached.container - become: true - - -- name: install network quadlets for rootless podman user - ansible.builtin.template: - src: "{{ item }}" - dest: "/home/{{ podman_link_podman_rootless_user }}/.config/containers/systemd/{{ item }}" - owner: "{{ podman_link_podman_rootless_user }}" - mode: "0400" - with_items: - - frontend.network - - link.network - become: true - -- name: verify quadlets are correctly defined - ansible.builtin.command: /usr/libexec/podman/quadlet -dryrun -user - register: podman_link_quadlet_result - ignore_errors: true - changed_when: false - become: true - become_user: "{{ podman_link_podman_rootless_user }}" - -- name: assert that the quadlet verification succeeded - ansible.builtin.assert: - that: - - podman_link_quadlet_result.rc == 0 - fail_msg: "'/usr/libexec/podman/quadlet -dryrun -user' failed! Output withheld to prevent leaking secrets." - -#- name: set up nginx -# ansible.builtin.include_role: -# name: irl.wip.podman_nginx -# vars: -# podman_nginx_frontend_network: frontend -# podman_nginx_podman_rootless_user: "{{ podman_link_podman_rootless_user }}" -# podman_nginx_primary_hostname: "{{ podman_link_web_hostname }}" -# podman_nginx_systemd_service_slice: "link.slice" -# podman_nginx_systemd_service_requires: ["zammad-nginx"] -# -# -#- name: create nginx configuration file -# ansible.builtin.template: -# src: nginx.conf -# dest: "/home/{{ podman_link_podman_rootless_user }}/nginx/nginx.conf" -# owner: "{{ podman_link_podman_rootless_user }}" -# group: "{{ podman_link_podman_rootless_user }}" -# mode: "0644" -# become: true - -- name: install services slice for rootless podman user - ansible.builtin.template: - src: "link.slice" - dest: "/home/{{ podman_link_podman_rootless_user }}/.config/systemd/user/link.slice" - owner: "{{ podman_link_podman_rootless_user }}" - group: "{{ podman_link_podman_rootless_user }}" - mode: "0655" - become: true - -- name: make sure services are started on boot - ansible.builtin.systemd_service: - name: "link.slice" - enabled: true - state: started - daemon_reload: true - scope: user - become: true - become_user: "{{ podman_link_podman_rootless_user }}" - notify: - - "restart link.slice" - - -- name: set es verify false - ansible.builtin.shell: > - podman exec zammad-railsserver rails r "Setting.set('es_ssl_verify', false)" - become: true - become_user: "{{ podman_link_podman_rootless_user }}" - notify: - - "restart link.slice" - register: es_ssl_result - retries: 20 - delay: 5 - until: es_ssl_result.rc == 0 - -- name: Run OpenSearch setup script - ansible.builtin.shell: | - podman exec zammad-opensearch /bin/sh -c ' - if [ ! -f /tmp/.securityadmin_done ]; then - chmod +x /usr/share/opensearch/plugins/opensearch-security/tools/securityadmin.sh && \ - /usr/share/opensearch/plugins/opensearch-security/tools/securityadmin.sh \ - -cd /usr/share/opensearch/config/opensearch-security/ \ - -icl \ - -key /usr/share/opensearch/config/kirk-key.pem \ - -cert /usr/share/opensearch/config/kirk.pem \ - -cacert /usr/share/opensearch/config/root-ca.pem \ - -nhnv && \ - touch /tmp/.securityadmin_done - fi - ' - become: true - become_user: "{{ podman_link_podman_rootless_user }}" - register: securityadmin_scipt_result - retries: 20 - delay: 5 - until: securityadmin_scipt_result.rc == 0 - notify: - - "restart link.slice" - -- name: set up nginx - ansible.builtin.include_role: - name: irl.wip.podman_nginx - vars: - podman_nginx_frontend_network: frontend - podman_nginx_podman_rootless_user: "{{ podman_link_podman_rootless_user }}" - podman_nginx_primary_hostname: "{{ podman_link_web_hostname }}" - podman_nginx_systemd_service_slice: link.slice - podman_nginx_systemd_service_requires: ["zammad-nginx"] -# podman_nginx_additional_volumes: -# - src: "/home/{{ podman_cleaninsights_podman_rootless_user }}/matomo" -# dest: "/var/www/html" -# options: "ro" - -- name: create nginx configuration file - ansible.builtin.template: - src: nginx.conf - dest: "/home/{{ podman_link_podman_rootless_user }}/nginx/nginx.conf" - owner: "{{ podman_link_podman_rootless_user }}" - group: "{{ podman_link_podman_rootless_user }}" - mode: "0644" - become: true - notify: - - "restart link.slice" \ No newline at end of file diff --git a/roles/podman_link/templates/bridge-postgresql.container b/roles/podman_link/templates/bridge-postgresql.container deleted file mode 100644 index bd7fcae..0000000 --- a/roles/podman_link/templates/bridge-postgresql.container +++ /dev/null @@ -1,11 +0,0 @@ -[Container] -ContainerName=bridge-postgresql -EnvironmentFile=common-bridge.env -Image=registry.gitlab.com/digiresilience/link/link-stack/postgresql:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/bridge-postgresql-data:/var/lib/postgresql/data -Network=link.network - - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/bridge-whatsapp.container b/roles/podman_link/templates/bridge-whatsapp.container deleted file mode 100644 index 247847d..0000000 --- a/roles/podman_link/templates/bridge-whatsapp.container +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Requires=bridge-postgresql.service link.service -After=bridge-postgresql.service link.service - -[Container] -ContainerName=bridge-whatsapp -Environment=BRIDGE_FRONTEND_URL=http://systemd-link:3000/link -EnvironmentFile=common-bridge.env -ExposeHostPort=5000 -Image=registry.gitlab.com/digiresilience/link/link-stack/bridge-whatsapp:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/bridge-whatsapp-data:/home/node/baileys -Network=link.network - - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/bridge-worker.container b/roles/podman_link/templates/bridge-worker.container deleted file mode 100644 index 7f6f0ad..0000000 --- a/roles/podman_link/templates/bridge-worker.container +++ /dev/null @@ -1,13 +0,0 @@ -[Unit] -Requires=bridge-postgresql.service link.service -After=bridge-postgresql.service link.service - -[Container] -ContainerName=bridge-worker -EnvironmentFile=common-bridge.env -Image=registry.gitlab.com/digiresilience/link/link-stack/bridge-worker:3.1.0b2 -Network=link.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/common-bridge.env b/roles/podman_link/templates/common-bridge.env deleted file mode 100644 index ff147a4..0000000 --- a/roles/podman_link/templates/common-bridge.env +++ /dev/null @@ -1,18 +0,0 @@ -POSTGRES_USER={{ podman_link_postgres_link_user }} -POSTGRES_PASSWORD={{ podman_link_postgres_link_password }} -POSTGRES_DB={{ podman_link_postgres_link_database }} -NEXTAUTH_URL=https://{{ podman_link_web_hostname }}/link/api/auth -NEXTAUTH_SECRET={{ podman_link_nextauth_secret }} -GOOGLE_CLIENT_ID={{ podman_link_google_client_id }} -GOOGLE_CLIENT_SECRET={{ podman_link_google_client_secret }} -BRIDGE_FRONTEND_URL=http://link:3000 -BRIDGE_SIGNAL_URL=http://signal-cli-rest-api:8081 -BRIDGE_WHATSAPP_URL=http://bridge-whatsapp:5000 -DATABASE_NAME={{ podman_link_postgres_link_database }} -DATABASE_HOST=bridge-postgresql -DATABASE_USER={{ podman_link_postgres_link_user }} -DATABASE_PASSWORD={{ podman_link_postgres_link_password }} -DATABASE_PORT=5432 -DATABASE_URL=postgresql://{{ podman_link_postgres_link_user }}:{{ podman_link_postgres_link_password }}@bridge-postgresql/{{ podman_link_postgres_link_database}} -TZ=Etc/UTC - diff --git a/roles/podman_link/templates/common-zammad.env b/roles/podman_link/templates/common-zammad.env deleted file mode 100644 index 6cb0ff1..0000000 --- a/roles/podman_link/templates/common-zammad.env +++ /dev/null @@ -1,13 +0,0 @@ -POSTGRESQL_HOST=zammad-postgresql -POSTGRESQL_PASS={{ podman_link_postgres_zammad_password }} -POSTGRESQL_USER={{ podman_link_postgres_zammad_user }} -POSTGRESQL_DB={{ podman_link_postgres_zammad_database }} -POSTGRESQL_OPTIONS=?pool=50 -REDIS_URL=redis://default:{{ podman_link_zammad_redis_password }}@zammad-redis:6379 -MEMCACHE_SERVERS=zammad-memcached:11211 -ELASTICSEARCH_HOST=zammad-opensearch -ELASTICSEARCH_USER=admin -ELASTICSEARCH_PASS={{ podman_link_opensearch_password }} -ELASTICSEARCH_SCHEMA=https -ELASTICSEARCH_REINDEX=true -ELASTICSEARCH_SSL_VERIFY=false \ No newline at end of file diff --git a/roles/podman_link/templates/link.container b/roles/podman_link/templates/link.container deleted file mode 100644 index d9ce0b2..0000000 --- a/roles/podman_link/templates/link.container +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Requires=bridge-postgresql.service -After=bridge-postgresql.service - -[Container] -ContainerName=link -Environment=ZAMMAD_VIRTUAL_HOST={{ podman_link_web_hostname }} -Environment=SETUP_MODE={{ podman_link_setup_mode }} -Environment=LEAFCUTTER_ENABLED={{ podman_link_leafcutter_enabled }} -Environment=LEAFCUTTER_DEFAULT_DASHBOARD_URL={{ podman_link_dashboard_url }} -Environment=ZAMMAD_API_TOKEN={{ podman_link_zammad_api_token }} -Environment=LINK_URL=https://localhost:3000/link -Environment=ZAMMAD_URL=http://zammad-nginx:8080 -EnvironmentFile=common-bridge.env -ExposeHostPort=3000 -Image=registry.gitlab.com/digiresilience/link/link-stack/link:3.1.0b2 -Network=link.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/link.network b/roles/podman_link/templates/link.network deleted file mode 100644 index beae94f..0000000 --- a/roles/podman_link/templates/link.network +++ /dev/null @@ -1,2 +0,0 @@ -[Network] -NetworkName=link diff --git a/roles/podman_link/templates/link.slice b/roles/podman_link/templates/link.slice deleted file mode 100644 index fb3ff33..0000000 --- a/roles/podman_link/templates/link.slice +++ /dev/null @@ -1,22 +0,0 @@ -[Unit] -Description=Link Slice -Before=slices.target -Requires=zammad-init.service -Requires=zammad-memcached.service -Requires=zammad-postgresql.service -Requires=zammad-redis.service -Requires=zammad-railsserver.service -Requires=zammad-scheduler.service -Requires=zammad-websocket.service -Requires=zammad-opensearch.service -Requires=link.service -Requires=bridge-postgresql.service -Requires=zammad-nginx.service -Requires=signal-cli-rest-api.service -Requires=bridge-whatsapp.service -Requires=bridge-worker.service -Requires=opensearch-dashboards.service -Requires=nginx.service - -[Install] -WantedBy=default.target diff --git a/roles/podman_link/templates/nginx.conf b/roles/podman_link/templates/nginx.conf deleted file mode 100644 index af60818..0000000 --- a/roles/podman_link/templates/nginx.conf +++ /dev/null @@ -1,91 +0,0 @@ -# If we receive X-Forwarded-Proto, pass it through; otherwise, pass along the -# scheme used to connect to this server -map $http_x_forwarded_proto $proxy_x_forwarded_proto { - default $http_x_forwarded_proto; - '' $scheme; -} -# If we receive X-Forwarded-Port, pass it through; otherwise, pass along the -# server port the client connected to -map $http_x_forwarded_port $proxy_x_forwarded_port { - default $http_x_forwarded_port; - '' $server_port; -} -# If we receive Upgrade, set Connection to "upgrade"; otherwise, delete any -# Connection header that may have been passed to this server -map $http_upgrade $proxy_connection { - default upgrade; - '' close; -} -# Apply fix for very long server names -server_names_hash_bucket_size 128; -# Default dhparam -# Set appropriate X-Forwarded-Ssl header -map $scheme $proxy_x_forwarded_ssl { - default off; - https on; -} -gzip_types text/plain text/css application/javascript application/json application/x-javascript text/xml application/xml application/xml+rss text/javascript; -log_format vhost '$host $remote_addr - $remote_user [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent"'; -access_log off; -# HTTP 1.1 support -proxy_http_version 1.1; -proxy_buffering off; -proxy_set_header Host $http_host; -proxy_set_header Upgrade $http_upgrade; -proxy_set_header Connection $proxy_connection; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $proxy_x_forwarded_proto; -proxy_set_header X-Forwarded-Ssl $proxy_x_forwarded_ssl; -proxy_set_header X-Forwarded-Port $proxy_x_forwarded_port; -# Mitigate httpoxy attack (see README for details) -proxy_set_header Proxy ""; - -server { - listen 80; - listen [::]:80; - - server_name {{ podman_link_web_hostname }}; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 301 https://{{ podman_link_web_hostname }}$request_uri; - } -} - -upstream zammad { - server zammad-nginx:8080; -} - -server { - server_name {{ podman_link_web_hostname }}; - listen 80 ; - access_log /var/log/nginx/access.log vhost; - return 301 https://$host$request_uri; -} - -server { - server_name {{ podman_link_web_hostname }}; - listen 443 ssl; - listen [::]:443 ssl; - http2 on; - server_tokens off; - - ssl_certificate /etc/letsencrypt/live/{{ podman_link_web_hostname }}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/{{ podman_link_web_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 Matomo instance - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - location / { - proxy_pass http://zammad; - } -} \ No newline at end of file diff --git a/roles/podman_link/templates/opensearch-config.yml b/roles/podman_link/templates/opensearch-config.yml deleted file mode 100644 index 26a3230..0000000 --- a/roles/podman_link/templates/opensearch-config.yml +++ /dev/null @@ -1,36 +0,0 @@ -_meta: - type: "config" - config_version: 2 - -config: - dynamic: - http: - anonymous_auth_enabled: false - xff: - enabled: true - remoteIpHeader: "x-forwarded-for" - internalProxies: ".*" - authc: - basic_internal_auth_domain: - description: "Authenticate via HTTP Basic against internal users database" - http_enabled: true - transport_enabled: true - order: 0 - http_authenticator: - type: basic - challenge: false - authentication_backend: - type: intern - proxy_auth_domain: - description: "Authenticate via proxy" - http_enabled: true - transport_enabled: true - order: 1 - http_authenticator: - type: proxy - challenge: false - config: - user_header: "x-forwarded-user" - roles_header: "x-forwarded-roles" - authentication_backend: - type: noop \ No newline at end of file diff --git a/roles/podman_link/templates/opensearch-dashboards.container b/roles/podman_link/templates/opensearch-dashboards.container deleted file mode 100644 index 9c2d42f..0000000 --- a/roles/podman_link/templates/opensearch-dashboards.container +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Requires=zammad-opensearch.service -After=zammad-opensearch.service - -[Container] -ContainerName=opensearch-dashboards -Environment=OPENSEARCH_USERNAME="kibanaserver" -Environment=OPENSEARCH_PASSWORD="kibanaserver" -Image=registry.gitlab.com/digiresilience/link/link-stack/opensearch-dashboards:3.1.0b2 -PublishPort=127.0.0.1:5601:5601 -#Volume=/home/{{ podman_link_podman_rootless_user }}/opensearch-dashboards-config.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml -Network=link.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/signal-cli-rest-api.container b/roles/podman_link/templates/signal-cli-rest-api.container deleted file mode 100644 index 1b3b73e..0000000 --- a/roles/podman_link/templates/signal-cli-rest-api.container +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Requires=link.service -After=link.service - -[Container] -ContainerName=signal-cli-rest-api -Environment=MODE=native -Environment=PORT=8081 -ExposeHostPort=8081 -Image=registry.gitlab.com/digiresilience/link/link-stack/signal-cli-rest-api:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/signal-cli-rest-api-data:/home/.local/share/signal-cli -Network=link.network - -[Service] -Restart=always -slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/zammad-database.yml b/roles/podman_link/templates/zammad-database.yml deleted file mode 100644 index e655d15..0000000 --- a/roles/podman_link/templates/zammad-database.yml +++ /dev/null @@ -1,9 +0,0 @@ -production: - adapter: postgresql - database: {{ podman_link_postgres_zammad_database }} - pool: 50 - timeout: 5000 - encoding: utf8 - username: {{ podman_link_postgres_zammad_user }} - password: {{ podman_link_postgres_zammad_password }} - host: zammad-postgresql diff --git a/roles/podman_link/templates/zammad-init.container b/roles/podman_link/templates/zammad-init.container deleted file mode 100644 index 7de78a6..0000000 --- a/roles/podman_link/templates/zammad-init.container +++ /dev/null @@ -1,21 +0,0 @@ -[Unit] -Requires=zammad-postgresql.service -After=zammad-postgresql.service -Requires=zammad-redis.service -After=zammad-redis.service -Requires=zammad-memcached.service -After=zammad-memcached.service - -[Container] -ContainerName=zammad-init -EnvironmentFile=common-zammad.env -Exec=zammad-init -Image=registry.gitlab.com/digiresilience/link/link-stack/zammad:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-config-nginx:/etc/nginx/sites-enabled -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-var:/opt/zammad/var -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-storage:/opt/zammad/storage:ro -Network=link.network - -[Service] -Restart=on-failure -Slice=link.slice diff --git a/roles/podman_link/templates/zammad-memcached.container b/roles/podman_link/templates/zammad-memcached.container deleted file mode 100644 index d521fe8..0000000 --- a/roles/podman_link/templates/zammad-memcached.container +++ /dev/null @@ -1,10 +0,0 @@ -[Container] -ContainerName=zammad-memcached -Exec=memcached -m 256M -Image=registry.gitlab.com/digiresilience/link/link-stack/memcached:3.1.0b2 -Network=link.network -ExposeHostPort=11211 - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/zammad-nginx.container b/roles/podman_link/templates/zammad-nginx.container deleted file mode 100644 index 6778441..0000000 --- a/roles/podman_link/templates/zammad-nginx.container +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Requires=zammad-railsserver.service -After=zammad-railsserver.service - -[Container] -ContainerName=zammad-nginx -EnvironmentFile=common-zammad.env -Exec=zammad-nginx -ExposeHostPort=8080 -Image=registry.gitlab.com/digiresilience/link/link-stack/zammad:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-config-nginx:/etc/nginx/sites-enabled -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-var:/opt/zammad/var:ro -Network=link.network -Network=frontend.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/zammad-opensearch.container b/roles/podman_link/templates/zammad-opensearch.container deleted file mode 100644 index 9e0c058..0000000 --- a/roles/podman_link/templates/zammad-opensearch.container +++ /dev/null @@ -1,24 +0,0 @@ -[Container] -ContainerName=zammad-opensearch -Environment=discovery.type=single-node -Environment=plugins.security.ssl.transport.enforce_hostname_verification=false -Environment=plugins.security.ssl.transport.resolve_hostname=false -Environment=cluster.routing.allocation.disk.watermark.low=3gb -Environment=cluster.routing.allocation.disk.watermark.high=2gb -Environment=cluster.routing.allocation.disk.watermark.flood_stage=500mb -Environment=cluster.info.update.interval=1m -Environment=node.name=opensearch-node1 -Environment=bootstrap.memory_lock=true -Environment=OPENSEARCH_JAVA_OPTS="-Xms{{ podman_link_opensearch_memory_limit }}m -Xmx{{podman_link_opensearch_memory_limit }}m -XX:-HeapDumpOnOutOfMemoryError" -Environment=OPENSEARCH_INITIAL_ADMIN_PASSWORD={{ podman_link_opensearch_password }} -Environment=compatibility.override_main_response_version=true -Image=registry.gitlab.com/digiresilience/link/link-stack/opensearch:3.1.0b2 -PublishPort=127.0.0.1:9200:9200 -PublishPort=127.0.0.1:9600:9600 -Volume=/home/{{ podman_link_podman_rootless_user }}/opensearch-data:/usr/share/opensearch/data -Volume=/home/{{ podman_link_podman_rootless_user }}/opensearch-config.yml:/usr/share/opensearch/config/opensearch-security/config.yml -Network=link.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/zammad-postgresql.container b/roles/podman_link/templates/zammad-postgresql.container deleted file mode 100644 index 5fe6911..0000000 --- a/roles/podman_link/templates/zammad-postgresql.container +++ /dev/null @@ -1,17 +0,0 @@ -[Container] -ContainerName=zammad-postgresql -Environment=POSTGRES_PASSWORD={{ podman_link_postgres_zammad_password }} -Environment=POSTGRES_USER={{ podman_link_postgres_zammad_user }} -Environment=POSTGRES_DB={{ podman_link_postgres_zammad_database }} -Environment=POSTGRES_HOST_AUTH_METHOD=scram-sha-256 -Environment=POSTGRES_INITDB_ARGS=--auth-host=scram-sha-256 -Image=registry.gitlab.com/digiresilience/link/link-stack/postgresql:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/postgresql-data:/var/lib/postgresql/data -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-data:/opt/zammad -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-backup:/var/tmp/zammad:ro -Network=link.network - -[Service] -Restart=always -Slice=link.slice - diff --git a/roles/podman_link/templates/zammad-railsserver.container b/roles/podman_link/templates/zammad-railsserver.container deleted file mode 100644 index 5037130..0000000 --- a/roles/podman_link/templates/zammad-railsserver.container +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Requires=zammad-memcached.service zammad-redis.service zammad-postgresql.service -After=zammad-memcached.service zammad-redis.service zammad-postgresql.service - -[Container] -ContainerName=zammad-railsserver -EnvironmentFile=common-zammad.env -Exec=zammad-railsserver -Image=registry.gitlab.com/digiresilience/link/link-stack/zammad:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-var:/opt/zammad/var -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-storage:/opt/zammad/storage -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-database.yml:/opt/zammad/config/database.yml:ro -Network=link.network - -[Service] -Restart=always -Slice=link.slice diff --git a/roles/podman_link/templates/zammad-redis.container b/roles/podman_link/templates/zammad-redis.container deleted file mode 100644 index 28bd9dd..0000000 --- a/roles/podman_link/templates/zammad-redis.container +++ /dev/null @@ -1,10 +0,0 @@ -[Container] -ContainerName=zammad-redis -Environment=REDIS_PASSWORD={{ podman_link_zammad_redis_password }} -Image=registry.gitlab.com/digiresilience/link/link-stack/redis:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/redis-data:/data -Network=link.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_link/templates/zammad-scheduler.container b/roles/podman_link/templates/zammad-scheduler.container deleted file mode 100644 index f993b53..0000000 --- a/roles/podman_link/templates/zammad-scheduler.container +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Requires=zammad-memcached.service zammad-railsserver.service zammad-redis.service -After=zammad-memcached.service zammad-railsserver.service zammad-redis.service - -[Container] -ContainerName=zammad-scheduler -EnvironmentFile=common-zammad.env -Exec=zammad-scheduler -Image=registry.gitlab.com/digiresilience/link/link-stack/zammad:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-var:/opt/zammad/var -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-storage:/opt/zammad/storage -Network=link.network - -[Service] -Restart=always -Slice=link.slice diff --git a/roles/podman_link/templates/zammad-websocket.container b/roles/podman_link/templates/zammad-websocket.container deleted file mode 100644 index be46550..0000000 --- a/roles/podman_link/templates/zammad-websocket.container +++ /dev/null @@ -1,16 +0,0 @@ -[Unit] -Requires=zammad-memcached.service zammad-railsserver.service zammad-redis.service -After=zammad-memcached.service zammad-railsserver.service zammad-redis.service - -[Container] -ContainerName=zammad-websocket -EnvironmentFile=common-zammad.env -Exec=zammad-websocket -Image=registry.gitlab.com/digiresilience/link/link-stack/zammad:3.1.0b2 -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-var:/opt/zammad/var -Volume=/home/{{ podman_link_podman_rootless_user }}/zammad-storage:/opt/zammad/storage -Network=link.network - -[Service] -Restart=always -Slice=link.slice \ No newline at end of file diff --git a/roles/podman_nginx/defaults/main.yml b/roles/podman_nginx/defaults/main.yml new file mode 100644 index 0000000..5d6b76e --- /dev/null +++ b/roles/podman_nginx/defaults/main.yml @@ -0,0 +1,6 @@ +--- +podman_nginx_additional_hostnames: [] +podman_nginx_certbot_testing: false +# podman_nginx_frontend_network: +podman_nginx_podman_rootless_user: nginx +# podman_nginx_primary_hostname: \ No newline at end of file diff --git a/roles/podman_nginx/handlers/main.yml b/roles/podman_nginx/handlers/main.yml new file mode 100644 index 0000000..f1b17d9 --- /dev/null +++ b/roles/podman_nginx/handlers/main.yml @@ -0,0 +1,18 @@ +--- +- name: restart certbot-renew + ansible.builtin.systemd_service: + name: certbot-renew + state: started + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: restart nginx + ansible.builtin.systemd_service: + name: nginx + state: restarted + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" diff --git a/roles/podman_nginx/tasks/main.yml b/roles/podman_nginx/tasks/main.yml new file mode 100644 index 0000000..7d3f3c8 --- /dev/null +++ b/roles/podman_nginx/tasks/main.yml @@ -0,0 +1,110 @@ +--- +- name: create service configuration directories + ansible.builtin.file: + path: "/home/{{ podman_nginx_podman_rootless_user }}/{{ item }}" + state: directory + owner: "{{ podman_nginx_podman_rootless_user }}" + group: "{{ podman_nginx_podman_rootless_user }}" + mode: "0755" + become: true + with_items: + - .config/systemd/user + - certbot/conf + - certbot/www + - nginx + +- name: install podman quadlet for rootless podman user + ansible.builtin.template: + src: "{{ item }}" + dest: "/home/{{ podman_nginx_podman_rootless_user }}/.config/containers/systemd/{{ item }}" + owner: "{{ podman_nginx_podman_rootless_user }}" + mode: "0400" + with_items: + - certbot-renew.container + - nginx.container + notify: + - "restart {{ item | split('.') | first }}" + become: true + +- name: install certbot renewal timer for rootless podman user + ansible.builtin.template: + src: "certbot-renew.timer" + dest: "/home/{{ podman_nginx_podman_rootless_user }}/.config/systemd/user/certbot-renew.timer" + owner: "{{ podman_nginx_podman_rootless_user }}" + mode: "0400" + become: true + +- name: verify quadlets are correctly defined + ansible.builtin.command: /usr/libexec/podman/quadlet -dryrun -user + register: podman_nginx_quadlet_result + ignore_errors: true + changed_when: false + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: check if certificate exists + stat: + path: "/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf/live/{{ podman_nginx_primary_hostname }}/fullchain.pem" + register: podman_nginx_cert_stat + become: yes + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: create temporary nginx configuration (no https) + ansible.builtin.template: + src: nginx.conf + dest: "/home/{{ podman_nginx_podman_rootless_user }}/nginx/nginx.conf" + owner: "{{ podman_nginx_podman_rootless_user }}" + group: "{{ podman_nginx_podman_rootless_user }}" + mode: "0644" + become: true + when: podman_nginx_cert_stat.stat.exists == false + +- name: start nginx + ansible.builtin.systemd_service: + name: nginx + state: started + scope: user + daemon_reload: true + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: run certbot container to create certificate + ansible.builtin.command: + cmd: > + podman run --name certbot-generate + --rm + --volume /home/{{ podman_nginx_podman_rootless_user }}/certbot/www:/var/www/certbot:rw + --volume /home/{{ podman_nginx_podman_rootless_user }}/certbot/conf:/etc/letsencrypt:rw + docker.io/certbot/certbot:latest + certonly + --register-unsafely-without-email + --agree-tos + --webroot + --webroot-path /var/www/certbot/ + -d "{{ podman_nginx_primary_hostname }}" + {% for hostname in podman_nginx_additional_hostnames %} -d "{{ hostname }}"{% endfor %} + {% if podman_nginx_certbot_testing %} --test-cert{% endif %} + when: podman_nginx_cert_stat.stat.exists == false + become: yes + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: check if certificate exists + stat: + path: "/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf/live/{{ podman_nginx_primary_hostname }}/fullchain.pem" + register: podman_nginx_cert_stat + become: yes + become_user: "{{ podman_nginx_podman_rootless_user }}" + +- name: ensure certificate exists now + ansible.builtin.assert: + that: + - podman_nginx_cert_stat.stat.exists + fail_msg: "Failed to get a Lets Encrypt certificate." + +- name: start certbot renewal timer + ansible.builtin.systemd_service: + name: "certbot-renew.timer" + state: started + scope: user + become: true + become_user: "{{ podman_nginx_podman_rootless_user }}" diff --git a/roles/podman_nginx/templates/certbot-renew.container b/roles/podman_nginx/templates/certbot-renew.container new file mode 100644 index 0000000..7d28fd4 --- /dev/null +++ b/roles/podman_nginx/templates/certbot-renew.container @@ -0,0 +1,13 @@ +[Unit] +Description=Run certbot renew + +[Container] +AutoUpdate=registry +ContainerName=certbot-renew +Exec=renew +Image=docker.io/certbot/certbot:latest +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/www:/var/www/certbot +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf:/etc/letsencrypt + +[Service] +Restart=no diff --git a/roles/podman_nginx/templates/certbot-renew.timer b/roles/podman_nginx/templates/certbot-renew.timer new file mode 100644 index 0000000..65f32c1 --- /dev/null +++ b/roles/podman_nginx/templates/certbot-renew.timer @@ -0,0 +1,9 @@ +[Unit] +Description=Timer for certbot renewals + +[Timer] +OnCalendar=daily +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/roles/podman_nginx/templates/nginx.conf b/roles/podman_nginx/templates/nginx.conf new file mode 100644 index 0000000..aea3186 --- /dev/null +++ b/roles/podman_nginx/templates/nginx.conf @@ -0,0 +1,17 @@ +# {{ ansible_managed }} + +server { + listen 80; + listen [::]:80; + + server_name {{ podman_nginx_primary_hostname }}; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + return 301 https://{{ podman_nginx_primary_hostname }}$request_uri; + } +} diff --git a/roles/podman_nginx/templates/nginx.container b/roles/podman_nginx/templates/nginx.container new file mode 100644 index 0000000..fb8cafe --- /dev/null +++ b/roles/podman_nginx/templates/nginx.container @@ -0,0 +1,16 @@ +[Container] +ContainerName=nginx +Image=docker.io/nginx:latest +{% if podman_nginx_frontend_network is defined %}Network={{ podman_nginx_frontend_network }}.network{% endif +%} +PublishPort=80:80 +PublishPort=443:443 +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/www:/var/www/certbot/:ro +Volume=/home/{{ podman_nginx_podman_rootless_user }}/certbot/conf/:/etc/letsencrypt/:ro +Volume=/home/{{ podman_nginx_podman_rootless_user }}/nginx:/etc/nginx/conf.d/:ro + +[Service] +RuntimeMaxSec=604800 +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/vps/tasks/main.yml b/roles/vps/tasks/main.yml new file mode 100644 index 0000000..759a183 --- /dev/null +++ b/roles/vps/tasks/main.yml @@ -0,0 +1,35 @@ +--- +# https://support.solusvm.com/hc/en-us/articles/21334950006807-How-to-install-Guest-Tools-manually-inside-a-VM-in-SolusVM-2 + +- name: install required packages + apt: + pkg: + - qemu-guest-agent + - cloud-init + - tuned + state: latest + cache_valid_time: 3600 + become: true + when: ansible_distribution == 'Debian' + +- name: install required packages + dnf: + name: + - qemu-guest-agent + - cloud-init + - tuned + state: latest + update_cache: true + become: true + when: ansible_distribution == 'AlmaLinux' + +- name: check tuned profile + command: tuned-adm active + register: vps_tuned_profile + become: true + changed_when: false + +- name: start tuned profile + shell: tuned-adm profile virtual-guest + become: true + when: "'virtual-guest' not in vps_tuned_profile.stdout"