Skip to content

Commit f8dcc53

Browse files
init
1 parent 48d6992 commit f8dcc53

2 files changed

Lines changed: 253 additions & 0 deletions

File tree

‎cmd/rds-iam-psql/README.md‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# rds-iam-psql
2+
3+
A simple CLI tool that bridges AWS RDS IAM authentication into an interactive `psql` session. It generates a short-lived IAM auth token and launches `psql` with the token as the password, so you never have to manage database passwords.
4+
5+
## Why?
6+
7+
RDS IAM authentication lets you connect to PostgreSQL using your AWS credentials instead of a static database password. However, the auth tokens are temporary (15 minutes) and cumbersome to generate manually. This tool handles token generation automatically and drops you into a familiar `psql` shell.
8+
9+
## Installation
10+
11+
```bash
12+
go install github.com/corbaltcode/go-libraries/cmd/rds-iam-psql@latest
13+
```
14+
15+
Or build from source:
16+
17+
```bash
18+
cd ./cmd/rds-iam-psql
19+
go build
20+
```
21+
22+
## Prerequisites
23+
24+
- **psql** installed and available in your PATH
25+
- **AWS credentials** configured (via environment variables, `~/.aws/credentials`, IAM role, etc.)
26+
- **RDS IAM authentication enabled** on your database instance
27+
- A database user configured for IAM authentication (created with `CREATE USER myuser WITH LOGIN; GRANT rds_iam TO myuser;`)
28+
29+
## Usage
30+
31+
```bash
32+
rds-iam-psql -host <rds-endpoint> -user <db-user> -db <database-name> [options]
33+
```
34+
35+
### Required Flags
36+
37+
| Flag | Description |
38+
|------|-------------|
39+
| `-host` | RDS endpoint hostname (without port), e.g. `mydb.abc123.us-east-1.rds.amazonaws.com` |
40+
| `-user` | Database username configured for IAM auth |
41+
| `-db` | Database name to connect to |
42+
43+
### Optional Flags
44+
45+
| Flag | Default | Description |
46+
|------|---------|-------------|
47+
| `-port` | `5432` | PostgreSQL port |
48+
| `-region` | auto | AWS region. If omitted, inferred from AWS config or the hostname |
49+
| `-profile` | | AWS shared config profile to use (e.g. `dev`, `prod`) |
50+
| `-psql` | `psql` | Path to the `psql` binary |
51+
| `-sslmode` | `require` | SSL mode (`require`, `verify-full`, etc.) |
52+
| `-search-path` | | PostgreSQL `search_path` to set on connection (e.g. `myschema,public`) |
53+
54+
## Examples
55+
56+
Basic connection:
57+
58+
```bash
59+
rds-iam-psql -host mydb.abc123.us-east-1.rds.amazonaws.com -user app_user -db myapp
60+
```
61+
62+
With a specific AWS profile and schema:
63+
64+
```bash
65+
rds-iam-psql \
66+
-host mydb.abc123.us-east-1.rds.amazonaws.com \
67+
-user app_user \
68+
-db myapp \
69+
-profile production \
70+
-search-path "app_schema,public"
71+
```
72+
73+
Using a non-standard port and explicit region:
74+
75+
```bash
76+
rds-iam-psql \
77+
-host mydb.abc123.us-east-1.rds.amazonaws.com \
78+
-port 5433 \
79+
-user admin \
80+
-db postgres \
81+
-region us-east-1
82+
```
83+
84+
## How It Works
85+
86+
1. Loads your AWS credentials from the standard credential chain
87+
2. Generates a temporary RDS IAM auth token using `auth.BuildAuthToken`
88+
3. Launches `psql` with:
89+
- `PGPASSWORD` set to the auth token
90+
- `PGSSLMODE` set according to `-sslmode`
91+
- `PGOPTIONS` set if `-search-path` is provided
92+
4. Attaches stdin/stdout/stderr for interactive use
93+
94+
## Setting Up IAM Auth on RDS
95+
96+
1. Enable IAM authentication on your RDS instance
97+
2. Create a database user and grant IAM privileges:
98+
```sql
99+
CREATE USER myuser WITH LOGIN;
100+
GRANT rds_iam TO myuser;
101+
```
102+
3. Attach an IAM policy allowing `rds-db:connect` to your AWS user/role:
103+
```json
104+
{
105+
"Version": "2012-10-17",
106+
"Statement": [
107+
{
108+
"Effect": "Allow",
109+
"Action": "rds-db:connect",
110+
"Resource": "arn:aws:rds-db:<region>:<account-id>:dbuser:<dbi-resource-id>/<db-user>"
111+
}
112+
]
113+
}
114+
```

