feat(maddy): add mail stack

This commit is contained in:
il
2026-06-25 15:17:35 +09:00
parent fb2578d28a
commit a2334bb236
21 changed files with 350 additions and 42 deletions
+30 -20
View File
@@ -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
+8
View File
@@ -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"
+11
View File
@@ -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
@@ -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'] }}"
@@ -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
@@ -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_<service>.sql files in
{{ node['home_path'] }}/data/containers/postgresql/backups/sql/
@@ -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
+1 -2
View File
@@ -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 {
+16 -4
View File
@@ -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;
+7 -5
View File
@@ -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
@@ -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'] }}"]
@@ -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]
@@ -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
}
@@ -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
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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)
@@ -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.
View File