AWS Database Blog

Cross-database access using module signing on Amazon RDS for SQL Server

If you need cross-database access on Amazon Relational Database Service (Amazon RDS) for SQL Server but can’t enable the TRUSTWORTHY database property, module signing with certificates is the secure, RDS-compatible alternative. In this post, we show you how to use certificate-based module signing to grant cross-database permissions to specific stored procedures. This approach works without TRUSTWORTHY and provides a stronger security posture, scoped to individual procedures rather than to an entire trusted database.

Many database administrators and application developers migrating Microsoft SQL Server workloads to Amazon RDS rely on TRUSTWORTHY for patterns such as stored procedures that query tables in other databases, Service Broker activation across databases, or impersonation with EXECUTE AS. On-premises, enabling TRUSTWORTHY is a one-line ALTER DATABASE statement. On Amazon RDS for SQL Server, however, you can’t set this property because it requires the sysadmin server role, which isn’t granted to the RDS master user. This post is written primarily for database administrators planning migrations and developers maintaining cross-database logic after migration. Whether you’re evaluating workarounds before a move or remediating issues after one, the approach applies equally.

Note: If you’re using Amazon RDS Custom for SQL Server, you have OS-level access and can use the standard file-based certificate transfer (BACKUP CERTIFICATE ... TO FILE / CREATE CERTIFICATE ... FROM FILE) directly. The module signing technique in this post is specifically for fully managed Amazon RDS for SQL Server, where filesystem access is not available.

Solution overview

The TRUSTWORTHY database property tells the SQL Server instance to trust the database and its contents. When enabled, modules (stored procedures, functions, triggers) that use impersonation or cross-database references can extend their security context beyond the local database boundary. Although it’s convenient, Microsoft recommends against using TRUSTWORTHY because it opens a broad privilege escalation path. Any db_owner in a trusted database owned by a sysadmin login can escalate to server-level sysadmin privileges.

Module signing is the recommended alternative. Instead of trusting an entire database, you sign a specific stored procedure with a certificate. SQL Server then grants the permissions associated with the certificate-mapped user in the target database to that procedure alone. This follows the principle of least privilege: the permission grant is scoped to a single module, not the entire database.

The following steps describe the module signing flow:

  1. A certificate is created in DatabaseB (the target database) and mapped to a user with SELECT permission on the target table.
  2. The same certificate is transferred to DatabaseA (the calling database) by using dynamic SQL with sp_executesql. This is the RDS-compatible approach that avoids filesystem access.
  3. The stored procedure in DatabaseA is signed with the certificate.
  4. When AppUser runs the signed procedure, SQL Server recognizes the certificate signature and temporarily adds the certificate-mapped user’s permissions from DatabaseB to the runtime context.

Disclaimer

  • This code is for demonstration only and isn’t intended for production.
  • We recommend that you add an explicit EXPIRY_DATE in the CREATE CERTIFICATE statement and set up rotation before expiry.
  • We recommend that you store database master key (DMK) passwords in AWS Secrets Manager and document the recovery procedure as needed.
  • Because this is only a demo, we don’t recommend hardcoding passwords.
  • We added extra characters next to the passwords to prevent copy and paste.

Prerequisites

To follow along with this walkthrough, you need:

  • An AWS account.
  • An Amazon RDS for SQL Server instance. Any edition works (Express, Web, Standard, or Enterprise). This post uses SQL Server 2022 Express Edition on a db.t3.xlarge instance.
  • A SQL client such as SQL Server Management Studio (SSMS) or sqlcmd (included in mssql-tools18) connected to the RDS instance as the master user.

You can provision this entire environment automatically with the accompanying AWS CloudFormation template (rds-crossdb-lab-secretsmanager.yaml). The template creates the Amazon RDS for SQL Server instance and an Amazon EC2 workload host running Windows Server 2022. It auto-generates every password in AWS Secrets Manager: the RDS master user, AppUser, the two database master keys, and the certificate-transfer password. Each password is encrypted with a customer managed AWS Key Management Service (AWS KMS) key, so no credential is hardcoded anywhere. This replaces the earlier approach of passing the RDS master password as a stack parameter.