‎cmd/rds-iam-psql/main.go‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// rds-iam-psql.go
2+
package main
3+
4+
import (
5+
"context"
6+
"flag"
7+
"fmt"
8+
"log"
9+
"os"
10+
"os/exec"
11+
"strings"
12+
13+
"github.com/aws/aws-sdk-go-v2/aws"
14+
awsconfig "github.com/aws/aws-sdk-go-v2/config"
15+
"github.com/aws/aws-sdk-go-v2/feature/rds/auth"
16+
)
17+
18+
func main() {
19+
var (
20+
host = flag.String("host", "", "RDS PostgreSQL endpoint hostname (no port, e.g. mydb.abc123.us-east-1.rds.amazonaws.com)")
21+
port = flag.Int("port", 5432, "RDS PostgreSQL port (default 5432)")
22+
user = flag.String("user", "", "Database user name")
23+
dbName = flag.String("db", "", "Database name")
24+
region = flag.String("region", "", "AWS region for the RDS instance (e.g. us-east-1). If empty, uses AWS config or tries to infer from host.")
25+
profile = flag.String("profile", "", "Optional AWS shared config profile (e.g. dev)")
26+
psqlPath = flag.String("psql", "psql", "Path to psql binary")
27+
sslMode = flag.String("sslmode", "require", "PGSSLMODE for psql (e.g. require, verify-full)")
28+
searchPath = flag.String("search-path", "", "Optional PostgreSQL search_path to set (e.g. 'myschema,public')")
29+
)
30+
flag.Parse()
31+
32+
if *host == "" || *user == "" || *dbName == "" {
33+
log.Fatalf("host, user, and db are required\n\nUsage example:\n %s -host mydb.abc123.us-east-1.rds.amazonaws.com -port 5432 -user myuser -db mydb -search-path \"login,public\" -region us-east-1\n", os.Args[0])
34+
}
35+
36+
ctx := context.Background()
37+
38+
// Load AWS config (standard RDS/IAM auth expects your AWS creds, *not* the DB password).
39+
var cfg aws.Config
40+
var err error
41+
if *profile != "" {
42+
cfg, err = awsconfig.LoadDefaultConfig(ctx, awsconfig.WithSharedConfigProfile(*profile))
43+
} else {
44+
cfg, err = awsconfig.LoadDefaultConfig(ctx)
45+
}
46+
if err != nil {
47+
log.Fatalf("failed to load AWS config: %v", err)
48+
}
49+
50+
awsRegion := *region
51+
if awsRegion == "" {
52+
awsRegion = cfg.Region
53+
}
54+
if awsRegion == "" {
55+
// Last resort: try to infer from the hostname if it looks like a standard RDS endpoint.
56+
if inferred := inferRegionFromHost(*host); inferred != "" {
57+
awsRegion = inferred
58+
}
59+
}
60+
61+
if awsRegion == "" {
62+
log.Fatalf("AWS region is not set; pass -region or set AWS_REGION / configure your AWS profile")
63+
}
64+
65+
endpointWithPort := fmt.Sprintf("%s:%d", *host, *port)
66+
67+
// Generate the IAM auth token.
68+
authToken, err := auth.BuildAuthToken(ctx, endpointWithPort, awsRegion, *user, cfg.Credentials)
69+
if err != nil {
70+
log.Fatalf("failed to build RDS IAM auth token: %v", err)
71+
}
72+
73+
// Prepare psql command. We pass the token through PGPASSWORD and SSL mode via PGSSLMODE.
74+
cmd := exec.Command(
75+
*psqlPath,
76+
"--host", *host,
77+
"--port", fmt.Sprintf("%d", *port),
78+
"--username", *user,
79+
"--dbname", *dbName,
80+
)
81+
82+
// Attach stdio so it behaves like an interactive shell.
83+
cmd.Stdin = os.Stdin
84+
cmd.Stdout = os.Stdout
85+
cmd.Stderr = os.Stderr
86+
87+
// Inherit existing env and add PG vars.
88+
env := os.Environ()
89+
env = append(env,
90+
"PGPASSWORD="+authToken,
91+
"PGSSLMODE="+*sslMode,
92+
)
93+
94+
// If a search path is provided, wire it through PGOPTIONS.
95+
if sp := strings.TrimSpace(*searchPath); sp != "" {
96+
// Build our addition: one -c flag.
97+
add := "-c search_path=" + sp
98+
99+
// Check if PGOPTIONS already exists; if so, append.
100+
found := false
101+
for i, e := range env {
102+
if strings.HasPrefix(e, "PGOPTIONS=") {
103+
current := strings.TrimPrefix(e, "PGOPTIONS=")
104+
if strings.TrimSpace(current) == "" {
105+
env[i] = "PGOPTIONS=" + add
106+
} else {
107+
env[i] = "PGOPTIONS=" + current + " " + add
108+
}
109+
found = true
110+
break
111+
}
112+
}
113+
if !found {
114+
env = append(env, "PGOPTIONS="+add)
115+
}
116+
}
117+
118+
cmd.Env = env
119+
120+
if err := cmd.Run(); err != nil {
121+
// psql will print its own error messages; just propagate the exit code.
122+
if exitErr, ok := err.(*exec.ExitError); ok {
123+
os.Exit(exitErr.ExitCode())
124+
}
125+
log.Fatalf("failed to run psql: %v", err)
126+
}
127+
}
128+
129+
// inferRegionFromHost tries to pull the AWS region out of a typical RDS hostname like
130+
// "mydb.abc123.us-east-1.rds.amazonaws.com". If it can't, it returns "".
131+
func inferRegionFromHost(host string) string {
132+
parts := strings.Split(host, ".")
133+
for i := 0; i < len(parts); i++ {
134+
if parts[i] == "rds" && i > 0 {
135+
return parts[i-1]
136+
}
137+
}
138+
return ""
139+
}

0 commit comments

Comments
 (0)