Restricting App Passwords to specific services

Hi,

Is there a way to limit the scope of App Passwords? I would like to restrict their usage to specific services (IMAP, CardDAV) while blocking access to the Admin UI.

Thanks for help.

Yes, this is supported. App Passwords have a permissions field that lets you scope them to a specific subset of the account’s permissions — so you can grant access to only IMAP and CardDAV while excluding anything else (including the Admin UI).

Here’s how it works in practice:

  1. The user (or an admin) opens the Account Manager self-service portal at /account, navigates to Credentials → App Passwords, and creates a new App Password.
  2. When creating it, set the permissions field to restrict or replace the account’s inherited permissions — selecting only the specific services you want (e.g. IMAP, CardDAV), and omitting administrative permissions entirely.
  3. Additionally, each App Password supports:
    • expiresAt — automatic expiration date
    • allowedIps — IP address restrictions
    • description — a human-readable label for auditing

This means a user could have one App Password for their IMAP client (IMAP-only), another for their contacts app (CardDAV-only), and their main account password for everything else — with no App Password ever granting Admin UI access.

:warning: Note: App Password management through the WebUI is only available when using the internal directory. With LDAP or SQL backends, App Passwords must be added as account secrets directly in the external directory, and fine-grained permission scoping may not apply.

For full details, see the App Passwords documentation and the AppPassword object reference.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

The bot reply is correct but the documentation needs to be updated because in v0.16 you can manage App Passwords when using external directories.

@helpbot I tryed this conf but without Sign in to the server i can not login by imap and if i allow this one, i can login by imap and by web UI by app passwort.
My test steps:
1 - Created a user with the user role.
2 - Navigated to Credentials → App Passwords.
3 - Set Permissions Mode to Replace all permissions.
4 - Added the following permissions: all under Calendar, IMAP, and Webdav.
5 - Selected Email: Send emails and Email: Receive emails.

As a result, I can’t log in to the Web UI using the app password, but I also can’t log in via IMAP.
Without the Sign in to the server permission, IMAP login fails. But with it enabled, the app password can also access the Web UI.

V 0.15 had authenticate-oauth, which disabled login to the web UI via app passwords, but this permissionis not available in version 0.16.

2026-05-08T08:04:32Z DEBUG IMAP connection started (imap.connection-start) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242
2026-05-08T08:04:32Z TRACE Raw IMAP input received (imap.raw-input) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242, size = 17, contents = "A001 CAPABILITY\r\n"
2026-05-08T08:04:32Z DEBUG IMAP CAPABILITIES command (imap.capabilities) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242, tls = true, strict = true, elapsed = 0ms
2026-05-08T08:04:32Z TRACE Raw IMAP output sent (imap.raw-output) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242, size = 155, contents = "* CAPABILITY IMAP4rev2 IMAP4rev1 ENABLE SASL-IR LITERAL+ ID UTF8=ACCEPT JMAPACCESS AUTH=PLAIN AUTH=OAUTHBEARER AUTH=XOAUTH2\r\nA001 OK CAPABILITY completed\r\n"
2026-05-08T08:04:32Z TRACE Raw IMAP input received (imap.raw-input) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242, size = 102, contents = "A002 AUTHENTICATE PLAIN AHRlc3RAaXRzaWRlLmN6AGFwcF9hYWFhYWFvMTEyeGw3d216YmV0MXBla2N4amxiZHBienhvOWE=\r\n"
2026-05-08T08:04:32Z DEBUG Cache miss (store.cache-miss) key = 1, collection = "domainId"
2026-05-08T08:04:32Z DEBUG Cache miss (store.cache-miss) key = "test", domain = 1, collection = "email"
2026-05-08T08:04:32Z INFO Authentication successful (auth.success) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242, accountName = "test", accountId = 2, id = 1, details = "Authenticated with app password"
2026-05-08T08:04:32Z DEBUG Cache miss (store.cache-miss) key = 2, collection = "accessToken"
2026-05-08T08:04:32Z DEBUG Cache miss (store.cache-miss) key = 1, collection = "role"
2026-05-08T08:04:32Z TRACE Data store iteration operation (store.data-iterate) elapsed = 0ms
2026-05-08T08:04:33Z INFO Unauthorized access (security.unauthorized) details = "authenticate", accountId = 2, remoteIp = 192.168.122.181, id = "A002"
2026-05-08T08:04:33Z DEBUG IMAP connection ended (imap.connection-end) listenerId = "imaps", localPort = 993, remoteIp = 192.168.122.181, remotePort = 44242, elapsed = 310ms

The permissions reference gives a clear answer to this. The log line Unauthorized access ... details = "authenticate" is failing on the authenticate permission (the base “Sign in to the server” permission under the User category), which is indeed required for IMAP login. However, that same authenticate permission is also what allows login to the Web UI.

The key insight from the permissions reference is that IMAP login has its own dedicated permission: imapAuthenticate“Sign in to the IMAP server”. There is a separate authenticate permission in the User category that is the generic “Sign in to the server” gate, and it appears to be checked before the IMAP-specific permissions even come into play.

Looking at the full permission hierarchy for what you actually need:

  • imapAuthenticate — Sign in to the IMAP server (IMAP-specific)
  • davCardPropFind, davCardGet, davCardPut, etc. — CardDAV access

The Web UI login path requires authenticate (User category), but IMAP is supposed to use imapAuthenticate. The fact that authenticate is being checked for your IMAP session suggests this may be a bug or a design constraint in the current release where authenticate is a prerequisite gating all protocol logins, including IMAP.

This specific interaction — where authenticate is required even for IMAP despite imapAuthenticate existing — isn’t fully explained in the current docs. This looks like it may be a bug or an undocumented dependency between authenticate and imapAuthenticate. A community member or staff member (particularly @stalwart) would be best placed to clarify whether this is intentional and whether there’s a workaround to allow imapAuthenticate without authenticate.

For reference, the full permissions list is at: https://stalw.art/docs/ref/permissions

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.

@helpbot I am also starting to suspect this might be a bug or an oversight. It seems strange that if a user with admin privileges uses an app password, the web UI remains accessible without any way to disable it.
Could you please report this issue to @stalwart?

The analysis from the previous reply stands: the docs confirm imapAuthenticate exists as a dedicated IMAP sign-in permission, yet the server logs show authenticate (the generic “Sign in to the server” permission) is being enforced as a prerequisite — and that same permission also unlocks the Web UI, with no documented way to split the two.

This does appear to be either a bug or an undocumented design constraint, and it’s already been flagged in this thread with @stalwart tagged directly. Since @stalwart is a moderator here with Trust Level 4, they will see the mention and can confirm whether this is intentional behaviour, a known issue, or something that will be addressed. No further action is needed on your part — the right people are already looped in on this thread.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.