You reach the Windows workload host through AWS Systems Manager Fleet Manager Remote Desktop (no inbound RDP port is opened on the security group), then run the SQL steps from SSMS installed on that host. Retrieve each password at runtime with Get-SECSecretValue from AWS Tools for PowerShell rather than typing it. When you connect SSMS to RDS, set Encryption to Mandatory and validate the server certificate by importing the Amazon RDS CA bundle into the Windows Trusted Root Certification Authorities store. Do not enable Trust Server Certificate.

Understanding the limitation

To confirm the limitation exists, connect to your RDS instance as the master user (we use admin in this post) and run the following setup.

Create the test databases and objects

Create two databases, a target table with sample data, and a limited login:

-- Create the two databases
IF DB_ID('DatabaseA') IS NULL CREATE DATABASE DatabaseA;
GO
IF DB_ID('DatabaseB') IS NULL CREATE DATABASE DatabaseB;
GO

-- Create the target table in DatabaseB with sample data
USE DatabaseB;
GO
CREATE TABLE dbo.SecretData (
Id INT PRIMARY KEY,
CustomerName VARCHAR(100),
CreditScore INT
);
INSERT INTO dbo.SecretData VALUES
(1, 'Acme Corp', 780),
(2, 'Globex Inc', 720);
GO

-- Create a limited login
USE master;
GO
CREATE LOGIN AppUser
WITH PASSWORD = "Your-Password", -- Intentional character to prevent copy-paste
CHECK_POLICY = OFF,
DEFAULT_DATABASE = DatabaseA;
GO

-- Map the login as a user in DatabaseA with EXECUTE only
USE DatabaseA;
GO
CREATE USER AppUser FOR LOGIN "AppUser"; -- Intentional character to prevent copy-paste
GRANT EXECUTE TO AppUser;
GO

-- Create the cross-database stored procedure
CREATE PROCEDURE dbo.GetSecretData
AS
SELECT Id, CustomerName, CreditScore
FROM DatabaseB.dbo.SecretData;
GO

Confirm TRUSTWORTHY is OFF

SELECT name, is_trustworthy_on
FROM sys.databases
WHERE name IN ('DatabaseA', 'DatabaseB');

Expected output:

name is_trustworthy_on
DatabaseA 0
DatabaseB 0

Demonstrate the cross-database access failure

Connect as AppUser and run the procedure:

-- Connect as AppUser (AppUser / <YOUR-APP-PASSWORD>) to DatabaseA
USE DatabaseA;
GO
EXEC dbo.GetSecretData; <-- Connect as AppUser with YOUR-APP-PASSWORD
GO

This fails with Error 916:

Msg 916, Level 14, State 1
The server principal "AppUser" is not able to access the database
"DatabaseB" under the current security context.

AppUser has EXECUTE permission on the procedure in DatabaseA, but the procedure references DatabaseB, which AppUser cannot access. Without TRUSTWORTHY, the security context cannot cross the database boundary.

Demonstrate the TRUSTWORTHY limitation on RDS

Back as the admin user, attempt to enable TRUSTWORTHY:

ALTER DATABASE DatabaseA SET TRUSTWORTHY ON;
GO

This fails with Error 15247:

Msg 15247, Level 16, State 1
User does not have permission to perform this action.

The RDS master user doesn’t have the sysadmin server role, which is required to set the TRUSTWORTHY property. This restriction applies across all RDS for SQL Server editions and engine versions. It stems from the managed-service design, in which AWS retains control of server-level operations to maintain the service boundary.

Implementing the workaround with module signing

Module signing uses certificates to grant cross-database permissions to specific signed modules. The process involves four steps:

  1. Create database master keys in both databases.
  2. Create a certificate in DatabaseB and map it to a user with the required permissions.
  3. Transfer the certificate to DatabaseA and sign the stored procedure.
  4. Verify the result.

Step 1: Create database master keys

Database master keys are required for certificate operations:

USE DatabaseA;
GO
CREATE MASTER KEY ENCRYPTION BY PASSWORD = <YOUR-PASSWORD>; -- Intentional character to prevent copy-paste
GO

USE DatabaseB;
GO
CREATE MASTER KEY ENCRYPTION BY PASSWORD = <YOUR-PASSWORD>; -- Intentional character to prevent copy-paste
GO

