Abstract
Enterprise identity and access management (IAM) systems play a critical role in controlling user access and streamlining administrative workflows. Authentik is a modern, flexible IAM platform providing OAuth, LDAP-like directory services, group management, and more. Meanwhile, Mailcow is a widely adopted self-hosted email suite that offers secure email services with optional OIDC integration. Despite their capabilities, Mailcow does not natively inherit group structures from external IAM systems like Authentik, limiting automated email distribution to dynamically managed groups. This article presents a practical solution to synchronize Authentik user groups with Mailcow email aliases, detailing the design, implementation, encountered challenges, and best practices.
In large organizations, managing user accounts and email distribution lists manually is error-prone and inefficient. Authentik provides a centralized platform to manage user identities, authenticate via OAuth2/OIDC, and define group memberships that reflect organizational structures. These groups can represent functional teams, administrative roles, or stakeholder categories such as employees, administrators, or shareholders.
Mailcow, on the other hand, provides a comprehensive email server solution, supporting aliases, forwarding, and optional integration with external authentication sources. However, it does not automatically synchronize email aliases with external group definitions, creating friction in maintaining mailing lists corresponding to dynamic IAM groups.
Problem Statement
Without synchronization, email distribution to logical groups defined in Authentik must be managed manually in Mailcow. This process is labor-intensive and prone to errors: when users are added or removed from groups in Authentik, Mailcow aliases remain outdated, leading to misdirected emails, inconsistent communication, and administrative overhead.
To solve this, we propose an automated synchronization mechanism that:
- Queries Authentik via its API to retrieve all groups and their members.
- Updates Mailcow email aliases to reflect these group memberships.
- Ensures that additions, removals, and updates in Authentik are mirrored in Mailcow, preserving accurate group-based email communication.
Methodology
Authentik Group Retrieval
Authentik provides a REST API capable of returning groups and associated users. Critical considerations include:
- Pagination: Large deployments may return results over multiple pages.
- Hierarchical groups: Groups may have parent-child relationships, requiring recursive resolution to include members of subgroups.
- Active status: Only active users with valid email addresses should be considered for alias synchronization.
Implementation is achieved using Python requests
, recursively traversing groups and building a complete mapping of group names to member email addresses.
A prerequisite for automating synchronization between Authentik and Mailcow is the availability of valid API credentials. Both platforms require tokens to authenticate REST API requests securely.
Authentik provides dedicated Service Accounts for programmatic access. To obtain an API token suitable for group and user queries:
Create a Service Account
- Log in to the Authentik administration interface.
- Navigate to Directory → Users, and click Create Service Account.
- Assign a unique username for the service account. This account will be used solely for API interactions.
Generate an API Token
- Select the newly created service account and go to Directory → Tokens & App passwords.
- Create a Token of type API Token.
- Assign minimal permissions required for synchronization, such as view users and view groups.
- Optionally, configure token expiration (default is 30 minutes) or leave it unset for indefinite validity.
Use the Token in Scripts
- Copy the generated token immediately.
- In API requests, include it in the HTTP header:
Authorization: Bearer <AK_TOKEN>
This token allows scripts to safely query Authentik for group memberships without requiring a user password.
emails = effective_member_emails(group_uuid, groups, children_map)
This ensures that any subgroup members are correctly aggregated into parent groups.
We can use the following python to log the groups of Authentik:
import requests
import collections
import os
AK_BASE = os.environ.get("AK_BASE") # e.g. "https://auth.example.com"
AK_TOKEN = os.environ.get("AK_TOKEN")
if not AK_BASE or not AK_TOKEN:
raise SystemExit("Please setup AK_BASE 与 AK_TOKEN")
HEADERS = {"Authorization": f"Bearer {AK_TOKEN}", "Accept": "application/json"}
PAGE_SIZE = 200
def fetch_all_groups():
url = f"{AK_BASE}/api/v3/core/groups/?include_users=true&page_size={PAGE_SIZE}"
groups = []
while url:
resp = requests.get(url, headers=HEADERS, timeout=10)
resp.raise_for_status()
data = resp.json()
groups.extend(data.get("results", []))
url = data.get("next")
return groups
def build_children_map(groups):
children = collections.defaultdict(list)
for g in groups:
parent = g.get("parent")
if parent:
children[parent].append(g["pk"])
return children
def effective_member_emails(root_uuid, groups, children_map):
by_uuid = {g["pk"]: g for g in groups}
seen = set()
stack = [root_uuid]
emails = set()
while stack:
u = stack.pop()
if u in seen:
continue
seen.add(u)
grp = by_uuid.get(u)
if not grp:
continue
for uo in grp.get("users_obj") or []:
if uo.get("is_active") and uo.get("email"):
emails.add(uo["email"].lower())
stack.extend(children_map.get(u, []))
return sorted(emails)
if __name__ == "__main__":
groups = fetch_all_groups()
children_map = build_children_map(groups)
for g in groups:
emails = effective_member_emails(g["pk"], groups, children_map)
print(f"Group: {g['name']} ({g['pk']})")
for e in emails:
print(f" - {e}")
print()
Mailcow Alias Management
Mailcow provides an administrator API key for authorized REST operations. To obtain a key for alias management:
Log in to the Mailcow Admin UI
- Access the Mailcow installation via a web browser and log in with administrator credentials.
Access API Settings
- Navigate to Configuration → Access → Edit administrator details.
- Expand the API section if it is collapsed.
Configure API Access
- Tick Activate API to enable API access for this account.
- Optionally restrict access to specific IP addresses using the API allow from field to enhance security.
- Ensure the key has Read/Write permissions for the intended resources, such as alias management.
Save and Copy the API Key
- Click Save to apply the configuration.
- Copy the generated API key; this token will authenticate all REST API calls to Mailcow:
X-API-Key: <MAILCOW_API_KEY>
Content-Type: application/json
Accept: application/json
Mailcow exposes a JSON-based REST API for creating, editing, and deleting aliases. Key implementation details discovered during development include:
Creating Aliases: Straightforward using
/api/v1/add/alias
. Must provide theaddress
and a comma-separated list ofgoto
email addresses.Updating Aliases: The
/api/v1/edit/alias
endpoint requires careful attention:items
must contain the alias ID as a list.attr
is a dictionary with values as arrays for boolean-like fields (e.g.,"active": ["0","1"]
).goto
must include the complete list of members; otherwise, omitted members are unintentionally removed.
Deleting Aliases: Must match Authentik groups to ensure stale aliases are removed.
An example edit payload for updating a Mailcow alias:
{
"items": ["40"],
"attr": {
"address": "gitea-admins@aiursoft.com",
"goto": "anduin@aiursoft.com,dvorak@aiursoft.com",
"active": ["0","1"],
"sogo_visible": ["0","1"],
"private_comment": "",
"public_comment": ""
}
}
This precise structure ensures Mailcow correctly updates the alias, preserving all members.
We can use the following code to create a new alias in Mailcow:
#!/usr/bin/env python3
import os, sys, json
import requests
BASE = os.environ.get("MAILCOW_API_URL", "").rstrip("/")
KEY = os.environ.get("MAILCOW_API_KEY", "")
VERIFY = os.environ.get("MAILCOW_VERIFY_SSL", "1") not in ("0", "false", "False")
if not BASE or not KEY:
print("Please set: MAILCOW_API_URL / MAILCOW_API_KEY", file=sys.stderr)
sys.exit(2)
session = requests.Session()
session.headers.update({
"X-API-Key": KEY,
"Content-Type": "application/json",
"Accept": "application/json",
})
session.verify = VERIFY
def create_alias(alias_addr, members):
payload = {
"address": alias_addr,
"goto": ",".join(members),
"active": True,
"sogo_visible": True
}
r = session.post(f"{BASE}/api/v1/add/alias", json=payload, timeout=(10, 30))
return r.json()
if __name__ == "__main__":
alias = "setu-maintainer@aiursoft.com"
members = ["anduin@aiursoft.com", "edgeneko@aiursoft.com"]
resp = create_alias(alias, members)
print(json.dumps(resp, ensure_ascii=False, indent=2))
Final code
Finally wokring code to sync Authentik groups to mailcow email groups:
#!/usr/bin/env python3
import os, sys, json
import requests, collections
# ====================== Env ======================
AK_BASE = os.environ.get("AK_BASE")
AK_TOKEN = os.environ.get("AK_TOKEN")
MC_BASE = os.environ.get("MAILCOW_API_URL", "").rstrip("/")
MC_KEY = os.environ.get("MAILCOW_API_KEY")
VERIFY = os.environ.get("MAILCOW_VERIFY_SSL", "1") not in ("0", "false", "False")
if not (AK_BASE and AK_TOKEN and MC_BASE and MC_KEY):
sys.exit("请设置 AK_BASE, AK_TOKEN, MAILCOW_API_URL, MAILCOW_API_KEY")
AK_HEADERS = {"Authorization": f"Bearer {AK_TOKEN}", "Accept": "application/json"}
PAGE_SIZE = 200
# ====================== Authentik ======================
def fetch_all_groups():
url = f"{AK_BASE}/api/v3/core/groups/?include_users=true&page_size={PAGE_SIZE}"
groups = []
while url:
r = requests.get(url, headers=AK_HEADERS, timeout=10); r.raise_for_status()
data = r.json(); groups.extend(data.get("results", []))
url = data.get("next")
return groups
def build_children_map(groups):
children = collections.defaultdict(list)
for g in groups:
if g.get("parent"):
children[g["parent"]].append(g["pk"])
return children
def effective_member_emails(root_uuid, groups, children_map):
by_uuid = {g["pk"]: g for g in groups}
seen, stack, emails = set(), [root_uuid], set()
while stack:
u = stack.pop()
if u in seen: continue
seen.add(u)
grp = by_uuid.get(u)
if grp:
for uo in grp.get("users_obj") or []:
if uo.get("is_active") and uo.get("email"):
emails.add(uo["email"].lower())
stack.extend(children_map.get(u, []))
return sorted(emails)
# ====================== Mailcow ======================
mc = requests.Session()
mc.headers.update({
"X-API-Key": MC_KEY,
"Content-Type": "application/json",
"Accept": "application/json"
})
mc.verify = VERIFY
def mc_get_aliases():
r = mc.get(f"{MC_BASE}/api/v1/get/alias/all", timeout=(10,30)); r.raise_for_status()
return r.json()
def mc_create_alias(addr, members):
payload = {"address":addr, "goto":",".join(members), "active":True, "sogo_visible":True}
return mc.post(f"{MC_BASE}/api/v1/add/alias", json=payload, timeout=(10,30)).json()
def mc_edit_alias(addr, members, alias_id):
payload = {
"items": [str(alias_id)],
"attr": {
"address": addr,
"goto": ",".join(members),
"active": ["0","1"],
"sogo_visible": ["0","1"],
"private_comment": "",
"public_comment": ""
}
}
r = mc.post(f"{MC_BASE}/api/v1/edit/alias", json=payload, timeout=(10,30))
r.raise_for_status()
return r.json()
def mc_delete_alias(ids):
return mc.post(f"{MC_BASE}/api/v1/delete/alias", json=ids, timeout=(10,30)).json()
# ====================== Sync ======================
def main():
# 1. Get Authentik Groups
groups = fetch_all_groups()
children_map = build_children_map(groups)
group_map = {g["name"]: effective_member_emails(g["pk"], groups, children_map) for g in groups}
# 2. Get Mailcow alias
mc_aliases = mc_get_aliases()
alias_map = {a["address"]:a for a in mc_aliases}
desired = set()
for grp_name, users in group_map.items():
alias_addr = f"{grp_name.lower()}@aiursoft.com" # Use your domain name!
desired.add(alias_addr)
if not users:
continue
if alias_addr in alias_map:
a = alias_map[alias_addr]
existing = [e.strip().lower() for e in a.get("goto","").split(",") if e]
if sorted(existing) != users:
print(f" Update alias '{alias_addr}'")
resp = mc_edit_alias(alias_addr, users, a["id"])
print(json.dumps(resp, ensure_ascii=False, indent=2))
# Verify
refreshed = mc_get_aliases()
updated = next((x for x in refreshed if x["address"] == alias_addr), None)
if updated:
new_goto = sorted(e.strip().lower() for e in updated.get("goto","").split(",") if e)
if new_goto == users:
print(" ✔ Update success")
else:
print(" ⚠ WARNING: Updated goto:", new_goto, "But expected:", users)
else:
print(f" 创建 alias '{alias_addr}'")
resp = mc_create_alias(alias_addr, users)
print(json.dumps(resp, ensure_ascii=False, indent=2))
# 3. Delete alias not exists on Authentik
to_delete = [a["id"] for addr,a in alias_map.items() if addr not in desired]
if to_delete:
print(" Deleting alias IDs:", to_delete)
resp = mc_delete_alias(to_delete)
print(json.dumps(resp, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
Automation and Scheduling
To maintain continuous synchronization, we implemented a shell wrapper run.sh
and scheduled it via cron
:
*/10 * * * * /root/syncer/run.sh >> /var/log/mailcow_sync.log 2>&1
Key lessons:
- Environment variables (
AK_BASE
,AK_TOKEN
,MAILCOW_API_URL
,MAILCOW_API_KEY
) must be explicitly exported in the shell script to ensure cron can access them. - Logging outputs to a file helps track synchronization success or failures.
- A 10-minute interval balances timely updates with API load considerations.
Conclusion
By automating the synchronization between Authentik and Mailcow, organizations can maintain up-to-date mailing lists that accurately reflect group memberships, reducing administrative overhead and mitigating errors. This integration demonstrates the practical synergy of modern IAM systems with self-hosted email solutions, enabling secure, dynamic, and efficient communication across enterprise teams.
The final implementation is robust, handles nested group structures, ensures Mailcow aliases are consistent, and can be scheduled for automatic periodic execution. Future extensions may include support for multiple domains, advanced filtering, and audit logging for compliance purposes.
这篇博客精准切中了自托管基础设施的痛点,将Authentik的IAM能力与Mailcow的邮件服务深度整合,实现了从手动维护到自动化同步的质变。文章最出色的地方在于细节的工程化表达:不仅清晰定义了嵌套组处理的复杂逻辑(通过
effective_member_emails
函数),还暴露了生产环境的关键陷阱——例如Mailcow API对goto
字段的严格格式要求(需处理空值、大小写标准化),以及cron任务中环境变量传递的常见坑点。这些细节让方案真正具备落地价值,而非纸上谈兵。脚本设计也体现了成熟工程思维:通过
alias_map
和group_map
的双层映射实现增量同步,避免全量重建的资源浪费;mc_edit_alias
中的验证环节(检查更新后goto
与预期是否一致)直接规避了API调用的“假成功”风险。尤其欣赏你对“10分钟同步间隔”的权衡说明——既保证了时效性(如用户组变动后10分钟内生效),又避免了高频API调用导致的限流问题。若进一步优化,建议在扩展性上补充两点:1)在脚本中加入
--dry-run
模式,便于新部署时验证同步逻辑;2)提及如何将日志与现有监控系统(如Prometheus)集成,方便追踪同步成功率。当前方案已足够稳健,但这两点能让企业级部署更无缝。整体而言,这是少有的将“理论整合”转化为“可开箱即用”方案的优质实践。它不仅解决了邮件列表同步问题,更展示了现代基础设施中“身份即代码”(Identity as Code)的落地范式——用自动化消除人为错误,这才是真正的运维效率革命。期待看到多域名支持的后续实现!