powershelldba.de · Uwe Janke

AlwaysOn with Standard vs. Contained Databases: Where Login Sync and Database Moves Break

An Availability Group replicates databases, not everything a database depends on. Two schematics below show exactly what crosses the wire and what does not: a standard database, where authentication lives outside the AG entirely, and a contained database, where it travels with the database itself. The difference decides how much manual work follows every failover, and every time a database moves to a new server or a new AG.

What an Availability Group Actually Copies

An AG replicates the databases you add to it: log records shipped from primary to secondary, the same mechanism behind database mirroring. It does not replicate anything that lives at the instance level: server logins, SQL Server Agent jobs, linked servers, credentials, certificates outside the database's own master key. Whether that gap matters depends on where user authentication actually lives, which is the one design choice between a standard database and a contained database.

Traditional AlwaysOn: Standard Database

Standard database — users live outside the AG SQL01 · Primary SERVER LOGINS AppSvc, ReportUser, sa … INSIDE THE AVAILABILITY GROUP DATABASE tables · schema · data (no user metadata) SQL02 · Secondary SERVER LOGINS separate copy, own SIDs INSIDE THE AVAILABILITY GROUP DATABASE tables · schema · data (kept current via log shipping) replicates automatically × not replicated
Server logins never cross the Availability Group boundary. Each replica keeps its own copy, matched by SID by hand or by a scheduled job.

The database box replicates cleanly, that part AlwaysOn was built for. The logins box next to it does not exist inside the AG at all. SQL02 either already has a login called AppUser with the same SID as SQL01, because someone put it there, or it does not, and the application starts failing the moment SQL02 becomes primary.

Problem 1: Server Logins Go Stale the Moment Anyone Stops Watching

A login created on the primary after go-live does not exist on the secondaries, and never will on its own. This isn't a setup-time task, it's a standing operational gap: every new application login, every password rotation, every new service account has to be pushed to every replica for as long as the AG exists.

The failure mode is specific and easy to misdiagnose. SQL Server maps database permissions through the login's SID, not its name, so a login with the right name but the wrong SID on a secondary produces an orphaned user after failover: the application connects successfully (the login exists), then gets permission errors inside the database (the SID doesn't map). The fix is a one-liner once you know that's the problem:

ALTER USER AppUser WITH LOGIN = AppUser;

The harder part is noticing it needs to run, on the right database, on the replica that just became primary, at 3 a.m. This is covered in more depth, including a scheduled-job approach, in Syncing SQL Logins Across an AlwaysOn Availability Group.

Standing fix for standard databases: Sync-sqmLoginsToAlwaysOn detects the current primary automatically and pushes logins outward with matching SIDs; New-sqmAutoLoginSyncJob turns that into a recurring SQL Server Agent job instead of a task someone has to remember. Full reference: sqmSQLTool commands.

When the Application Creates Its Own Logins

Problem 1 above assumes a DBA or a change process creates the login, which at least means someone in the room knew it happened. Plenty of applications, ERPs, trading and portfolio-management platforms such as FrontArena among them, provision their own SQL logins and database users directly as part of onboarding a new user or a new integration, through the application's own admin screen, with no DBA in that workflow at all.

That changes the shape of the problem, not just its size:

Containment does not fix this on its own. Whether a newly created principal lands as a contained database user or a classic server login is decided entirely by which statement the application issues at creation time: CREATE USER ... WITH PASSWORD for a contained user, versus the classic CREATE LOGIN followed by CREATE USER ... FOR LOGIN. An application was written for one pattern or the other; turning on CONTAINMENT = PARTIAL on the database it happens to be using does not make it switch. Most commercial, off-the-shelf applications, particularly ones that predate SQL Server 2012, use the classic pattern regardless of what the target database's containment setting is. Assume any application you did not build yourself keeps creating server-level logins whichever way the database is configured, and keep the sync job running either way.

Combine sync with an audit, not just sync alone: Sync-sqmLoginsToAlwaysOn / New-sqmAutoLoginSyncJob keep an application-created login usable after failover, but they replicate whatever security posture that login already has. Invoke-sqmLoginAudit is what actually catches the policy-off, never-expiring, over-permissioned login the application just created, on a schedule, so app-managed provisioning doesn't quietly become the least-audited path into the instance.

Problem 2: Moving a Database to a New Server or a New AG