Step 2: Create a certificate in DatabaseB and map it to a user

Create a certificate in the target database, then create a user from that certificate and grant it the required permissions:

USE DatabaseB;
GO
CREATE CERTIFICATE CrossDBCert
WITH SUBJECT = 'Cross-DB access cert';
GO

CREATE USER CertUser FROM CERTIFICATE CrossDBCert;
GRANT SELECT ON dbo.SecretData TO CertUser;
GO

CertUser is a certificate-mapped user, not a login. Nobody can connect to SQL Server as CertUser. Its sole purpose is to hold the SELECT permission that the signed procedure inherits at runtime.

Step 3: Transfer the certificate to DatabaseA and sign the procedure

This is the step where Amazon RDS differs from a self-managed SQL Server instance.

Why the file-based approach fails on RDS:

On-premises, the typical path is BACKUP CERTIFICATE ... TO FILE followed by CREATE CERTIFICATE ... FROM FILE. On RDS, the master user has no write access to the underlying Windows filesystem, so this approach fails with a permission error.

Why manual hex copy-paste is fragile:

The alternative documented in many guides is to extract the certificate as hex with CERTENCODED() and paste it into CREATE CERTIFICATE ... FROM BINARY, but this is error-prone in practice. Terminal tools like sqlcmd truncate long output by default, and CREATE CERTIFICATE ... FROM BINARY only accepts hex literals (not variables), which makes it difficult to script.

The recommended approach — dynamic SQL with sp_executesql:

The most reliable RDS-compatible method keeps everything server-side. When you build the CREATE CERTIFICATE ... FROM BINARY statement as a dynamic SQL string inside SQL Server, the full binary never passes through the terminal, which removes truncation and copy-paste errors:

USE DatabaseB;
GO

-- Extract cert and key as VARBINARY, build the CREATE CERTIFICATE
-- statement dynamically, and run it against DatabaseA.
DECLARE @cert VARBINARY(MAX) = CERTENCODED(CERT_ID('CrossDBCert'));
DECLARE @key VARBINARY(MAX) = CERTPRIVATEKEY(CERT_ID('CrossDBCert'), '<Store-Password-In-Secrets'); -- Intentional syntax error to prevent copy-paste
DECLARE @sql NVARCHAR(MAX);

SET @sql = N'CREATE CERTIFICATE CrossDBCert FROM BINARY = '
+ CONVERT(VARCHAR(MAX), @cert, 1)
+ N' WITH PRIVATE KEY (BINARY = '
+ CONVERT(VARCHAR(MAX), @key, 1)
+ N', DECRYPTION BY PASSWORD = '<YOUR-DECRYPTION-PASSWORD>');'; -- Intentional syntax error to prevent copy-paste

-- Run in the context of DatabaseA
EXEC DatabaseA.dbo.sp_executesql @sql;
GO

How this works:

  1. CERTENCODED() extracts the certificate’s public portion as VARBINARY(MAX).
  2. CERTPRIVATEKEY() extracts the private key, encrypted with the transfer password <YOUR-TRANSFER-PASSWORD>.
  3. CONVERT(..., 1) renders each VARBINARY as a 0x... hex string inside the dynamic SQL.
  4. sp_executesql runs the fully formed CREATE CERTIFICATE ... FROM BINARY statement against DatabaseA. By the time it runs, the hex is already a literal in the string, which satisfies the parser requirement.

Important: CERT_ID() resolves in the current database context. You must run this while connected to DatabaseB (where the certificate was created). Running it from DatabaseA returns NULL.

Now sign the stored procedure with the imported certificate:

USE DatabaseA;
GO
ADD SIGNATURE TO dbo.GetSecretData
BY CERTIFICATE CrossDBCert;
GO

Step 4: Verify the result

Confirm the signature is attached:

USE DatabaseA;
GO
SELECT
OBJECT_NAME(cp.major_id) AS signed_module,
c.name AS certificate_name,
cp.crypt_type_desc AS signature_type
FROM sys.crypt_properties cp
JOIN sys.certificates c ON cp.thumbprint = c.thumbprint;
GO

Output:

signed_module certificate_name signature_type
GetSecretData CrossDBCert SIGNATURE BY CERTIFICATE

