A developer's laptop reaches the private RDS instance through the staff VPN, and the application on EC2 cannot connect at all. That is not a broken deployment. The laptop has a tunnel that the EC2 instance never inherits, and "private" in RDS means the database has no public endpoint, not that human VPN users are the only permitted callers. The application needs its own authorized path, its own security-group relationship and its own database identity, and the order in which you choose those three saves most of the debugging. Decide the route first, then the permissions, then verify from the deployed environment rather than from your desk.

The laptop VPN is not the application's path

Whatever the staff VPN does for people, it does nothing for an instance unless that instance is explicitly part of the same network path. An EC2 instance in a private subnet reaches RDS through VPC routing, security groups and network ACLs. It does not inherit a human's tunnel, a human's session or a human's credentials, and it should not: unattended application connectivity and staff remote access need to be independently operable.

That independence is a feature. When the VPN is down, the application keeps working. When the application misbehaves, nobody loses their remote access. Designs that collapse the two into one path tend to be discovered during an incident, at the worst possible moment.

Decision 1: same VPC, direct path

When the application and the database are in the same VPC with direct routing and no inspection middlebox between them, the path is simple and the security-group reference is the strongest tool available. Give the application its own group, then allow the database port from that group rather than from an address range:

aws ec2 authorize-security-group-ingress --group-id sg-0db123   --protocol tcp --port 5432 --source-group sg-0app456

aws rds describe-db-instances --db-instance-identifier prod-db   --query 'DBInstances[0].{Endpoint:Endpoint.Address,Port:Endpoint.Port,Public:PubliclyAccessible,Groups:VpcSecurityGroups}'

The port is illustrative and has to match the engine. A group reference authorizes traffic from the application's instances; it does not copy their rules, and it does not create a database login. Two separate problems, two separate fixes.

Then check the rest of the path rather than stopping at the rule you just added. The application's outbound policy, the database's complete set of attached groups, the subnet route tables and any network ACLs all have to agree. Security-group rules are stateful, while a custom network ACL must permit the request and the return traffic in separate rules, ephemeral ports included. A narrow new database rule also does not cancel an older broad allowance that was already attached.

Decision 2: different VPC or another account

If the application is in another VPC, the network owners choose a supported private interconnect: peering, a transit design, or a shared VPC arrangement. Then both directions of routing have to exist, name resolution has to work for the RDS endpoint, and the source the database policy evaluates has to match what actually arrives. Matching account names or a shared billing relationship supplies none of those.

Security-group referencing is the part that varies. It works in several private interconnect designs, but it stops working the moment a middlebox rewrites the source or terminates the connection, because the database then sees the middlebox rather than your application group. The RDS VPC scenarios separate same-VPC access from the other arrangements, and it is worth checking your specific topology against them before assuming a rule style carries over.

Do not solve a missing route by making the database publicly accessible. That is a different exposure decision with a different owner, and it is usually proposed by whoever is furthest from the consequence.

Decision 3: what identity the database accepts

Network access and database authentication are separate layers, and conflating them produces the most misleading errors. The instance role is an AWS identity for calling AWS APIs. It is not a database user. IAM database authentication is a supported feature where the engine and configuration allow it, and it is still a mapping to a database user rather than a replacement for one.

Use the RDS endpoint name in the application's configuration instead of an address captured from one lookup. Multi-AZ failover changes the address behind that name, and an application pinned to an IP will fail at exactly the moment it is needed. Keep the engine's TLS verification enabled rather than disabling it to make an error disappear, because a verification failure is telling you something about the endpoint you reached.

Then grant the database account the minimum the application needs, in the schema it actually uses. That work is boring, it is auditable, and it is the part an incident review will ask about.

Verify from the deployed environment

Test from the instance or from a task running with the same group, subnet and role. Four failures look similar from a laptop and are trivially distinguishable from there: a name that does not resolve, a connection that times out, a TLS validation failure, and a rejected database login. Each one points at a different boundary, and changing the database's exposure before knowing which one you have is guesswork.

psql "host=prod-db.abc123.eu-west-1.rds.amazonaws.com port=5432 dbname=app sslmode=verify-full" -c 'select 1'

If the connection succeeds from the instance and fails from your desk, the application path is fine and your access is the thing to fix. If it fails from both, start with the group and route checks above rather than with the credentials. That single comparison prevents most of the churn these tickets generate.

Sɛbɛn minnu bɛ tali kɛ