From a2334bb23665419e657e7d90f24870847a7a1666 Mon Sep 17 00:00:00 2001 From: il Date: Thu, 25 Jun 2026 15:17:35 +0900 Subject: [PATCH] feat(maddy): add mail stack --- ansible/inventory/group_vars/all.yaml | 50 +++++--- ansible/playbooks/infra/site.yaml | 8 ++ ansible/roles/infra/handlers/main.yaml | 11 ++ .../infra/tasks/services/set_grafana.yaml | 2 +- .../roles/infra/tasks/services/set_maddy.yaml | 107 ++++++++++++++++ .../infra/tasks/services/set_postgresql.yaml | 10 +- ansible/roles/vmm/tasks/node/set_libvirt.yaml | 2 + config/node/fw/nftables.conf.j2 | 3 +- config/node/infra/nftables.conf.j2 | 20 ++- config/secrets/secrets.yaml | 12 +- .../containers/infra/grafana/etc/ldap.toml.j2 | 2 +- .../infra/grafana/grafana.container.j2 | 2 +- .../containers/infra/maddy/data/maddy.conf.j2 | 114 ++++++++++++++++++ .../containers/infra/maddy/maddy.container.j2 | 36 ++++++ .../systemd/fw/bind/lib/db.1.00df.ip6.arpa | 2 +- .../fw/bind/lib/db.1.168.192.in-addr.arpa | 2 +- .../systemd/fw/bind/lib/db.10.00df.ip6.arpa | 2 +- .../fw/bind/lib/db.10.168.192.in-addr.arpa | 2 +- .../systemd/fw/bind/lib/db.ilnmors.com | 2 +- .../systemd/fw/bind/lib/db.ilnmors.internal | 3 +- data/sql/postgresql/.gitkeep | 0 21 files changed, 350 insertions(+), 42 deletions(-) create mode 100644 ansible/roles/infra/tasks/services/set_maddy.yaml create mode 100644 config/services/containers/infra/maddy/data/maddy.conf.j2 create mode 100644 config/services/containers/infra/maddy/maddy.container.j2 delete mode 100644 data/sql/postgresql/.gitkeep diff --git a/ansible/inventory/group_vars/all.yaml b/ansible/inventory/group_vars/all.yaml index 27a00ee..bc38582 100644 --- a/ansible/inventory/group_vars/all.yaml +++ b/ansible/inventory/group_vars/all.yaml @@ -20,6 +20,7 @@ domain: # Check the hosts.j2 when cname records are fixed services: + # common crowdsec: domain: internal: "crowdsec" @@ -27,6 +28,18 @@ services: https: "8080" dependency: cert: true + kopia: + domain: + internal: "kopia" + ports: + https: "51515" + dependency: + cert: true + caddy: + ports: + http: "2080" + https: "2443" + # fw bind: domain: internal: "bind" @@ -41,13 +54,7 @@ services: dot: "853" dependency: cert: true - kopia: - domain: - internal: "kopia" - ports: - https: "51515" - dependency: - cert: true + # infra postgresql: domain: internal: "postgresql" @@ -67,13 +74,15 @@ services: cert: true web: "infra-internal" db: "postgresql" - mail: + maddy: domain: - internal: "mail" - ports: "" - subuid: "" + internal: "maddy" + ports: + smtp: "2587" + imaps: "2993" dependency: cert: true + db: "postgresql" ca: domain: internal: "ca" @@ -110,10 +119,7 @@ services: dependency: web: "infra-internal" db: "postgresql" - caddy: - ports: - http: "2080" - https: "2443" + # auth authelia: domain: public: "authelia" @@ -124,6 +130,7 @@ services: db: "postgresql" redis: subuid: "100998" + # app homepage: domain: public: "@" @@ -259,16 +266,19 @@ version: caddy: "2.11.4" # infra step: "0.30.2" + ## Postgresql + postgresql: "18.4" + ### For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml + ### pgvector: "v0.8.1" + vectorchord: "1.1.1" + ## lldap ldap: "v0.6.3" + ## maddy + maddy: "0.9.5" x509-exporter: "4.1.0" prometheus: "v3.12.0" loki: "3.7.1" grafana: "13.0.1" - ## Postgresql - postgresql: "18.4" - # For immich - https://github.com/immich-app/base-images/blob/main/postgres/versions.yaml -# pgvector: "v0.8.1" - vectorchord: "1.1.1" # Auth authelia: "4.39.20" # App diff --git a/ansible/playbooks/infra/site.yaml b/ansible/playbooks/infra/site.yaml index 540ad6f..154ffb3 100644 --- a/ansible/playbooks/infra/site.yaml +++ b/ansible/playbooks/infra/site.yaml @@ -178,6 +178,14 @@ tags: ["site", "ldap"] tags: ["site", "ldap"] + - name: Set maddy + ansible.builtin.include_role: + name: "infra" + tasks_from: "services/set_maddy" + apply: + tags: ["site", "maddy"] + tags: ["site", "maddy"] + - name: Set x509 exporter ansible.builtin.include_role: name: "infra" diff --git a/ansible/roles/infra/handlers/main.yaml b/ansible/roles/infra/handlers/main.yaml index 2586a99..e925898 100644 --- a/ansible/roles/infra/handlers/main.yaml +++ b/ansible/roles/infra/handlers/main.yaml @@ -83,3 +83,14 @@ changed_when: false listen: "notification_restart_x509-exporter" ignore_errors: true # noqa: ignore-errors + +- name: Restart maddy.service + ansible.builtin.systemd: + name: "maddy.service" + state: "restarted" + enabled: true + daemon_reload: true + scope: "user" + changed_when: false + listen: "notification_restart_maddy" + ignore_errors: true # noqa: ignore-errors diff --git a/ansible/roles/infra/tasks/services/set_grafana.yaml b/ansible/roles/infra/tasks/services/set_grafana.yaml index b39820f..8cfcf19 100644 --- a/ansible/roles/infra/tasks/services/set_grafana.yaml +++ b/ansible/roles/infra/tasks/services/set_grafana.yaml @@ -36,7 +36,7 @@ state: "present" force: true loop: - - name: "LDAP_BIND_PASSWORD" + - name: "GF_LDAP_BIND_PASSWORD" value: "{{ hostvars['console']['ldap']['password']['services']['grafana'] }}" - name: "GF_DB_PASSWORD" value: "{{ hostvars['console']['postgresql']['password']['grafana'] }}" diff --git a/ansible/roles/infra/tasks/services/set_maddy.yaml b/ansible/roles/infra/tasks/services/set_maddy.yaml new file mode 100644 index 0000000..b76e03e --- /dev/null +++ b/ansible/roles/infra/tasks/services/set_maddy.yaml @@ -0,0 +1,107 @@ +--- +- name: Create maddy directory + ansible.builtin.file: + path: "{{ node['home_path'] }}/{{ item }}" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + state: "directory" + mode: "0770" + loop: + - "containers/maddy" + - "containers/maddy/ssl" + - "data/containers/maddy" + - "data/containers/maddy/data" + +- name: Deploy maddy certificate and key + ansible.builtin.copy: + content: | + {{ item.value }} + dest: "{{ node['home_path'] }}/containers/maddy/ssl/{{ item.name }}" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "{{ item.mode }}" + loop: + - name: "{{ root_cert_filename }}" + value: "{{ hostvars['console']['ca']['root']['crt'] }}" + mode: "0440" + - name: "maddy.crt" + value: | + {{ hostvars['console']['maddy']['crt'] | trim }} + {{ hostvars['console']['ca']['intermediate']['crt'] }} + mode: "0440" + - name: "maddy.key" + value: "{{ hostvars['console']['maddy']['key'] }}" + mode: "0400" + become: true + notify: "notification_restart_maddy" + no_log: true + +- name: Register secret value to podman secret + containers.podman.podman_secret: + name: "{{ item.name }}" + data: "{{ item.value }}" + state: "present" + force: true + loop: + - name: "MADDY_LDAP_BIND_PASSWORD" + value: "{{ hostvars['console']['ldap']['password']['services']['maddy'] }}" + - name: "MADDY_POSTGRES_URL" + value: "postgresql://maddy:{{ hostvars['console']['postgresql']['password']['maddy'] | urlencode | replace('/', '%2F') }}\ + @{{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}/maddy_db?sslmode=verify-full&\ + sslrootcert=/etc/ssl/maddy/{{ root_cert_filename }}" + notify: "notification_restart_maddy" + no_log: true + +- name: Deploy configuration files + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/maddy/data/maddy.conf.j2" + dest: "{{ node['home_path'] }}/containers/maddy/maddy.conf" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0400" + notify: "notification_restart_maddy" + no_log: true + +- name: Pull image + containers.podman.podman_image: + name: "docker.io/foxcpp/maddy" + tag: "{{ version['containers']['maddy'] }}" + state: "present" + pull: true + +- name: Deploy container file + ansible.builtin.template: + src: "{{ hostvars['console']['node']['config_path'] }}/services/containers/infra/maddy/maddy.container.j2" + dest: "{{ node['home_path'] }}/.config/containers/systemd/maddy.container" + owner: "{{ ansible_user }}" + group: "{{ node['group'] }}" + mode: "0644" + notify: "notification_restart_maddy" + +- name: Enable maddy + ansible.builtin.systemd: + name: "maddy.service" + state: "started" + enabled: true + daemon_reload: true + scope: "user" + +- name: Check container archive images + ansible.builtin.stat: + path: "{{ node['home_path'] }}/archives/containers/docker.io_foxcpp_maddy_{{ version['containers']['maddy'] }}.tar" + register: container_archive_images + +- name: Save container archive images + containers.podman.podman_save: + image: + - "docker.io/foxcpp/maddy:{{ version['containers']['maddy'] }}" + dest: "{{ node['home_path'] }}/archives/containers/docker.io_foxcpp_maddy_{{ version['containers']['maddy'] }}.tar" + format: "oci-archive" + force: false + when: not container_archive_images.stat.exists + +- name: Fetch container archive images + ansible.builtin.fetch: + src: "{{ node['home_path'] }}/archives/containers/docker.io_foxcpp_maddy_{{ version['containers']['maddy'] }}.tar" + dest: "{{ hostvars['console']['node']['data_path'] }}/images/containers/" + flat: true diff --git a/ansible/roles/infra/tasks/services/set_postgresql.yaml b/ansible/roles/infra/tasks/services/set_postgresql.yaml index eac2483..4f1913b 100644 --- a/ansible/roles/infra/tasks/services/set_postgresql.yaml +++ b/ansible/roles/infra/tasks/services/set_postgresql.yaml @@ -15,6 +15,12 @@ | list }} +- name: Delete existing init files + ansible.builtin.file: + path: "{{ node['home_path'] }}/containers/postgresql/init" + state: "absent" + become: true + - name: Create postgresql directory ansible.builtin.file: path: "{{ node['home_path'] }}/{{ item }}" @@ -109,10 +115,10 @@ - name: Validate initiating SQL file count ansible.builtin.assert: that: - - postgresql_init_sql_files.matched == 0 or postgresql_init_sql_files.matched == ((connected_services | length) + 1) + - postgresql_init_sql_files.matched == 0 or postgresql_init_sql_files.matched <= ((connected_services | length) + 1) fail_msg: >- Init SQL count mismatch. - expected=0 or {{ (connected_services | length) + 1 }}, + expected=0 or above {{ (connected_services | length) + 1 }}, actual={{ postgresql_init_sql_files.matched }}. Check pg_cluster.sql and pg_.sql files in {{ node['home_path'] }}/data/containers/postgresql/backups/sql/ diff --git a/ansible/roles/vmm/tasks/node/set_libvirt.yaml b/ansible/roles/vmm/tasks/node/set_libvirt.yaml index bf57366..c5e06a8 100644 --- a/ansible/roles/vmm/tasks/node/set_libvirt.yaml +++ b/ansible/roles/vmm/tasks/node/set_libvirt.yaml @@ -26,6 +26,8 @@ group: "root" mode: "0711" loop: "{{ is_libvirt_dir.results }}" + loop_control: + label: "{{ item.item }}" when: not item.stat.exists become: true diff --git a/config/node/fw/nftables.conf.j2 b/config/node/fw/nftables.conf.j2 index e2c6161..6d942cb 100644 --- a/config/node/fw/nftables.conf.j2 +++ b/config/node/fw/nftables.conf.j2 @@ -49,8 +49,7 @@ define PORTS_DHCP = { 67, 68, 546, 547 } define PORTS_DNS = 53 define PORTS_NTP = 123 define PORTS_VPN = 11290 -define PORTS_CROWDSEC = 8080 -define PORTS_NAS = { 5000, 5001 } +define PORTS_CROWDSEC = {{ services['crowdsec']['ports']['https'] }} table inet nat { chain prerouting { diff --git a/config/node/infra/nftables.conf.j2 b/config/node/infra/nftables.conf.j2 index 66b8dd4..b6588aa 100644 --- a/config/node/infra/nftables.conf.j2 +++ b/config/node/infra/nftables.conf.j2 @@ -9,15 +9,19 @@ define HOSTS4_CONSOLE = { {{ hostvars['fw']['network4']['console'].values() | jo define HOSTS6_CONSOLE = { {{ hostvars['fw']['network6']['console'].values() | join(', ') }} } define PORTS_SSH = 22 define PORTS_DB = 5432 -define PORTS_CA = 9000 +define PORTS_CA = {{ services['ca']['ports']['https'] }} define PORTS_LDAPS = 636 -define PORTS_LDAPS_FORWARD = 6360 +define PORTS_LDAPS_FORWARD = {{ services['ldap']['ports']['ldaps'] }} define PORTS_HTTP = 80 define PORTS_HTTP_FORWARD = 2080 define PORTS_HTTPS = 443 define PORTS_HTTPS_FORWARD = 2443 -define PORTS_PROMETHEUS = 9090 -define PORTS_LOKI = 3100 +define PORTS_PROMETHEUS = {{ services['prometheus']['ports']['https'] }} +define PORTS_LOKI = {{ services['loki']['ports']['https'] }} +define PORTS_SMTP = 587 +define PORTS_SMTP_FORWARD = {{ services['maddy']['ports']['smtp'] }} +define PORTS_IMAPS = 993 +define PORTS_IMAPS_FORWARD = {{ services['maddy']['ports']['imaps'] }} table inet nat { chain prerouting { @@ -25,6 +29,8 @@ table inet nat { tcp dport $PORTS_HTTP dnat to :$PORTS_HTTP_FORWARD comment "DNAT http ports to $PORTS_HTTP_FORWARD" tcp dport $PORTS_HTTPS dnat to :$PORTS_HTTPS_FORWARD comment "DNAT https ports to $PORTS_HTTPS_FORWARD" tcp dport $PORTS_LDAPS dnat to :$PORTS_LDAPS_FORWARD comment "DNAT ldaps ports to $PORTS_LDAPS_FORWARD" + tcp dport $PORTS_SMTP dnat to :$PORTS_SMTP_FORWARD comment "DNAT smtp ports to $PORTS_SMTP_FORWARD" + tcp dport $PORTS_IMAPS dnat to :$PORTS_IMAPS_FORWARD comment "DNAT imaps ports to $PORTS_IMAPS_FORWARD" } chain postrouting { @@ -34,6 +40,8 @@ table inet nat { oifname "lo" tcp dport $PORTS_HTTP dnat to :$PORTS_HTTP_FORWARD comment "DNAT http ports to $PORTS_HTTP_FORWARD out of LOCALHOST" oifname "lo" tcp dport $PORTS_HTTPS dnat to :$PORTS_HTTPS_FORWARD comment "DNAT https ports to $PORTS_HTTPS_FORWARD out of LOCALHOST" oifname "lo" tcp dport $PORTS_LDAPS dnat to :$PORTS_LDAPS_FORWARD comment "DNAT ldaps ports to $PORTS_LDAPS_FORWARD out of LOCALHOST" + oifname "lo" tcp dport $PORTS_SMTP dnat to :$PORTS_SMTP_FORWARD comment "DNAT smtp ports to $PORTS_SMTP_FORWARD" + oifname "lo" tcp dport $PORTS_IMAPS dnat to :$PORTS_IMAPS_FORWARD comment "DNAT imaps ports to $PORTS_IMAPS_FORWARD" } } @@ -60,6 +68,10 @@ table inet filter { ip6 saddr $NET6_SERVER tcp dport $PORTS_PROMETHEUS accept comment "allow ipv6 prometheus connection: SERVER > INFRA" ip saddr $NET4_SERVER tcp dport $PORTS_LOKI accept comment "allow ipv4 loki connection: SERVER > INFRA" ip6 saddr $NET6_SERVER tcp dport $PORTS_LOKI accept comment "allow ipv6 loki connection: SERVER > INFRA" + ip saddr $NET4_SERVER tcp dport $PORTS_SMTP_FORWARD ct original proto-dst $PORTS_SMTP accept comment "allow ipv4 smtp connection: SERVER > INFRA" + ip6 saddr $NET6_SERVER tcp dport $PORTS_SMTP_FORWARD ct original proto-dst $PORTS_SMTP accept comment "allow ipv6 smtp connection: SERVER > INFRA" + ip saddr $NET4_SERVER tcp dport $PORTS_IMAPS_FORWARD ct original proto-dst $PORTS_IMAPS accept comment "allow ipv4 imaps connection: SERVER > INFRA" + ip6 saddr $NET6_SERVER tcp dport $PORTS_IMAPS_FORWARD ct original proto-dst $PORTS_IMAPS accept comment "allow ipv6 imaps connection: SERVER > INFRA" } chain forward { type filter hook forward priority 0; policy drop; diff --git a/config/secrets/secrets.yaml b/config/secrets/secrets.yaml index b9aaafb..dc07aec 100644 --- a/config/secrets/secrets.yaml +++ b/config/secrets/secrets.yaml @@ -126,6 +126,7 @@ postgresql: postgres: ENC[AES256_GCM,data:XlpsKT0/3VgNesMdifvHlYZ2dh7mlMEnxi0MJpQZiQ+1tZ0/3zMxQ7+vwec=,iv:akcPRmDZQfFLxelG7V87UnNB8ez28PPLCmY1MYfzStU=,tag:14jvIKP7Htfh+/oMwLYC2Q==,type:str] alloy: ENC[AES256_GCM,data:ifgJH2e7MTAJ/+ppdWHEmLC3DAsKHWNi8j4w1ZVjUh4tN1cDLDJR3EvJTl0=,iv:rDYUkVe6idIf5Si5E9V+mvBaIdYQTJJCB2rOV0Suvr0=,tag:xDCL25NwuTp6H1DOGTIm4w==,type:str] ldap: ENC[AES256_GCM,data:mJrxIhXynHxJhncw3upHpOkXIw+Ka9bmDBJwkDjYl+D9Pg4RDvL6WzBjthw=,iv:y8MUYo6VhgTzbWh/+n7/hf1Jw+L2KcdxKvulPJ67xn8=,tag:4ZFpj1UdOwXmaZjYvC/s3A==,type:str] + maddy: ENC[AES256_GCM,data:D8cFa65up8DVGBiIAXvh/2BdYoeZ9X4wPZmCU6aQ6p37+4edBhsl6VWYhH4=,iv:AORId0QsAkeIQH2IMcI+HKFKW7NLrZ0GXi0yCn2NK5Y=,tag:RtI/Vn8WydkjIiNZB9om/w==,type:str] grafana: ENC[AES256_GCM,data:P9okJ7bcsqmeGstkSwbDq/RgnG+lFrgAOvcj8A5lOTpmHaSlXGiKG+ybXa0=,iv:Di1ghnxIbAb/u7uo/mJCC3QYVjdweTHaQDZmXTx8OG4=,tag:DT3a1zgU9sTr0BXpyoZ/SQ==,type:str] authelia: ENC[AES256_GCM,data:OqyloAADO6KKEaBjGLsJc9GTe77wn6IvA1VCD2dfCWxx+zgzUYh87fK1XX8=,iv:QIOHNTdNnzcY/f3Co8dPdNHykhBnYRhm43nt35hbALM=,tag:DLQq58GrZd+Ul7MSn6s9uQ==,type:str] vaultwarden: ENC[AES256_GCM,data:BPj5eFo54DTZ82n3yTIqEbm7kb/jWT0n2kZY//oV5q48eRch3C2RBuxn/Ko=,iv:DGC4ipHMyVs25gc4sNMt8LN1RsHjiR/b303vgiFoxMY=,tag:k1eb4DoRPLKvvMstSI1faQ==,type:str] @@ -156,6 +157,7 @@ ldap: admin: ENC[AES256_GCM,data:xMBoPycO/5wDae2AMZlgomoXjB98BpCOQkxek3W7t7Wk+az1GMqJhdB0osY=,iv:E6nNmNNKlJoNC7KwE0PaDIB8QyGM0MuNutQty3bi1Pc=,tag:yc8nlegBUOX2hfaMBoNaCw==,type:str] authelia: ENC[AES256_GCM,data:1hHNPWgiRte/TJr4gBuwFt3zMHPivgRp7Z2kurYG+bT96HWmwk1+4iKSQVM=,iv:cpV2P14daWabaDuQ9KgrGs8bN+4pc7sXbAUE2mjWQL0=,tag:lsLgHkkhgSPfwxC6vu5JEg==,type:str] grafana: ENC[AES256_GCM,data:42rnv/NMwmvHdyWW6PZj1Lto5mRDTHp6h55OuRYd9xMOtmenX5zBJzGMvnE=,iv:jUKuPisxa/S9erKjXHl3y0zJR+Tfe36O85/BdGmN+gE=,tag:h1InzKaa+bWjlmIq9OZRpw==,type:str] + maddy: ENC[AES256_GCM,data:+E8ggJax1s7Nwdc8EpAWHnVuM0ceJqpW++z4UGeTpPN0DYDPqNSBi/7rW6s=,iv:n+iMKOBjuyeVNRLZQTr+3/Fuu3KEYolLjCXLZ9lTBvk=,tag:B5NZO7zKxh+7nN29vvE1Jw==,type:str] users: il: ENC[AES256_GCM,data:GcOsUe7yJFLdhPp2H7i2,iv:T4NGNvYe5beYbxr+ezfT86aS5lCBa/TG3bUWuUrCZ+A=,tag:rrdHtLyZI7pdTGq5qmptYw==,type:str] morsalin: ENC[AES256_GCM,data:86+imwqNJ5Xg,iv:Lgt1I12gtDsjvE1yMjiyed0sZEeExQZoJPD+xKzHBqk=,tag:YiQgczFZnzh7p1t+KIzM6Q==,type:str] @@ -163,9 +165,9 @@ ldap: # # #ENC[AES256_GCM,data:6RVyF1gPUfKEvkV3RQ==,iv:0MTKlm5kE0kZi3E013Kk/Wo642BizBRmcHXR+Zwf/W8=,tag:SaQJ7lvFUuALxXWf41C24A==,type:comment] -mail: - key: ENC[AES256_GCM,data:2iT93HHmX3eCTtKb8/C5mI2uEwdck7R6TCXoLuWprfs6KMza8mwd+qDeYGauUHUojEplmopycWup1cVWyQRN2B3udxd7nR9YXcdL0ZNvjmEcjRKDicHEhIw3DVb7N5GuGkFDhc4dEx4gen5Lwu3c+b2hpL58fvZNmrldKV1Qs2jnyi4LcFxEaldPNAiJJA5CYC3M/FFrEDw+Zh1VP6rliz0wYvEoqsIanKgDs5sVkSw7IT9iBidg8EagPrlSEcYJOSHlNVJU2a28P45az0G1drw4BWiXASmCWk6CNr2WT26vz24=,iv:8h45V8wA83AHrej7ajzV3z3KUS2eu0fegjPJk7l8YoQ=,tag:kD+QVBEur6xSI0iLEthWeg==,type:str] - crt: ENC[AES256_GCM,data:v1nr+IQowiph7eRyENALTAXHt7NPoEdbyBcb3lVxUp9Ga6nGs7IPSyzMTfc3Am4cZ/6kRpXJG30cjINWpfhFkruS6SCPqMUcsxkVw6oLArbN62wV4umws6xh0pQNajTGY8ZNhya4RudWSLAtmIlbI0aneJKxrypPQKXeX+nML48TLcV8PicAhr4gyEk967IaHLCPK3GAuDwrD/b5NsaU3sVb5bHwcSsUKUBzdAYdHTDwsuOVwbrYT0egQ+Bcn44bY/EWw8SbIa90A+iqU6R4G9V2+/eHCSdqaNX27Yp+h3YgRJH+NqItiMlgeJ99bjvYjdSaOucmUE9gcUt7BFHpEuCI6nBGduFu/PEsFd1vNB/jKjLidN/wjcHHXXfmLy1vw8ut8ns9cl3Ufyk2VcwTfexKTuZbcAIlDdc2HGB03Z8bju8Ci96+j6cZEgv04eUZSl/5erRuyrIrA/erC09J3gqz3ojMTPsGxmQs8Rbkv6ZWM2mrsBizZq3K3XMCAP/eC1072pnOjgE91PzDUmQwBSm2xxyvjDDjJxp0M5EVdTQ/GiJXtV/Fwq8NvhI1P3nkQoDOEz+i3I1ZNrsRPLoaC+HKd+DdnO24slTUXXYO2/j9/Y3z+gjj25WR3Q1iFzKXcVH5pPVToDavMKLhlchcXI0FvqjKPYyqAY1uNEg1oVlqcyD/CcOUur1ugNQGriXPf+//nb3WZgxfcZiCwyee4oCAa3OxSMIaENn3QQ6QVGNocNanvulcA9LwIMU8qXp0+AixtFvbTlM/LHBWy10YDH2LKg7kZ9ePd6JnSFYIdB8gKhnbolrtLVIpC9NDcyzYosFrZ1NsvGgEiy+5+tLl+XERaZunulZ3VBG+4w583/PfeIZ4l7kRde1HWkym5oI0IluQG9MagyIv2a2fSScZEIClDPkOJqFaL6WszVViAgB3pAL3dEr8+48=,iv:bByFvLj8L+yZ2ErlkDBHGRhnNVQsFOe7q3WwyGVWQOU=,tag:VK8iwa8m0thEBNyTvEZOZw==,type:str] +maddy: + key: ENC[AES256_GCM,data:NN1w/AQAOac7898/tk2W8qDe0TA1W4rwe2USinUW3CxvQO3Gy2oGgcHT7xKDOxnqz/Ec3rBBFZ3v1rAH7236L6GNQ700pFRuddn0qdMukLHJGbY3mWGRWVQYwV1JX3WVdM2HMrrmLKXe1ArsiTfRVjSvVj0kVNjnMwCSFl/zNe2PgwIMSLi+L5KeJ+G5OOywmR3ctQPJQQefeu+eXXkDFPRwENQlD8i5H1K9vSwRVy09DKzFtc74Xi3srVozWE6U+bCTE/rRdxWN+N0wJNl8tUO9Ndbw98rRaD6WdARRXta6nfU=,iv:MpSJnnrragy8hCOwytSuTfJ3i8+8DnSEYUemDK6rBCM=,tag:pjWVUZSUhVsx2nCMzNpXSQ==,type:str] + crt: ENC[AES256_GCM,data:O912cPVNMp7H/pEtrzaqv3QckgoAdf3E36E0G4yvVVUwfhfnxTwlLkQ3uk2FGA6VpCuwOvSBYiSnngL0/55hfulVTWmW32oT5pkW9c8UhyA+769o4YrDiX7yG/4YbFyO4JK44f/J/z1zZc7E6Oni5z9GOTQuo4SjRqM3iT+pDqnTRnB26HVSSAZBgGJpL/6lFM/WKheDGrzh/SVJmgr+/uHEWJTuo/L+CsxZ1Qn3WDj4/s7HtCu8I5qvEUPwII86wo6FNXmhTapjIyNDsbgvr/QgNGEvSvVQtSaMyoKWEOKPRIOhdvcZ36qSegqVLO6pvBtvrZabMsNDnTyraAUo7ePs4t+5NM9xAfwJ8vrkqkqYMS22prdHBy5mYebczFQdxGhr+kDi0itAVWZULIXnc/7fptrwoB7jJjO3i9bHSfrXx3DdpU8ivIi/snwKDsA24g1tqK5/y4dgCNakzp1GhZVrPT3WKnqCUPapn7vjEjucClg3m+5poNmdUZs1SgGRhqpeBz1inUzhsBPrk+qIPXXY5eSZ37DH8hQG0NnxsgFiPpLAn/5yfHUI3eVoS6i2kNbK4KkpD2sJ/amRX7Pxder5+tIIu8GzmN8EgfgucV8/DlrX+Z0u6HvA9lOxDxMR+/Bast5e9jc7dCKY25x8oun3OAiG0kAAsJYVcB0/wvskYJoBoKOICRjF8kf0w27kxMgCuFh4LLaqeBbtBXh7lKXjD3gEcTZgPWnxeb63RCWZqoTBGeyUcBkGByAIclhbOHClDG/X397pM0Rn+QbaR04LSGgRPF5NCKqbwmvh8z24b0n6mvyrBpBqhCxdlBz1f5RqlIaCGuy/oR0E2vEfeMiz5LAFZydALZM1v7GkoiRPPasYgMhoHfeSPfCgCyFugtGfU0SqtyksJQxYQUzKAuchALHAi/PCQ7YtXq/1K6C7aK4/Z8+Fy+0=,iv:+MJeh6rwG6PtAp5o5v2wtlhd1wrXkPCdHmEed70F0eQ=,tag:xH9CxCF8mC7PCV0xeE9UCA==,type:str] #ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment] # # @@ -343,7 +345,7 @@ sops: k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ== -----END AGE ENCRYPTED FILE----- recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y - lastmodified: "2026-06-25T00:07:39Z" - mac: ENC[AES256_GCM,data:XNOyATqwo6BygVz6nzBZx6Jn8YONifcvwRKFxWsVxy+aqRRCbUA4eY3g9s6EstpZwHLpf1KeH6iS1ogzwlZfw1Xp8xaWjXgZTEV9R6jEKRzZ4pwPnmC6UVTfrJ2XBB/mjTel0jSR6gbKSgXMJw4rsEyvth+tPxMAaHvrIEcFGqE=,iv:2W2ohbVz4zFDFHG6T5loayab6nOsZaJ/OwqTfNNhEYs=,tag:TNoVHAW5g4/pAOdqolmD7Q==,type:str] + lastmodified: "2026-06-25T05:44:30Z" + mac: ENC[AES256_GCM,data:eVRWoJ48BjmflUJoXy3hJpf48H4+7zum47xBHZi674UFH4wSWa/01ctq94fXLNeGpovbaPnh2997c33lm26JVXNfSUPzkjALzf5e7pzMLoxnMbpHrAqyctfcx8PxLXWW1DBNOym0Bi0CM8F4BDsuroQPDDtVGzZk2mouzl0iJqo=,iv:ZRqcYFRxae2cnhwAEbHlq66ydV7PbvSdDlu+Nl2dYlw=,tag:bIAhfpzaJhO+STMC34Qxnw==,type:str] unencrypted_suffix: _unencrypted version: 3.13.1 diff --git a/config/services/containers/infra/grafana/etc/ldap.toml.j2 b/config/services/containers/infra/grafana/etc/ldap.toml.j2 index 9f960fc..57078f3 100644 --- a/config/services/containers/infra/grafana/etc/ldap.toml.j2 +++ b/config/services/containers/infra/grafana/etc/ldap.toml.j2 @@ -15,7 +15,7 @@ root_ca_cert = "/etc/ssl/grafana/{{ root_cert_filename }}" # client_key = "/path/to/client.key" bind_dn = "uid=grafana,ou=people,{{ domain['dc'] }}" -bind_password = "$__file{/run/secrets/LDAP_BIND_PASSWORD}" +bind_password = "$__file{/run/secrets/GF_LDAP_BIND_PASSWORD}" search_filter = "(&(|(uid=%s)(mail=%s))(|(memberOf=cn=admins,ou=groups,{{ domain['dc'] }})(memberOf=cn=users,ou=groups,{{ domain['dc'] }})))" search_base_dns = ["{{ domain['dc'] }}"] diff --git a/config/services/containers/infra/grafana/grafana.container.j2 b/config/services/containers/infra/grafana/grafana.container.j2 index a2d485d..9affef4 100644 --- a/config/services/containers/infra/grafana/grafana.container.j2 +++ b/config/services/containers/infra/grafana/grafana.container.j2 @@ -31,7 +31,7 @@ Environment="GF_PATHS_CONFIG=/etc/grafana/grafana.ini" Environment="GF_FEATURE_TOGGLES_EXPAND_ENV_VARS=true" Secret=GF_DB_PASSWORD,target=/run/secrets/GF_DB_PASSWORD -Secret=LDAP_BIND_PASSWORD,target=/run/secrets/LDAP_BIND_PASSWORD +Secret=GF_LDAP_BIND_PASSWORD,target=/run/secrets/GF_LDAP_BIND_PASSWORD Secret=GF_ADMIN_PASSWORD,target=/run/secrets/GF_ADMIN_PASSWORD [Service] diff --git a/config/services/containers/infra/maddy/data/maddy.conf.j2 b/config/services/containers/infra/maddy/data/maddy.conf.j2 new file mode 100644 index 0000000..26bd454 --- /dev/null +++ b/config/services/containers/infra/maddy/data/maddy.conf.j2 @@ -0,0 +1,114 @@ +# Base + +$(hostname) = {{ services['maddy']['domain']['internal'] }}.{{ domain['internal'] }} +$(primary_domain) = {{ domain['internal'] }} +$(local_domains) = $(primary_domain) +state_dir /data +runtime_dir /tmp/maddy +hostname $(hostname) +tls file /etc/ssl/maddy/maddy.crt /etc/ssl/maddy/maddy.key { + protocols tls1.2 tls1.3 +} + +# LDAP +auth.ldap smtp_ldap { + urls ldaps://{{ services['ldap']['domain']['internal'] }}.{{ domain['internal'] }} + bind plain "uid=maddy,ou=people,{{ domain['dc'] }}" "{env:MADDY_LDAP_BIND_PASSWORD}" + base_dn "ou=people,{{ domain['dc'] }}" + filter "(&(uid={username})(objectClass=person)(|(memberOf=cn=admins,ou=groups,{{ domain['dc'] }})(memberOf=cn=users,ou=groups,{{ domain['dc'] }})(memberOf=cn=services,ou=groups,{{ domain['dc'] }})))" + starttls off + tls_client { + root_ca /etc/ssl/maddy/{{ root_cert_filename }} + } +} +auth.ldap imap_ldap { + urls ldaps://{{ services['ldap']['domain']['internal'] }}.{{ domain['internal'] }} + bind plain "uid=maddy,ou=people,{{ domain['dc'] }}" "{env:MADDY_LDAP_BIND_PASSWORD}" + base_dn "ou=people,{{ domain['dc'] }}" + filter "(&(uid={username})(objectClass=person)(|(memberOf=cn=admins,ou=groups,{{ domain['dc'] }})(memberOf=cn=users,ou=groups,{{ domain['dc'] }})))" + starttls off + tls_client { + root_ca /etc/ssl/maddy/{{ root_cert_filename }} + } +} + +storage.imapsql local_mailboxes { + driver postgres + dsn "{env:MADDY_POSTGRES_URL}" + msg_store fs messages/ + delivery_map email_localpart + delivery_normalize precis_casefold_email +} + +table.chain local_rewrites { + optional_step regexp "(.+)\+(.+)@(.+)" "$1@$3" + + optional_step static { + entry postmaster postmaster@$(primary_domain) + } +} + +msgpipeline local_routing { + destination postmaster $(local_domains) { + modify { + replace_rcpt &local_rewrites + } + + deliver_to &local_mailboxes + } + + default_destination { + reject 550 5.1.1 "User doesn't exist" + } +} + +submission tcp://0.0.0.0:587 { + hostname $(hostname) + + limits { + all rate 20 1s + all concurrency 20 + } + + auth &smtp_ldap + insecure_auth no + sasl_login yes + max_message_size 32M + + source $(local_domains) { + check { + authorize_sender { + prepare_email &local_rewrites + + auth_normalize precis_casefold + from_normalize precis_casefold_email + user_to_email regexp "(.*)" "$1@$(primary_domain)" + + check_header yes + } + } + + destination postmaster $(local_domains) { + deliver_to &local_routing + } + + default_destination { + reject 550 5.7.1 "External delivery is disabled" + } + } + + default_source { + reject 550 5.7.1 "Non-local sender domain" + } +} + +imap tls://0.0.0.0:993 { + auth &imap_ldap + storage &local_mailboxes + + insecure_auth no + sasl_login yes + + storage_map identity + storage_map_normalize precis_casefold +} diff --git a/config/services/containers/infra/maddy/maddy.container.j2 b/config/services/containers/infra/maddy/maddy.container.j2 new file mode 100644 index 0000000..e559be7 --- /dev/null +++ b/config/services/containers/infra/maddy/maddy.container.j2 @@ -0,0 +1,36 @@ +[Quadlet] +DefaultDependencies=false + +[Unit] +Description=Maddy + +After=network-online.target +Wants=network-online.target + +[Container] +Image=docker.io/foxcpp/maddy:{{ version['containers']['maddy'] }} + +ContainerName=maddy +HostName=maddy + +PublishPort={{ services['maddy']['ports']['smtp'] }}:587/tcp +PublishPort={{ services['maddy']['ports']['imaps'] }}:993/tcp + +AddHost={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}:host-gateway +AddHost={{ services['ldap']['domain']['internal'] }}.{{ domain['internal'] }}:host-gateway + +Volume=%h/containers/maddy/maddy.conf:/data/maddy.conf:ro +Volume=%h/containers/maddy/ssl:/etc/ssl/maddy:ro +Volume=%h/data/containers/maddy/data:/data:rw + +Environment="TZ={{ timezone }}" +Secret=MADDY_LDAP_BIND_PASSWORD,type=env +Secret=MADDY_POSTGRES_URL,type=env + +[Service] +Restart=always +RestartSec=10s +TimeoutStopSec=120 + +[Install] +WantedBy=default.target diff --git a/config/services/systemd/fw/bind/lib/db.1.00df.ip6.arpa b/config/services/systemd/fw/bind/lib/db.1.00df.ip6.arpa index a4fd193..8d3fc55 100644 --- a/config/services/systemd/fw/bind/lib/db.1.00df.ip6.arpa +++ b/config/services/systemd/fw/bind/lib/db.1.00df.ip6.arpa @@ -1,6 +1,6 @@ $TTL 86400 -@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. ( +@ IN SOA bind.ilnmors.internal. bind.ilnmors.internal. ( 2026021201 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) diff --git a/config/services/systemd/fw/bind/lib/db.1.168.192.in-addr.arpa b/config/services/systemd/fw/bind/lib/db.1.168.192.in-addr.arpa index a6d7501..01cf961 100644 --- a/config/services/systemd/fw/bind/lib/db.1.168.192.in-addr.arpa +++ b/config/services/systemd/fw/bind/lib/db.1.168.192.in-addr.arpa @@ -1,6 +1,6 @@ $TTL 86400 -@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. ( +@ IN SOA bind.ilnmors.internal. bind.ilnmors.internal. ( 2026021201 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) diff --git a/config/services/systemd/fw/bind/lib/db.10.00df.ip6.arpa b/config/services/systemd/fw/bind/lib/db.10.00df.ip6.arpa index 0663dac..f5fdc9f 100644 --- a/config/services/systemd/fw/bind/lib/db.10.00df.ip6.arpa +++ b/config/services/systemd/fw/bind/lib/db.10.00df.ip6.arpa @@ -1,6 +1,6 @@ $TTL 86400 -@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. ( +@ IN SOA bind.ilnmors.internal. bind.ilnmors.internal. ( 2026021201 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) diff --git a/config/services/systemd/fw/bind/lib/db.10.168.192.in-addr.arpa b/config/services/systemd/fw/bind/lib/db.10.168.192.in-addr.arpa index c024ea7..fd27991 100644 --- a/config/services/systemd/fw/bind/lib/db.10.168.192.in-addr.arpa +++ b/config/services/systemd/fw/bind/lib/db.10.168.192.in-addr.arpa @@ -1,6 +1,6 @@ $TTL 86400 -@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. ( +@ IN SOA bind.ilnmors.internal. bind.ilnmors.internal. ( 2026021201 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) diff --git a/config/services/systemd/fw/bind/lib/db.ilnmors.com b/config/services/systemd/fw/bind/lib/db.ilnmors.com index 308a9c1..4fac5f3 100644 --- a/config/services/systemd/fw/bind/lib/db.ilnmors.com +++ b/config/services/systemd/fw/bind/lib/db.ilnmors.com @@ -1,6 +1,6 @@ $TTL 86400 -@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. ( +@ IN SOA bind.ilnmors.internal. bind.ilnmors.internal. ( 2026021201 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) diff --git a/config/services/systemd/fw/bind/lib/db.ilnmors.internal b/config/services/systemd/fw/bind/lib/db.ilnmors.internal index 72c66e3..f0658a5 100644 --- a/config/services/systemd/fw/bind/lib/db.ilnmors.internal +++ b/config/services/systemd/fw/bind/lib/db.ilnmors.internal @@ -1,6 +1,6 @@ $TTL 86400 -@ IN SOA bind.ilnmors.internal. mail.ilnmors.internal. ( +@ IN SOA bind.ilnmors.internal. bind.ilnmors.internal. ( 2026021201 ; serial 3600 ; refresh (1 hour) 1800 ; retry (30 minutes) @@ -35,6 +35,7 @@ kopia IN CNAME bk.ilnmors.internal. ca IN CNAME infra.ilnmors.internal. postgresql IN CNAME infra.ilnmors.internal. ldap IN CNAME infra.ilnmors.internal. +maddy IN CNAME infra.ilnmors.internal. prometheus IN CNAME infra.ilnmors.internal. loki IN CNAME infra.ilnmors.internal. grafana IN CNAME infra.ilnmors.internal. diff --git a/data/sql/postgresql/.gitkeep b/data/sql/postgresql/.gitkeep deleted file mode 100644 index e69de29..0000000