|
| 1 | +# Networking for private-subnet RDS with `sp_invoke_external_rest_endpoint` |
| 2 | + |
| 3 | +When your Amazon RDS for SQL Server 2025 instance is in a **private subnet** |
| 4 | +(no public IP, `PubliclyAccessible = false`), `sp_invoke_external_rest_endpoint` |
| 5 | +requires outbound internet access to reach the regional API Gateway endpoint. |
| 6 | +This page covers the required networking setup and documents a known limitation |
| 7 | +with private API Gateway endpoints. |
| 8 | + |
| 9 | +## What you need (NAT gateway) |
| 10 | + |
| 11 | +`sp_invoke_external_rest_endpoint` resolves the API hostname via **public DNS** |
| 12 | +and makes an outbound HTTPS call over the internet. A private-subnet RDS instance |
| 13 | +has no internet path by default, so you must provide one: |
| 14 | + |
| 15 | +1. **Create a public subnet** in the same VPC with a route to an internet gateway. |
| 16 | +2. **Create a NAT gateway** in that public subnet (requires an Elastic IP). |
| 17 | +3. **Add a default route** (`0.0.0.0/0 → NAT gateway`) to the route table used |
| 18 | + by the RDS subnets. |
| 19 | + |
| 20 | +After this, outbound traffic from the RDS subnets exits via the NAT gateway's |
| 21 | +Elastic IP. RDS stays private (no inbound path from the internet). |
| 22 | + |
| 23 | +### Verify the egress IP |
| 24 | + |
| 25 | +From SQL Server, confirm the NAT path works and note the egress IP: |
| 26 | + |
| 27 | +```sql |
| 28 | +DECLARE @ret INT, @response NVARCHAR(MAX); |
| 29 | +EXEC @ret = sp_invoke_external_rest_endpoint |
| 30 | + @url = N'https://api.ipify.org?format=json', |
| 31 | + @method = 'GET', |
| 32 | + @timeout = 30, |
| 33 | + @response = @response OUTPUT; |
| 34 | +SELECT @ret AS ReturnCode, @response AS Response; |
| 35 | +-- The "ip" field in the result is the public IP API Gateway sees. |
| 36 | +``` |
| 37 | + |
| 38 | +The returned IP is what you allowlist in the API Gateway resource policy (see |
| 39 | +[`docs/api-resource-policy.md`](api-resource-policy.md)). |
| 40 | + |
| 41 | +### Without the NAT gateway |
| 42 | + |
| 43 | +If there is no outbound route, `sp_invoke_external_rest_endpoint` fails with: |
| 44 | + |
| 45 | +``` |
| 46 | +Msg 31608, Level 16, State 24 ... |
| 47 | +An error occurred, failed to communicate with the external rest endpoint. |
| 48 | +HRESULT: 0x80072ee7. |
| 49 | +``` |
| 50 | + |
| 51 | +`0x80072ee7` is WinHTTP `ERROR_WINHTTP_NAME_NOT_RESOLVED` — DNS resolution |
| 52 | +failed because the engine had no path to a public resolver or endpoint. |
| 53 | + |
| 54 | +## Why a private API Gateway endpoint does NOT work |
| 55 | + |
| 56 | +You might expect to use a **private** API Gateway endpoint (endpoint type |
| 57 | +`PRIVATE`) with an interface VPC endpoint (`com.amazonaws.<region>.execute-api`) |
| 58 | +to keep all traffic inside the VPC. This **does not work** with |
| 59 | +`sp_invoke_external_rest_endpoint` on Amazon RDS. |
| 60 | + |
| 61 | +### What was tested |
| 62 | + |
| 63 | +| Test | Result | |
| 64 | +| --- | --- | |
| 65 | +| Private API + VPCe private DNS enabled, standard hostname from RDS | `0x80072ee7` NAME_NOT_RESOLVED | |
| 66 | +| Private API + VPCe hostname with `Host` / `x-apigw-api-id` header from RDS | HTTP 403 ForbiddenException (`aws:sourceVpce` not populated) | |
| 67 | +| Same private API + VPCe, called from Linux EC2 in the same subnet | HTTP 200 (success) | |
| 68 | +| Regional API via NAT from RDS | HTTP 200 (success) | |
| 69 | +| Public endpoint (`api.ipify.org`) via NAT from RDS | HTTP 200 (success) | |
| 70 | + |
| 71 | +### Root cause |
| 72 | + |
| 73 | +The managed RDS engine's `sp_invoke_external_rest_endpoint` implementation: |
| 74 | + |
| 75 | +1. **Does not resolve the API hostname to the VPC endpoint's private ENI IPs**, |
| 76 | + even when VPCe private DNS is enabled and functioning for other workloads |
| 77 | + (EC2) in the same subnet. |
| 78 | +2. **Does not populate `aws:sourceVpce`** in the request context when it does |
| 79 | + reach API Gateway (e.g., via the VPCe-specific hostname), so a private API's |
| 80 | + resource policy condition `StringNotEquals: aws:sourceVpce` always denies. |
| 81 | + |
| 82 | +An EC2 instance in the same VPC and subnet, using the same security group, |
| 83 | +successfully resolves to the VPCe and receives HTTP 200 — proving the VPCe |
| 84 | +setup is correct and the limitation is specific to the managed RDS engine. |
| 85 | + |
| 86 | +### Recommendation |
| 87 | + |
| 88 | +Use a **regional** (public) API Gateway endpoint and restrict access with: |
| 89 | + |
| 90 | +- A **resource policy** that allows only the NAT gateway's Elastic IP |
| 91 | + (`aws:SourceIp`). See [`docs/api-resource-policy.md`](api-resource-policy.md). |
| 92 | +- The **API key** (already required by the method and stored in the |
| 93 | + `DATABASE SCOPED CREDENTIAL`). |
| 94 | + |
| 95 | +This gives you IP-level network restriction plus application-level |
| 96 | +authentication, while avoiding the VPCe path that RDS cannot use. |
| 97 | + |
| 98 | +## DNS caching note |
| 99 | + |
| 100 | +If the API hostname was previously associated with a private hosted zone (via |
| 101 | +VPCe private DNS), the RDS engine may cache stale/negative DNS answers for up to |
| 102 | +~15 minutes after the override is removed. During this window, |
| 103 | +`sp_invoke_external_rest_endpoint` returns `0x80072ee7`. The fix is time — let |
| 104 | +the TTL expire. Rebooting the RDS instance can also clear the engine's DNS cache, |
| 105 | +but only after the VPC resolver itself has propagated the correct (public) |
| 106 | +answer. |
0 commit comments