Now test cross-database access as AppUser:

-- Connect as AppUser (AppUser / <YOUR-APP-PASSWORD>) to DatabaseA
USE DatabaseA;
GO
EXEC dbo.GetSecretData;
GO

Output:

Id CustomerName CreditScore
1 Acme Corp 780
2 Globex Inc 720

AppUser can now read data from DatabaseB through the signed procedure. This works without TRUSTWORTHY, without sysadmin, and without granting AppUser any direct access to DatabaseB.

Verifying least privilege

To confirm that module signing is targeted and does not grant blanket cross-database access, create an identical but unsigned procedure:

-- As admin:
USE DatabaseA;
GO
CREATE PROCEDURE dbo.GetSecretUnsigned
AS
SELECT Id, CustomerName, CreditScore
FROM DatabaseB.dbo.SecretData;
GO
GRANT EXECUTE ON dbo.GetSecretUnsigned TO AppUser;
GO

As AppUser:

USE DatabaseA;
GO
EXEC dbo.GetSecretUnsigned;
GO

This fails with the same Error 916:

Msg 916, Level 14, State 1
The server principal "AppUser" is not able to access the database
"DatabaseB" under the current security context.

Only the signed procedure receives cross-database access. The unsigned procedure, which has identical code and permissions, is denied. This confirms that module signing follows the principle of least privilege.

Automating the transfer in continuous integration and continuous delivery (CI/CD) pipelines