Failover keeps a database inside the same AG, on a replica that was already prepared for it. Moving a database is a different operation: consolidating instances, decommissioning old hardware, standing up a new AG in a different region, or handing a database to a different team. With a standard database, the login problem above doesn't just recur once, it becomes the entire migration checklist:

How a Contained Database Changes the Picture

A database with CONTAINMENT = PARTIAL can hold its own users directly: a password hash for a SQL-authenticated contained user, or a Windows SID for a Windows-authenticated one, stored inside the database instead of pointing at a server login. Because the AG ships log records for the whole database, that user catalog travels along for free.

AlwaysOn with a Contained Database

Contained database — users travel with the data SQL01 · Primary SERVER LOGINS sysadmin · Agent · services only INSIDE THE AVAILABILITY GROUP DATABASE CONTAINED USERS password hash / SID mapping, stored in the DB SQL02 · Secondary SERVER LOGINS sysadmin · Agent · services only INSIDE THE AVAILABILITY GROUP DATABASE CONTAINED USERS arrives with the database, no separate sync replicates automatically, users included still local, still needs sync
Contained users move with the database, no orphaned users after failover. The server logins that remain are the ones with nothing to do with this specific database.

Failover no longer touches the login problem for these users at all: SQL02 already has whatever the database's contained users need, because it arrived with the last log record shipped. Moving the database to a different server or a different AG carries the same benefit, restore the database (or add it to a new AG), and its users are already there, correct SIDs and all.

Turning On Containment Doesn't Convert Existing Users

Enabling containment on a database that already has users is additive, not transformative:

ALTER DATABASE MyDb SET CONTAINMENT = PARTIAL;

This makes it possible for the database to hold contained users from now on. It does not touch a single user that already exists. Every user mapped to a server login before the change is still mapped to that exact same server login afterward, still exposed to the identical orphaned-user and SID-mismatch failure from Problem 1. Nothing about running this one statement moves anyone onto contained authentication.

To actually convert an existing SQL-authenticated user, SQL Server ships a system procedure for exactly that:

EXEC sp_migrate_user_to_contained
    @username = 'AppUser',
    @rename = 'keep_name',
    @disablelogin = 'do_not_disable_login';

@disablelogin lets the original server login keep working during a transition, in case another database or another application still depends on it, or disables it once nothing else does. This has to be run once per user, on the primary; it is a one-time migration step, not something a recurring sync job should ever do on its own.

A partially contained database is not all-or-nothing: newly onboarded users can be created as contained from day one while long-standing users stay login-mapped until someone gets around to converting them, both kinds coexisting in the same database indefinitely if that's where the migration stalls.

Moving a Contained Database to Another Domain

A contained SQL login's credential, its password hash, is a self-contained fact stored inside the database. It references nothing outside itself, which is exactly what makes it portable to any server, any AG, any domain, with no extra work. A contained Windows-authenticated user does not get the same portability, and the two look identical in sys.database_principals, which is what makes this easy to miss.

A contained Windows user's identity is still an Active Directory SID. Containment stores that SID inside the database instead of relying on a matching server login, which solves the AlwaysOn replication gap from Problem 1, but it does nothing about the SID's dependency on the domain that issued it. Move the database to a server in a different domain, or a domain outside the original AD trust (a divested subsidiary, an acquisition being separated onto its own infrastructure, a DR site in a different forest), and the stored SID stops resolving to anyone. The failure mode is the same orphaned-Windows-login problem a standard database has, just quieter: the user still exists in the catalog, containment status hasn't changed, nothing looks broken until DOMAIN\jsmith tries to connect from the new environment and there is no AD account with that SID left for SQL Server to check against.

Portability is a SQL-authentication story, not a blanket guarantee Cross-domain database portability, the scenario containment is usually pitched for, only pays off fully for SQL-authenticated contained users. A database whose users are predominantly Windows-authenticated, common in environments that standardized on Windows auth precisely for the audit and traceability a hardened baseline wants, needs the same identity-remapping work after a domain move whether or not the database is contained; the only difference is recreating those identities as contained Windows users against the new domain's accounts, instead of as server logins.

What Containment Fixes, and What It Doesn't

