Engineering NotesDatabase connections and OwlDB
Customer RDS lists are read with a 60-minute role, not an access key

To connect a customer's Amazon RDS or Aurora database from OwlAI SaaS, OwlAI needs the endpoint, port and engine, plus network facts such as whether the database sits in a public subnet and which VPC and security groups it belongs to. Aurora has separate writer and reader endpoints, and copying them by hand can mix the two up. Reading these values straight from AWS means OwlAI has to get into the customer's account. The usual way is an access key for a read-only IAM user, and that key stays valid until the customer deletes it. Once the list has been read, a credential with nothing left to do stays on OwlAI's side.
So the customer creates a CloudFormation stack in their own account. The stack creates a read-only role that expires in 60 minutes and tells OwlAI about it; OwlAI assumes the role once (AssumeRole), reads the database list and network facts, and never uses it again. For a database in a private subnet, the same approach creates one more stack, a bastion, and the stack also tells OwlAI the bastion's host key fingerprint. This post covers how OwlAI checks a link the customer can edit and notifications whose sender it doesn't know. The principal the customer's role trusts lives in OwlAI's AWS account, so this feature exists only in OwlAI SaaS.
Access keys and hand-built roles both outlive the job
Create a read-only IAM user, take its access key, and the implementation is a few lines. But the key lives until the customer deletes it, so if storage on OwlAI's side leaks, a key that can read the customer's account leaks with it.
A cross-account role means no secret changes hands. The customer follows a guide, creates a role that trusts OwlAI, and sends us its ARN. The trouble is that a condition is easy to leave out of a hand-written trust policy. Without the external ID condition, nothing stops the confused deputy problem (another customer getting OwlAI to assume this role); without an expiry condition, the role stays until the customer deletes it.
A stack the customer creates makes a role with an expiry built in
When an administrator clicks Connect with Amazon RDS, a CloudFormation Quick Create link with the template and parameters already filled in opens in a new tab. The person on the customer's side checks the IAM resource acknowledgment and clicks Create stack. The discovery stack creates three resources.
- A start notification. It is a Custom Resource, and every other resource waits for it.
- The discovery role. Its only trusted principal is OwlAI's service role, under two conditions: a matching
sts:ExternalIdandaws:CurrentTime<ExpiresAt. Its permissions are five actions:rds:DescribeDBInstances,rds:DescribeDBClusters,ec2:DescribeRegions,ec2:DescribeSubnetsandec2:DescribeRouteTables. - A registration notification that sends the new role's ARN to OwlAI.
The ServiceToken of both notifications is an OwlAI SNS topic. CloudFormation publishes the request document to the topic, it moves on to a queue on OwlAI's side, and a worker picks it up. Meanwhile the customer's stack waits for OwlAI's response, up to a ServiceTimeout of 300 seconds. If OwlAI answers FAILED, the stack rolls back and the role is deleted with it.
The customer can edit the link, so every notification is checked again
A Quick Create link's parameters show up in the customer's console as they are, and the customer can edit them. So the link carries no secrets. ExternalId isn't a secret either; it marks that this role came from this customer's request. The request token is 32 random bytes. OwlAI keeps only its hash for comparison and consumes it exactly once when the registration notification arrives. The token's 60-minute lifetime ends at the same instant as the trust policy's ExpiresAt.
OwlAI does not collect customer AWS account IDs in advance. The topic is open to CloudFormation from any account, which makes every notification input from an unknown sender. The worker accepts a notification only after confirming all of the following.
- The request token matches, has not expired and has not been used by another stack.
- The account in the role ARN, the stack's account and the caller account confirmed after assuming the role are all the same.
- The role cannot be assumed without
ExternalId.
If any of these fails, OwlAI answers FAILED and does not use the role. The start notification checks the token and expiry first, so a stale or edited link fails before the role is created.
SQS can deliver the same message twice, so a request that already has an answer gets no second one. If a second pass sent SUCCESS to a stack we had answered FAILED for a broad role, the role that should have rolled back would stay in the customer's account.
The role is used for one read of the list
Once the checks pass, the worker answers SUCCESS first, so the customer's stack can finish, and then reads the list. The role session is 900 seconds, the minimum STS allows, so everything later steps need is read now.
- Regions: every region enabled in the account (
DescribeRegions), eight at a time. A region blocked by an SCP is recorded as failed and the rest continue. - Public check: connecting over the public address is suggested only when
PubliclyAccessibleis true and the database's subnet has a0.0.0.0/0→ internet gateway route; otherwise an SSH tunnel is suggested. - Aurora: a cluster shows as one row, and whether it is public is decided from its member instances. Queries are read-only, so the reader endpoint is the default.
- Engines: only PostgreSQL, MySQL, MariaDB and their Aurora variants can be selected. Oracle and SQL Server appear in the list but cannot be selected.
After this, OwlAI does not call AWS again. Prefilling the registration form, the bastion link's parameters and the registration check all work from the stored list. The database password is not read from AWS. The discovery role has no permission for it, so an administrator enters it. The discovery stack has no further use, so the screen links to it in the console and the customer deletes it.
At registration, the stored list checks the request. If the requested engine, host and port don't match that database's endpoint in the list, the request is rejected with AWS_RDS_TARGET_MISMATCH. TLS is always on, even through a bastion, and the server certificate is verified against the RDS root CAs for that region. The AWS global bundle (174 KB) exceeds the 64 KB cap on a connection's CA, so only that region's three roots (RSA2048, RSA4096 and ECC384, about 5 KB) go in. How an imported connection's address is locked and changed through Import again is covered in “OwlDB connections hold a database ID instead of credentials”; how the certificate name is checked over a tunnel is covered in “Certificate errors are fixed with the right CA and the right name, not by turning off verification”.
A private database's bastion comes from a customer stack, which reports its host key
Picking a database leads to three connection paths. The public address path opens the RDS security group to OwlAI's egress IPs on the database port; the existing bastion path puts one authorized_keys line on the customer's bastion, as in “The bastion gets one key line that only forwards to the database, not an account”. A private database without a bastion gets a new one.
The bastion stack's Quick Create link is filled with the VPC, public subnet, database security group and database port read during discovery, plus OwlAI's egress IPs (one to three IPv4 addresses), an authorized_keys line that forwards only to that one database, and a request token. The network facts are already stored, so the link can be built even after the discovery role has expired. Without an egress IP or a public subnet, no link is issued, and before the link is issued the screen shows the instance type and an estimated monthly cost. Once the customer approves, the stack creates four things.
- A t4g instance. sshd has password login and root login turned off.
- An Elastic IP, so the address survives restarts.
- An SSH security group that accepts port 22 only from OwlAI's egress IPs.
- One database security group rule allowing the bastion on the database port. The customer can turn it off and open the port themselves.
Trust the host key from the first connection, and a server sitting in between could receive the database account. For a bastion the customer runs, an administrator compares fingerprints (“The bastion gets one key line that only forwards to the database, not an account”), but nobody knows the fingerprint of a bastion that was just created. So the bastion reports it itself. At the end of its UserData, the booted bastion computes its ed25519 host key fingerprint and uploads it to a WaitConditionHandle. Once the WaitCondition, which waits up to 15 minutes, receives it, the registration notification carries the fingerprint along with the public IP, instance and security group. The fingerprint travels through CloudFormation inside the customer's account, so OwlAI knows it before connecting to the bastion over the network.
Like a discovery notification, a bastion notification has its token checked, along with whether the stack's account and region match the requested database. The worker also checks the fingerprint's format and that the public IP is an internet-reachable IPv4 address, then pins the fingerprint. A failed or cancelled stack sends OwlAI no notification, so a bastion request is treated as over after 20 minutes. Until then, no second bastion is created for the same database; the screen resumes the request in progress.
On every boot, the bastion rewrites its authorized_keys line from the stack parameters. To open another database or replace the SSH key, the customer edits AuthorizedKeysLine (plus AuthorizedKeysLine2 during the key change) and updates the stack; the instance only restarts, so its address and host key stay the same.
The bastion lives apart from the connection, so changing the connection's password, database or account leaves it as it is. OwlAI's record of the bastion goes with the last connection that used it, and the customer deletes the stack. The cost is billed to the customer's account until the stack is deleted.
Design choices
We had the customer create a stack instead of handing over an access key. The customer clicks one more checkbox and button in the console, and that person needs permission to create IAM roles. In return, no long-lived AWS credential stays on OwlAI's side, and after 60 minutes the trust policy blocks the role.
We check every notification instead of registering customer account IDs in advance. That removes a registration step, but notifications become input from an unknown sender, and code that checks the token, accounts and external ID takes its place. That code has to handle when each check runs and duplicate delivery, so it is more complex than collecting account IDs up front.
We read the list once instead of holding on to the role. The list is a copy of that moment, so when a database address changes, Import again means creating one more stack.
We left the bastion as a resource in the customer's account instead of managing it ourselves. Cleanup and cost fall to the customer, but OwlAI never holds permission to create or delete instances in a customer's account.
If your SaaS only needs to read metadata from a customer's cloud account once, the same setup carries over. Put the expiry in the trust policy of a role that can only read, and treat every notification from the customer's side as input whose account and token need checking.
What problem does this solve?
More in Database connections and OwlDB

Engineering Notes · Database connections and OwlDB
The bastion gets one key line that only forwards to the database, not an account
When a production database sits in a private network behind a bastion host (an SSH server that leads into that…

Engineering Notes · Database connections and OwlDB
Certificate errors are fixed with the right CA and the right name, not by turning off verification
When OwlAI runs on a corporate network and connects over TLS to a cloud LLM or a customer database, certificate errors come from two places.

Engineering Notes · Database connections and OwlDB
Supabase connection addresses are filled by OAuth approval, not copied by hand
The Supabase dashboard lists three addresses for the same database: the project's direct address, port 6543 on…