The dynamic SQL approach shown in Step 3 works directly from sqlcmd or any SQL client with no external dependencies. For application-level automation (Python, C#, Windows PowerShell), you can also extract the binary at the driver layer:

import pyodbc

conn_str = (
"DRIVER={ODBC Driver 18 for SQL Server};"
f"SERVER={rds_endpoint},1433;UID="admin;PWD={"your-password"};"  # Intentional character to prevent copy-paste
"Encrypt=yes;TrustServerCertificate=yes"
)

with pyodbc.connect(conn_str) as cn:
cn.autocommit = True
cur = cn.cursor()

# Extract cert from DatabaseB as bytes
cur.execute("USE DatabaseB")
cur.execute("""
SELECT CERTENCODED(CERT_ID('CrossDBCert')),
CERTPRIVATEKEY(CERT_ID('CrossDBCert'), ?)
""", ("Store-Password-In-Secrets",))
cert_bytes, key_bytes = cur.fetchone()

# Build FROM BINARY literal
cert_hex = "0x" + cert_bytes.hex().upper()
key_hex = "0x" + key_bytes.hex().upper()

cur.execute("USE DatabaseA")
cur.execute(f"""
CREATE CERTIFICATE CrossDBCert
FROM BINARY = {cert_hex}
WITH PRIVATE KEY (
BINARY = {key_hex},
DECRYPTION BY PASSWORD = '<YOUR-DECRYPTION-PASSWORD>' -- Intentional character to prevent copy-paste
)
""")

# Sign the module
cur.execute("ADD SIGNATURE TO dbo.GetSecretData BY CERTIFICATE CrossDBCert")

However, for most use cases, the T-SQL dynamic SQL approach from Step 3 is simpler. It requires no external tools and works from any SQL client.

For validated TLS in the preceding automation example, import the Amazon RDS CA bundle on the client host and use Encrypt=yes;TrustServerCertificate=no instead of TrustServerCertificate=yes, and source the connection password from AWS Secrets Manager instead of embedding it in the connection string.

Pre-migration checklist

If you’re migrating SQL Server workloads to Amazon RDS, audit your databases for TRUSTWORTHY dependencies before migration:

Find databases with TRUSTWORTHY enabled:

SELECT name, is_trustworthy_on
FROM sys.databases
WHERE is_trustworthy_on = 1
AND name NOT IN ('msdb'); -- msdb is TRUSTWORTHY by design

Find modules using EXECUTE AS (potential cross-database impersonation):

SELECT
DB_NAME() AS database_name,
OBJECT_NAME(object_id) AS module_name,
execute_as_principal_id
FROM sys.sql_modules
WHERE execute_as_principal_id IS NOT NULL;

Find cross-database references in stored procedures:

SELECT
OBJECT_NAME(referencing_id) AS referencing_module,
referenced_database_name,
referenced_schema_name,
referenced_entity_name
FROM sys.sql_expression_dependencies
WHERE referenced_database_name IS NOT NULL
AND referenced_database_name <> DB_NAME();

For each dependency found, implement module signing as described in this post before disabling TRUSTWORTHY on the source or migrating to Amazon RDS.

Key considerations

When you implement module signing in production, keep the following operational aspects in mind.

Certificates have an expiration date, so plan for periodic certificate rotation. The process involves creating a new certificate in the target database, mapping it to a user with the same grants, transferring it to the calling database, and re-signing the affected modules. During the transition, signed modules can carry multiple signatures simultaneously, so you can add the new signature before removing the old one. This avoids any downtime window.

Be aware of signature invalidation when modifying signed modules. If you alter a signed stored procedure (using ALTER PROCEDURE), SQL Server automatically removes the signature. You must re-sign the procedure after any modification. This is a security feature that prevents silent permission retention across code changes. It ensures that any change to the module’s logic requires an explicit re-authorization step.

If a procedure accesses tables in multiple target databases, you need a certificate-mapped user with the appropriate grants in each target database. You can use the same certificate across all of them, transferred through the same dynamic SQL approach shown in Step 3. Each target database independently validates the certificate thumbprint, so a single certificate can authorize access to as many databases as needed.

The DECRYPTION BY PASSWORD value used during certificate transfer is a transfer-time secret for the private key. Treat it like any other credential: keep it out of source control, rotate it periodically, and use AWS Secrets Manager to store and retrieve it in automated workflows.

Finally, the dynamic SQL approach requires no filesystem access. The certificate binary is extracted, transferred, and imported entirely in memory within SQL Server. No files are written to disk at any point, which aligns with compliance requirements that prohibit storing key material on shared storage. We recommend that you consult your compliance team. Under the AWS shared responsibility model, assessing compliance is your responsibility.

Cleaning up

To avoid ongoing charges, delete the resources you created as part of this post:

  1. Delete your RDS instance.
  2. Delete your EC2 instance.
  3. Drop the certificates, users, logins, and keys created during the demo.

If you deployed the accompanying CloudFormation stack, the simplest cleanup is to delete the stack. This removes the RDS instance, the Windows EC2 workload host, the Secrets Manager secrets, the KMS key, and all networking in a single step.

Conclusion

In this post, we showed you how to achieve cross-database access on Amazon RDS for SQL Server using module signing with certificates. Module signing is a secure, least-privilege alternative to the TRUSTWORTHY database property, which cannot be enabled on RDS.

The key RDS-specific finding is that the standard file-based certificate transfer (BACKUP CERTIFICATE TO FILE) does not work on Amazon RDS. The recommended approach is to use dynamic SQL with sp_executesql to build and run the CREATE CERTIFICATE ... FROM BINARY statement entirely server-side. This approach avoids filesystem access, removes terminal truncation issues, and works from any SQL client without external tooling.

To get started, try this approach in your own RDS environment using the steps in this post. For more information, see the following resources:


About the authors

Nirupam Datta

Nirupam Datta

Nirupam is a Sr. Technical Account Manager at AWS. He has been with AWS for over 5 years. With over 14 years of experience in database engineering and infrastructure architecture, Nirupam is also a subject matter expert in the Amazon RDS core systems, Amazon RDS for SQL Server, and Amazon Aurora MySQL. He provides technical assistance to customers, guiding them to migrate, optimize, and navigate their journey in the AWS Cloud.

Venkatesh Arveti

Venkatesh Arveti

Venkatesh is a Technical Account Manager at AWS with over 8 years of hands-on experience in networking, databases, and infrastructure architecture. Venkatesh partners with enterprise customers to optimize their cloud workloads, guiding them through migration strategies, resilience planning, and infrastructure design that balances performance with cost efficiency.

Ranvir Singh

Ranvir Singh

Ranvir is a Technical Account Manager at AWS with 5 years of hands on experience with cloud storage. Ranvir partners with enterprise customers to optimize their workloads, align cloud strategies with their business objectives, performance optimization, implement best cloud practices and help build cost-effective infrastructure.