Aspect Standard database Contained database
App users after failover Orphaned unless pre-synced with matching SIDs Arrive with the database, no action needed
Moving the database to a new server/AG Recreate every login, re-map every user Users travel with the database automatically
SQL Agent jobs, linked servers, credentials Instance-level, not covered either way Instance-level, not covered either way
sysadmin / service accounts Server logins, must still be synced Server logins, must still be synced
Cross-database queries to other DBs on the instance Work normally Deliberately restricted by design, need extra configuration
Requires instance-level opt-in No Yes, contained database authentication must be enabled
Existing users after enabling containment N/A Stay login-mapped until individually converted (sp_migrate_user_to_contained)
Windows-authenticated users after a domain move Orphaned, same as any Windows login Also orphaned, the stored AD SID doesn't resolve in the new domain

Containment solves the piece of the migration checklist that was the most repetitive and the easiest to get wrong: the per-user, per-login remapping. It does not solve the rest of it. A database move still needs its own checklist for Agent jobs, linked servers, and any credential or certificate that lived at the instance level, whether or not the database itself is contained.

The Security Trade-off That Gets Skipped

Contained database authentication is off by default and has to be turned on explicitly:

EXEC sp_configure 'contained database authentication', 1;
RECONFIGURE;

ALTER DATABASE MyDb SET CONTAINMENT = PARTIAL;

That setting is an instance-wide authentication policy change, not a per-database convenience flag. A contained user authenticates straight into a database without a server-level login in the path at all, which means server-level login auditing and server-level login triggers never see that connection the way they see everything else. Anyone with permission to create users inside a contained database can create another contained user without an instance-level administrator being involved.

Why hardened environments push back on this Security baselines built around centralizing authentication at the server level, and auditing every login through one choke point, treat contained database authentication as something to review and often restrict, not something to enable by default. Turning it on for AlwaysOn convenience is a policy decision for whoever owns that baseline, not a DBA-level toggle.

sqmSQLTool Support for "Normal" AlwaysOn

Everything in this article applies whether or not a single line of PowerShell is involved. For a standard-database AG built and run with sqmSQLTool, these are the functions that carry the actual work:

Setup and topology

Operations and recovery

Health and drift reporting

The login and configuration drift this article is actually about

Moving across AGs, sites, or domains

Full command reference: Every function above, with parameters and examples, is documented at sqmSQLTool commands.

Practical Guidance

Questions People Actually Ask

Q: What is the difference between a standard and a contained database in SQL Server AlwaysOn? A standard database stores no user authentication data of its own. Its database users map to server-level logins, which live outside the database and outside the Availability Group. A contained database (CONTAINMENT = PARTIAL) can hold its own users directly, with a password hash or a Windows SID stored inside the database, so no matching server login is required to connect.
Q: Why do SQL Server logins need to be synced manually in an AlwaysOn Availability Group? An Availability Group replicates the databases added to it, nothing else. Server-level logins exist outside any database, so they are never part of AG data movement. Every replica needs its own copy of every login, with a matching SID, kept in sync by hand or by a scheduled job.
Q: Do contained databases eliminate the need for login synchronization in AlwaysOn? For the users inside that specific database, yes: contained users replicate along with the database itself, so there is no orphaned-user problem after failover. It does not remove the need to sync server-level logins used for anything outside that one database, such as SQL Agent, sysadmin access, or logins shared across multiple databases on the instance.
Q: Is enabling contained database authentication a security risk? It changes the trust boundary rather than simply removing a limitation. A contained user can authenticate directly against a database without ever passing through a server-level login, which means server-level login auditing and login triggers do not see that connection the same way. Hardened environments often restrict or disable contained database authentication for exactly this reason, so turning it on should go through the same review as any other authentication change.
Q: Does converting a database to CONTAINMENT = PARTIAL automatically convert its existing users? No. Enabling containment only makes it possible for the database to hold contained users going forward; every user that already existed stays mapped to its server login exactly as before, still exposed to the same orphaned-user risk. Converting an existing user requires running sp_migrate_user_to_contained for that user individually.
Q: What happens to a contained database's users when it moves to a different Active Directory domain? SQL-authenticated contained users move without issue, their credential is a password hash stored entirely inside the database. Windows-authenticated contained users do not: their identity is still an Active Directory SID from the original domain, and that SID stops resolving to anyone once the database is on a server in a different domain, producing the same orphaned-login failure a standard database would have.
Q: What happens when an application creates its own SQL logins, for example an ERP or trading platform? The same login-sync gap as any other login, except with no change window to catch it: the application can create a login at any time with no DBA involved, so only an unattended, scheduled sync job closes the gap reliably. Containment does not change this, since whether the application creates a classic server login or a contained user is decided entirely by which statement the application itself issues, not by the database's containment setting.