fix: restore tree dropped by 6d224861; land stem95su gdrive-sync (10m) [ci skip]
6d224861 came from a --no-checkout worktree whose empty index made the
commit drop every file except two. This restores 05b50d2b's full tree and
correctly adds stacks/stem95su/gdrive-sync.tf + the service-catalog stem95su
entry. Forward-only (parent=6d224861, no force-push); [ci skip] since the
live infra was never applied from the broken commit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
6d224861c4
commit
fd0f4a0365
1166 changed files with 358546 additions and 0 deletions
32
stacks/mailserver/main.tf
Normal file
32
stacks/mailserver/main.tf
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# =============================================================================
|
||||
# Mailserver Stack — docker-mailserver
|
||||
# =============================================================================
|
||||
|
||||
variable "tls_secret_name" { type = string }
|
||||
variable "nfs_server" { type = string }
|
||||
variable "mysql_host" { type = string }
|
||||
|
||||
data "vault_kv_secret_v2" "secrets" {
|
||||
mount = "secret"
|
||||
name = "platform"
|
||||
}
|
||||
|
||||
locals {
|
||||
mailserver_accounts = jsondecode(data.vault_kv_secret_v2.secrets.data["mailserver_accounts"])
|
||||
mailserver_aliases = jsondecode(data.vault_kv_secret_v2.secrets.data["mailserver_aliases"])
|
||||
mailserver_opendkim_key = jsondecode(data.vault_kv_secret_v2.secrets.data["mailserver_opendkim_key"])
|
||||
mailserver_sasl_passwd = jsondecode(data.vault_kv_secret_v2.secrets.data["mailserver_sasl_passwd"])
|
||||
}
|
||||
|
||||
module "mailserver" {
|
||||
source = "./modules/mailserver"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
nfs_server = var.nfs_server
|
||||
mysql_host = var.mysql_host
|
||||
mailserver_accounts = local.mailserver_accounts
|
||||
postfix_account_aliases = local.mailserver_aliases
|
||||
opendkim_key = local.mailserver_opendkim_key
|
||||
sasl_passwd = local.mailserver_sasl_passwd
|
||||
roundcube_db_password = data.vault_kv_secret_v2.secrets.data["mailserver_roundcubemail_db_password"]
|
||||
tier = local.tiers.edge
|
||||
}
|
||||
12
stacks/mailserver/modules/mailserver/extra/aliases.txt
Normal file
12
stacks/mailserver/modules/mailserver/extra/aliases.txt
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
firmly-gerardo-generated@viktorbarzin.me me@viktorbarzin.me
|
||||
closely-keith-generated@viktorbarzin.me vbarzin@gmail.com
|
||||
literally-paolo-generated@viktorbarzin.me viktorbarzin@fb.com
|
||||
hastily-stefanie-generated@viktorbarzin.me elliestamenova@gmail.com
|
||||
vaultwarden@viktorbarzin.me me@viktorbarzin.me
|
||||
|
||||
# plans@ -> spam@: authorizes tripit (SMTP-authed as spam@) to send mail
|
||||
# From: plans@viktorbarzin.me under docker-mailserver SPOOF_PROTECTION (the
|
||||
# smtpd_sender_login_maps union exact-matches this alias to spam@; the @domain
|
||||
# catch-all does NOT, so an explicit entry is required). Also keeps inbound
|
||||
# plans@ routing to spam@ for the tripit mail-ingest poller.
|
||||
plans@viktorbarzin.me spam@viktorbarzin.me
|
||||
1598
stacks/mailserver/modules/mailserver/main.tf
Normal file
1598
stacks/mailserver/modules/mailserver/main.tf
Normal file
File diff suppressed because it is too large
Load diff
303
stacks/mailserver/modules/mailserver/roundcubemail.tf
Normal file
303
stacks/mailserver/modules/mailserver/roundcubemail.tf
Normal file
|
|
@ -0,0 +1,303 @@
|
|||
variable "roundcube_db_password" {
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
variable "mysql_host" { type = string }
|
||||
|
||||
resource "kubernetes_config_map" "roundcubemail_config" {
|
||||
metadata {
|
||||
name = "roundcubemail.config"
|
||||
namespace = "mailserver"
|
||||
|
||||
labels = {
|
||||
app = "roundcubemail"
|
||||
}
|
||||
annotations = {
|
||||
"reloader.stakater.com/match" = "true"
|
||||
}
|
||||
}
|
||||
|
||||
data = {
|
||||
# Disable TLS peer verification for internal service name connections
|
||||
# The mailserver cert is issued for mail.viktorbarzin.me, not the k8s service name
|
||||
"custom.php" = <<-EOF
|
||||
<?php
|
||||
$config['imap_conn_options'] = [
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
];
|
||||
$config['smtp_conn_options'] = [
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
],
|
||||
];
|
||||
?>
|
||||
EOF
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "roundcube_html_encrypted" {
|
||||
wait_until_bound = false
|
||||
metadata {
|
||||
name = "roundcubemail-html-encrypted"
|
||||
namespace = kubernetes_namespace.mailserver.metadata[0].name
|
||||
annotations = {
|
||||
"resize.topolvm.io/threshold" = "10%"
|
||||
"resize.topolvm.io/increase" = "100%"
|
||||
"resize.topolvm.io/storage_limit" = "5Gi"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
storage_class_name = "proxmox-lvm-encrypted"
|
||||
resources {
|
||||
requests = {
|
||||
storage = "1Gi"
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
# The autoresizer expands requests.storage up to storage_limit and
|
||||
# PVCs can't shrink. Without this, every TF apply tries to revert
|
||||
# to the spec value, K8s rejects the shrink, and the PVC ends up
|
||||
# in Terminating-but-in-use limbo.
|
||||
ignore_changes = [spec[0].resources[0].requests]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_persistent_volume_claim" "roundcube_enigma_encrypted" {
|
||||
wait_until_bound = false
|
||||
metadata {
|
||||
name = "roundcubemail-enigma-encrypted"
|
||||
namespace = kubernetes_namespace.mailserver.metadata[0].name
|
||||
annotations = {
|
||||
"resize.topolvm.io/threshold" = "10%"
|
||||
"resize.topolvm.io/increase" = "100%"
|
||||
"resize.topolvm.io/storage_limit" = "5Gi"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
access_modes = ["ReadWriteOnce"]
|
||||
storage_class_name = "proxmox-lvm-encrypted"
|
||||
resources {
|
||||
requests = {
|
||||
storage = "1Gi"
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
# The autoresizer expands requests.storage up to storage_limit and
|
||||
# PVCs can't shrink. Without this, every TF apply tries to revert
|
||||
# to the spec value, K8s rejects the shrink, and the PVC ends up
|
||||
# in Terminating-but-in-use limbo.
|
||||
ignore_changes = [spec[0].resources[0].requests]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_deployment" "roundcubemail" {
|
||||
metadata {
|
||||
name = "roundcubemail"
|
||||
namespace = "mailserver"
|
||||
labels = {
|
||||
"app" = "roundcubemail"
|
||||
tier = var.tier
|
||||
}
|
||||
annotations = {
|
||||
"reloader.stakater.com/search" = "true"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
replicas = "1"
|
||||
strategy {
|
||||
type = "Recreate"
|
||||
}
|
||||
selector {
|
||||
match_labels = {
|
||||
"app" = "roundcubemail"
|
||||
}
|
||||
}
|
||||
template {
|
||||
metadata {
|
||||
labels = {
|
||||
"app" = "roundcubemail"
|
||||
}
|
||||
}
|
||||
spec {
|
||||
container {
|
||||
name = "roundcube"
|
||||
image = "roundcube/roundcubemail:1.6.13-apache"
|
||||
volume_mount {
|
||||
name = "roundcube-config"
|
||||
mount_path = "/var/roundcube/config/custom.php"
|
||||
sub_path = "custom.php"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DEFAULT_HOST"
|
||||
value = "ssl://mailserver" # internal k8s service name
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DEFAULT_PORT"
|
||||
value = "993"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_SMTP_SERVER"
|
||||
value = "tls://mailserver" # internal k8s service name
|
||||
}
|
||||
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_SMTP_PORT"
|
||||
value = 587
|
||||
}
|
||||
|
||||
# DB Settings
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DB_TYPE"
|
||||
value = "mysql"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DB_HOST"
|
||||
value = var.mysql_host
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DB_USER"
|
||||
value = "roundcubemail"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DB_PASSWORD"
|
||||
value = var.roundcube_db_password
|
||||
}
|
||||
# Plugins
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_COMPOSER_PLUGINS"
|
||||
value = "mmvi/twofactor_webauthn,texxasrulez/persistent_login,dsoares/rcguard"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_PLUGINS"
|
||||
value = "attachment_reminder,database_attachments,enigma,twofactor_webauthn,persistent_login,rcguard"
|
||||
}
|
||||
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_SMTP_DEBUG"
|
||||
value = "false"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_DEBUG_LEVEL"
|
||||
value = "1"
|
||||
}
|
||||
env {
|
||||
name = "ROUNDCUBEMAIL_LOG_DRIVER"
|
||||
# value = "file"
|
||||
value = "syslog"
|
||||
}
|
||||
port {
|
||||
name = "web"
|
||||
container_port = 80
|
||||
protocol = "TCP"
|
||||
}
|
||||
volume_mount {
|
||||
name = "html"
|
||||
mount_path = "/var/www/html"
|
||||
}
|
||||
volume_mount {
|
||||
name = "enigma"
|
||||
mount_path = "/var/roundcube/enigma"
|
||||
}
|
||||
resources {
|
||||
requests = {
|
||||
cpu = "25m"
|
||||
memory = "192Mi"
|
||||
}
|
||||
limits = {
|
||||
memory = "192Mi"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "roundcube-config"
|
||||
config_map {
|
||||
name = kubernetes_config_map.roundcubemail_config.metadata[0].name
|
||||
}
|
||||
}
|
||||
|
||||
volume {
|
||||
name = "html"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.roundcube_html_encrypted.metadata[0].name
|
||||
}
|
||||
}
|
||||
volume {
|
||||
name = "enigma"
|
||||
persistent_volume_claim {
|
||||
claim_name = kubernetes_persistent_volume_claim.roundcube_enigma_encrypted.metadata[0].name
|
||||
}
|
||||
}
|
||||
dns_config {
|
||||
option {
|
||||
name = "ndots"
|
||||
value = "2"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
lifecycle {
|
||||
# KYVERNO_LIFECYCLE_V1: Kyverno admission webhook mutates dns_config with ndots=2
|
||||
ignore_changes = [
|
||||
spec[0].template[0].spec[0].dns_config, # KYVERNO_LIFECYCLE_V1
|
||||
metadata[0].annotations["keel.sh/policy"],
|
||||
metadata[0].annotations["keel.sh/trigger"],
|
||||
metadata[0].annotations["keel.sh/pollSchedule"], # KYVERNO_LIFECYCLE_V2
|
||||
metadata[0].annotations["keel.sh/match-tag"],
|
||||
spec[0].template[0].spec[0].container[0].image, # KEEL_IGNORE_IMAGE — Keel manages tag updates
|
||||
metadata[0].annotations["kubernetes.io/change-cause"],
|
||||
metadata[0].annotations["deployment.kubernetes.io/revision"],
|
||||
spec[0].template[0].metadata[0].annotations["keel.sh/update-time"], # KEEL_LIFECYCLE_V1
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
resource "kubernetes_service" "roundcubemail" {
|
||||
metadata {
|
||||
name = "roundcubemail"
|
||||
namespace = "mailserver"
|
||||
|
||||
labels = {
|
||||
app = "roundcubemail"
|
||||
}
|
||||
}
|
||||
|
||||
spec {
|
||||
selector = {
|
||||
app = "roundcubemail"
|
||||
}
|
||||
|
||||
port {
|
||||
name = "roundcube"
|
||||
protocol = "TCP"
|
||||
port = 80
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module "ingress" {
|
||||
source = "../../../../modules/kubernetes/ingress_factory"
|
||||
dns_type = "non-proxied"
|
||||
namespace = "mailserver"
|
||||
name = "mail"
|
||||
service_name = "roundcubemail"
|
||||
tls_secret_name = var.tls_secret_name
|
||||
auth = "required"
|
||||
extra_annotations = {
|
||||
"gethomepage.dev/enabled" = "true"
|
||||
"gethomepage.dev/name" = "Roundcube Mail"
|
||||
"gethomepage.dev/description" = "Webmail client"
|
||||
"gethomepage.dev/icon" = "roundcube.png"
|
||||
"gethomepage.dev/group" = "Other"
|
||||
"gethomepage.dev/pod-selector" = ""
|
||||
}
|
||||
}
|
||||
48
stacks/mailserver/modules/mailserver/variables.tf
Normal file
48
stacks/mailserver/modules/mailserver/variables.tf
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
# this is appended and merged to the main postfix.cf
|
||||
# see defaults - https://github.com/docker-mailserver/docker-mailserver/blob/master/target/postfix/main.cf
|
||||
variable "postfix_cf" {
|
||||
default = <<EOT
|
||||
relayhost = [smtp-relay.brevo.com]:587
|
||||
smtp_sasl_auth_enable = yes
|
||||
smtp_sasl_password_maps = hash:/etc/postfix/sasl/passwd
|
||||
smtp_sasl_security_options = noanonymous
|
||||
smtp_sasl_tls_security_options = noanonymous
|
||||
smtp_tls_security_level = encrypt
|
||||
# TLS cert/key come from docker-mailserver's SSL_TYPE=manual flow, which writes
|
||||
# the authoritative `smtpd_tls_chain_files` into main.cf at boot. Setting the
|
||||
# legacy smtpd_tls_cert_file/smtpd_tls_key_file here too makes postfix warn
|
||||
# ("Both smtpd_tls_chain_files and one or more of the legacy ...") and ignore
|
||||
# them. Dropped to silence the warning — functionally a no-op (chain_files wins).
|
||||
smtpd_use_tls=yes
|
||||
# Require STARTTLS before any AUTH command on the SMTPD listener.
|
||||
# Without this, a misconfigured client that skips STARTTLS would send
|
||||
# PLAIN/LOGIN creds in the clear. docker-mailserver's default does NOT
|
||||
# enforce this at the main.cf level for submission (587).
|
||||
# Note: smtpd_sasl_auth_only (sometimes cited) is NOT a real Postfix
|
||||
# parameter — only smtpd_tls_auth_only is. Addresses code-vnw.
|
||||
smtpd_tls_auth_only = yes
|
||||
header_size_limit = 4096000
|
||||
|
||||
# Debug mail tls
|
||||
smtpd_tls_loglevel = 1
|
||||
#smtpd_tls_ciphers = TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:!aNULL:!SEED:!CAMELLIA:!RSA+AES:!SHA1
|
||||
#tls_medium_cipherlist = ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA256:!aNULL:!SEED:!CAMELLIA:!RSA+AES:!SHA1
|
||||
|
||||
# Rate limiting (brute-force protection)
|
||||
smtpd_client_connection_rate_limit = 10
|
||||
smtpd_client_message_rate_limit = 30
|
||||
anvil_rate_time_unit = 60s
|
||||
|
||||
# Disable the postscreen decision cache. The default (btree) driver
|
||||
# requires an exclusive file lock for every access, and with postscreen
|
||||
# re-spawning per connection (master.cf: maxproc=1) that produces thousands
|
||||
# of 'unable to get exclusive lock' fatals per day — stalling SMTP
|
||||
# acceptance and starving inbound delivery. lmdb would avoid the lock but
|
||||
# isn't compiled into docker-mailserver 15.0.0's Postfix build
|
||||
# (postconf -m → no lmdb). Proxy:btree is unsafe because postscreen does
|
||||
# its own locking. An empty value disables the cache entirely — legitimate
|
||||
# clients pay the greet/bare-newline re-check on every new TCP session,
|
||||
# which is trivial at our volume (~100 deliveries/day).
|
||||
postscreen_cache_map =
|
||||
EOT
|
||||
}
|
||||
1
stacks/mailserver/secrets
Symbolic link
1
stacks/mailserver/secrets
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../secrets
|
||||
8
stacks/mailserver/terragrunt.hcl
Normal file
8
stacks/mailserver/terragrunt.hcl
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
include "root" {
|
||||
path = find_in_parent_folders()
|
||||
}
|
||||
|
||||
dependency "infra" {
|
||||
config_path = "../infra"
|
||||
skip_outputs = true
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue