A hands-on lab that demonstrates how to achieve cross-database access on Amazon RDS for SQL Server without the TRUSTWORTHY property, using certificate-based module signing. All credentials are auto-generated and stored in AWS Secrets Manager — zero hardcoded passwords.
SQL Server DBAs migrating to Amazon RDS discover that ALTER DATABASE ... SET TRUSTWORTHY ON fails because the RDS master user does not have the sysadmin server role. This breaks any stored procedure that queries tables in another database. Module signing with certificates is the RDS-compatible, least-privilege alternative.
This lab deploys a fully automated environment where you can:
- Reproduce the cross-database access failure (Error 916)
- Apply certificate-based module signing as the fix
- Verify that only the signed procedure gains cross-database access
You run the walkthrough interactively in SQL Server Management Studio (SSMS) on a Windows workload host, using the queries in blog-demo-queries.sql.
┌────────────────────────────────────────────────────────────────────────┐
│ VPC (10.0.0.0/16) │
│ │
│ ┌────────────────────────────┐ ┌─────────────────────────────────┐ │
│ │ EC2 Workload Host │ │ RDS SQL Server Express │ │
│ │ (Windows Server 2022) │ │ (SQL Server 2022) │ │
│ │ │ │ │ │
│ │ • SSMS 20.2 (auto-install)│ │ • DatabaseA │ │
│ │ via boot scheduled task │───▶│ └─ dbo.GetSecretData (proc) │ │
│ │ • labadmin local admin │ │ • DatabaseB │ │
│ │ (for RDP login) │ │ └─ dbo.SecretData (table) │ │
│ │ │ │ │ │
│ │ Access: Systems Manager │ │ Access: port 1433 from │ │
│ │ Fleet Manager Remote │ │ EC2 security group only │ │
│ │ Desktop (RDP over SSM) │ └─────────────────────────────────┘ │
│ │ + SSM Session Manager │ │
│ │ No inbound ports open │ │
│ └──────────┬─────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────┐ │
│ │ AWS Secrets Manager │ │
│ │ │ │
│ │ • rds-master-password │ │
│ │ • app-user-password │ │
│ │ • db-master-key-a │ │
│ │ • db-master-key-b │ │
│ │ • cert-transfer-password │ │
│ │ • windows-admin-password │ │
│ └────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────┘
The workload host sits in a private subnet with no public IP and no inbound ports. Outbound internet (SSM registration, Secrets Manager, and the SSMS download) is via a NAT gateway. You reach the desktop through Fleet Manager Remote Desktop, which tunnels RDP over the SSM channel.
All credentials are auto-generated at stack creation and retrieved at runtime. No passwords appear in the template or command history.
| Secret | Purpose |
|---|---|
rds-master-password |
RDS admin login (also enriched with host/port via SecretTargetAttachment) |
app-user-password |
AppUser SQL login (the limited user) |
db-master-key-a |
Database master key for DatabaseA |
db-master-key-b |
Database master key for DatabaseB |
cert-transfer-password |
Private key encryption during certificate transfer |
windows-admin-password |
Local Windows labadmin account for Fleet Manager Remote Desktop login |
Note: The Windows AMI ships AWS Tools for PowerShell, not the
awsCLI. On the box, retrieve secrets withGet-SECSecretValue, notaws secretsmanager.
- AWS account with permissions to create VPCs, EC2, IAM roles, RDS instances, KMS keys, and Secrets Manager secrets.
- AWS CLI v2 installed and configured (for deploying the stack and reading secrets from your workstation).
aws --version aws sts get-caller-identity
- Session Manager plugin for the AWS CLI (used by Fleet Manager and for optional RDP port-forwarding).
Other platforms: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html
# macOS brew install --cask session-manager-plugin session-manager-plugin --version - A web browser for the Fleet Manager Remote Desktop console session. (Optional: a native RDP client such as Microsoft Remote Desktop if you prefer full clipboard/keyboard support over an SSM port-forward.)
export AWS_REGION=us-west-2No password parameter needed — Secrets Manager generates all credentials automatically.
aws cloudformation create-stack \
--stack-name rds-crossdb-lab \
--template-body file://rds-crossdb-lab-secretsmanager-test-passed.yaml \
--capabilities CAPABILITY_IAM \
--region $AWS_REGIONaws cloudformation wait stack-create-complete \
--stack-name rds-crossdb-lab --region $AWS_REGION \
&& echo "Stack ready!"aws cloudformation describe-stacks \
--stack-name rds-crossdb-lab --region $AWS_REGION \
--query 'Stacks[0].Outputs' --output tableUseful outputs: WorkloadDriverInstanceId, RdsEndpoint, WindowsAdminSecretArn, RdsMasterSecretArn, and GetWindowsAdminPasswordCommand.
The instance registers with Systems Manager a few minutes after it reaches running / 2 status checks. SSMS installs in the background via a one-time scheduled task and appears ~10–15 minutes after boot (it does not block login).
aws secretsmanager get-secret-value \
--secret-id rds-crossdb-lab/windows-admin-password \
--region $AWS_REGION --query SecretString --output textCopy the password value (username is labadmin).
Systems Manager console → Fleet Manager → select the node → Node actions → Connect with Remote Desktop → Authentication type: User credentials → labadmin + the password above.
No key pair exists on this instance, so the "Get Windows password" (key-pair) path does not apply — use User credentials with
labadmin.
If the browser client's keyboard/clipboard is awkward, port-forward RDP over SSM and use a native client:
aws ssm start-session \
--target <WorkloadDriverInstanceId> \
--document-name AWS-StartPortForwardingSession \
--parameters '{"portNumber":["3389"],"localPortNumber":["13389"]}' \
--region $AWS_REGIONThen connect your RDP client to localhost:13389 as labadmin.
Launch SQL Server Management Studio 20 from the Start menu, then open blog-demo-queries.sql. You will use two connections: one as admin for setup/signing, and a second as AppUser for the failure/success tests.
Get the endpoint and admin password (the RDS master secret is enriched with host/port). On the box:
$secret = (Get-SECSecretValue -SecretId 'rds-crossdb-lab/rds-master-password' -Region 'us-west-2').SecretString | ConvertFrom-Json
"Server : {0},{1}" -f $secret.host, $secret.port
"Login : {0}" -f $secret.username # admin
$secret.password | Set-Clipboard # paste into SSMS with Ctrl+VIn Connect to Server: Server name endpoint,1433, Authentication SQL Server Authentication, Login admin, Encryption Mandatory. Leave Trust server certificate unchecked — instead, download the Amazon RDS CA bundle and import it into the Windows Trusted Root Certification Authorities store so SSMS can validate the RDS server certificate. Checking "Trust server certificate" bypasses TLS certificate validation and must not be used.
| Part | Run as | What it does | Expected result |
|---|---|---|---|
| 1. Setup | admin | Create DatabaseA/B, dbo.SecretData, AppUser, unsigned dbo.GetSecretData |
Objects created; TRUSTWORTHY = 0 |
| 2a/2b | admin | Confirm TRUSTWORTHY off; try to enable it | is_trustworthy_on = 0; ALTER ... TRUSTWORTHY ON fails with Msg 15247 |
| 2c | AppUser | EXEC dbo.GetSecretData |
Msg 916 — cross-DB access denied |
| 3. Sign | admin | Master keys, CrossDBCert in DatabaseB, transfer to DatabaseA via sp_executesql, sign the proc |
Signature attached |
| 4. Verify | AppUser | EXEC dbo.GetSecretData |
Returns Acme Corp (780), Globex Inc (720) |
| 5. Least privilege | AppUser | EXEC dbo.GetSecretUnsigned |
Msg 916 — only the signed proc gains access |
| 6. Cleanup | admin | Drop databases + login | See below |
Notes for running in SSMS:
- Placeholders: replace
<APP_USER_PASSWORD>,<MK_A_PASSWORD>,<MK_B_PASSWORD>, and both<CERT_TRANSFER_PASSWORD>occurrences with the matching Secrets Manager values (fetch withGet-SECSecretValue). The two cert-transfer values must match. - Run as AppUser = open a second query window (File → New → Database Engine Query) with SQL auth as
AppUser. Error 916 only appears forAppUser; running asadminalways succeeds. - Batch scope: the certificate-transfer block (
DECLARE @cert ... EXEC DatabaseA.dbo.sp_executesql @sql;) must run as one selection — aGOin the middle drops the variables (Msg 137). Run it while connected to DatabaseB soCERT_ID('CrossDBCert')resolves. - Object Explorer doesn't auto-refresh after DDL — right-click Databases → Refresh to see new/removed objects.
| File | Purpose |
|---|---|
rds-crossdb-lab-secretsmanager-test-passed.yaml |
CloudFormation template (VPC, RDS, Windows workload host, KMS, Secrets Manager) |
blog-demo-queries.sql |
The full demo: setup, failure, module signing, verification, least-privilege proof, cleanup |
final_blog.md |
The blog post write-up |
Removes RDS, EC2, secrets, KMS key, networking — everything — in one step.
aws cloudformation delete-stack \
--stack-name rds-crossdb-lab --region $AWS_REGION
aws cloudformation wait stack-delete-complete \
--stack-name rds-crossdb-lab --region $AWS_REGION \
&& echo "Stack deleted!"Run Part 6 in blog-demo-queries.sql from an admin query window connected to master. The script:
- Kills any sessions connected to DatabaseA/DatabaseB or logged in as AppUser (this prevents "database is currently in use" errors).
- Forces
SET SINGLE_USER WITH ROLLBACK IMMEDIATE+DROP DATABASEfor each database — cascading removes all contained procedures, certificates, users, master keys, and tables automatically. - Drops the server-level
AppUserlogin. - Ends with a verify query returning no rows.
The cleanup is idempotent — safe to re-run with no "does not exist" errors.
Important: close any SSMS tabs connected as AppUser or connected to DatabaseA/DatabaseB before running cleanup (or let the
KILLblock handle them). A bareDROP DATABASEwill fail with "database is currently in use" if leftover sessions exist.
| Resource | Sizing | Approx. hourly cost |
|---|---|---|
| RDS SQL Server Express | db.t3.xlarge, 100 GB gp3 | ~$0.35/hr |
| EC2 workload host | t3.large (Windows Server 2022) | ~$0.10/hr |
| NAT Gateway | per-hour + per-GB | ~$0.05/hr |
| Secrets Manager | 6 secrets | ~$0.00 |
| Data transfer (intra-VPC) | Minimal | ~$0.00 |
A few hours of testing costs roughly $3–5. Delete the stack when done. Prices based on us-west-2. Use the AWS Pricing Calculator for current estimates.
| Symptom | Fix |
|---|---|
| Fleet Manager doesn't show the node | Wait 3–5 minutes after 2/2 status checks. If still absent after 10+ minutes, stop/start the instance (not terminate — just stop → start) to force SSM re-registration. |
| "Unable to establish Remote Desktop connection" | Either UserData hasn't finished (give it 3–5 min), or the password is wrong. Verify labadmin exists via Session Manager: Get-LocalUser labadmin. |
| SSMS not in Start menu | The background scheduled task installs it ~10–15 min after boot. Check progress: Get-Content C:\Windows\Temp\ssms-install.log -Wait. If it failed, re-run manually: see Troubleshooting SSMS install below. |
| SSMS Connect fails (timeout) | Make sure you use the RDS endpoint (not instance ID) and port 1433. The RDS SG only allows connections from the EC2 SG. |
| Msg 916 as admin | Not expected. Double-check you're running as AppUser — admin always succeeds. |
| "database is currently in use" on cleanup | Close extra SSMS tabs or run the KILL block in Part 6. See the cleanup section above. |
Get-SECSecretValue errors |
Ensure you're running on the EC2 box (not your laptop) and the stack name in the secret-id is correct. |
| Stack creation fails | aws cloudformation describe-stack-events --stack-name rds-crossdb-lab --query 'StackEvents[?ResourceStatus==\CREATE_FAILED`]'` |
| Can't type in Fleet Manager RDP session | Click into the remote desktop area to give it focus. If keyboard remains unresponsive, try a different browser or use the native RDP port-forward (see Connect section). |
If SSMS didn't install automatically (the scheduled task failed), install manually from PowerShell on the box:
$out = "$env:TEMP\SSMS-Setup-ENU.exe"
Invoke-WebRequest -Uri 'https://go.microsoft.com/fwlink/?linkid=2313753&clcid=0x409' -OutFile $out -UseBasicParsing
Start-Process -FilePath $out -ArgumentList '/Install','/Quiet','/Norestart' -WaitThis installs SSMS 20.2.1 (self-contained, ~660 MB, no runtime download). Takes ~10 min.
- All passwords are randomly generated (24–30 characters, no special-character conflicts)
- EC2 IAM role has least-privilege access: only
secretsmanager:GetSecretValueon the 6 lab secrets +kms:Decrypton the lab CMK AmazonSSMManagedInstanceCoremanaged policy for SSM / Fleet Manager connectivity- RDS instance is not publicly accessible — reachable only from the EC2 security group on port 1433
- No inbound ports on the EC2 security group — Fleet Manager tunnels RDP over SSM (no port 3389 open)
- RDS master secret is attached to the instance via
SecretTargetAttachment(enables future rotation) - No passwords are logged, printed to stdout, or stored in plain text on disk
- The
labadminlocal account password is fetched from Secrets Manager at boot and set securely; it never appears in the template