AWS RDS / Aurora MySQL Community
Status: tested against
querycopHEAD on 2026-05-18. Endpoint conventions cited as of 2026-05.
This page covers running Querycop in front of:
- AWS RDS for MySQL (single-instance)
- AWS Aurora MySQL (writer + reader endpoints)
Both share the same global CA bundle, the same IAM-token tooling, and
the same verify-full recipe — only the endpoint shape and (for
Aurora) the writer / reader topology differ.
⚠️ MySQL protocol coverage in Querycop is partial. Querycop today only fully understands MySQL’s text protocol (
COM_QUERY). Binary protocol / server-side prepared statements (COM_STMT_PREPARE,COM_STMT_EXECUTE, etc.) and packets that change connection state mid-stream are not parsed for risk-scoring — they pass through but are not subject to the same policy enforcement as text queries. If your app usesmysql2/mysqljswith prepared statements, JDBC withuseServerPrepStmts=true, or PHP’smysqliprepared bindings, audit what coverage gap that creates before relying on Querycop for guard-rails on those flows. Seedocs/known-limitations.mdfor the full list.
For an Aurora MySQL cluster writer endpoint with IAM auth, the production-ready config is:
| Env var | Value |
|---|---|
GATEKEEPER_BACKEND_HOST | mydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com |
GATEKEEPER_BACKEND_PORT | 3306 |
GATEKEEPER_BACKEND_TLS_MODE | verify-full |
GATEKEEPER_BACKEND_TLS_CA_FILE | /etc/ssl/certs/rds-global-bundle.pem (downloaded from AWS) |
GATEKEEPER_BACKEND_TLS_SERVER_NAME | (unset — derived from BACKEND_HOST) |
GATEKEEPER_BACKEND_TOKEN_CMD | aws rds generate-db-auth-token --hostname "$QUERYCOP_BACKEND_HOST" --port 3306 --region us-east-1 --username "$QUERYCOP_BACKEND_USER" |
For RDS MySQL with password auth (no IAM), drop the
BACKEND_TOKEN_CMD row and supply the password through your client.
Unlike Cloud SQL / AlloyDB, AWS RDS’s cert SAN matches the public DNS
hostname (*.rds.amazonaws.com), so verify-full against the
endpoint’s DNS name does pass hostname verification — no
BACKEND_TLS_SERVER_NAME override required.
Prerequisites
Section titled “Prerequisites”-
A running RDS MySQL instance OR Aurora MySQL cluster.
-
For IAM auth: IAM database authentication enabled on the DB instance (RDS) or the DB cluster (Aurora) — same setting as RDS PostgreSQL, applies dynamically with no reboot required:
Terminal window # RDS for MySQL (single instance)aws rds modify-db-instance \--db-instance-identifier mydb \--enable-iam-database-authentication \--apply-immediately# Aurora MySQL (cluster)aws rds modify-db-cluster \--db-cluster-identifier mydb \--enable-iam-database-authentication \--apply-immediately -
A MySQL user created for IAM auth. The MySQL-side incantation differs from PostgreSQL:
CREATE USER 'iam_user'@'%' IDENTIFIED WITH AWSAuthenticationPlugin AS 'RDS';GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'iam_user'@'%';The
AWSAuthenticationPlugin … AS 'RDS'is what tells the server to validate the auth payload as an IAM token rather than a password hash. -
An AWS IAM principal that holds
rds-db:connectfor the resource ARNarn:aws:rds-db:<region>:<account>:dbuser:<resource-id>/iam_user. -
Network reachability from the Querycop host to port 3306 on the RDS endpoint.
-
awsCLI installed on the Querycop host with credentials available (env vars / instance profile / IRSA / Workload Identity). -
Querycop with backend IAM token injection support (
GATEKEEPER_BACKEND_TOKEN_CMD).
Pattern A — Password auth (evaluation / early production)
Section titled “Pattern A — Password auth (evaluation / early production)”Querycop forwards the client’s password unchanged.
Step 1: Download the AWS root CA bundle
Section titled “Step 1: Download the AWS root CA bundle”The same global bundle that RDS PostgreSQL uses — it chains every RDS engine’s root CA across all regions:
curl -fsSL https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem \ -o /etc/ssl/certs/rds-global-bundle.pem
grep -c BEGIN /etc/ssl/certs/rds-global-bundle.pem # should be ≥ 1AWS rotates these roots on a published schedule;
when they do, re-download and restart Querycop. No in-process
hot-reload of BACKEND_TLS_CA_FILE.
Step 2: Configure Querycop
Section titled “Step 2: Configure Querycop”# Required: where to find the backendexport GATEKEEPER_BACKEND_HOST=mydb.cxxxxxxxxxxxxxxxx.us-east-1.rds.amazonaws.comexport GATEKEEPER_BACKEND_PORT=3306
# Required: full TLS verification against the AWS-published CAexport GATEKEEPER_BACKEND_TLS_MODE=verify-fullexport GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/rds-global-bundle.pem# SERVER_NAME not set — Querycop derives it from BACKEND_HOST,# which matches the SAN AWS issues for the endpoint.
# Standard Querycop runtimeexport GATEKEEPER_LISTEN_PORT=15336 # convention: 1xxxx for MySQL tooexport GATEKEEPER_API_PORT=8080export ADMIN_API_KEY=$(openssl rand -hex 16)
querycopStep 3: Connect the client
Section titled “Step 3: Connect the client”mysql -h 127.0.0.1 -P 15336 -u appuser -p appdb# Password prompt → enter the password set in RDSverify-full is the right default. require (skip verification)
is evaluation-only — use it when spinning up against a brand-new
instance and you haven’t yet downloaded the bundle, then switch back.
disable is not appropriate for managed cloud DB: it strips the
backend TLS leg entirely.
Pattern B — IAM auth (recommended for production)
Section titled “Pattern B — IAM auth (recommended for production)”IAM auth eliminates static database passwords. Querycop mints a
15-minute token per connection via aws rds generate-db-auth-token
and rewrites the client’s mysql_clear_password handshake response
to inject that token — Querycop watches for the auth-plugin negotiation
that ends in mysql_clear_password (the server side of an
AWSAuthenticationPlugin user requests it), and substitutes the
minted token in place of whatever the client sent in the cleartext
password packet.
Because the client must send its password via the cleartext auth
plugin for this rewrite to work, the client invocation needs the
--enable-cleartext-plugin flag (the MySQL CLI refuses to send
cleartext passwords without it, even over TLS). This is true even
though the wire connection between Querycop and the client is what
ultimately carries the (placeholder) password — the client’s own
plugin-selection logic gates the packet before TLS comes into play.
Step 1: Confirm the AWS-side IAM auth wiring
Section titled “Step 1: Confirm the AWS-side IAM auth wiring”# 1. IAM auth enabled on the instance / clusteraws rds describe-db-instances --db-instance-identifier mydb \ --query 'DBInstances[0].IAMDatabaseAuthenticationEnabled'# Or for Aurora:aws rds describe-db-clusters --db-cluster-identifier mydb \ --query 'DBClusters[0].IAMDatabaseAuthenticationEnabled'# Expected: true
# 2. The MySQL user exists with AWSAuthenticationPluginmysql -h <direct connection> -u <admin> -p -e \ "SELECT user, host, plugin FROM mysql.user WHERE user='iam_user'"# Expected: plugin = AWSAuthenticationPlugin
# 3. The IAM principal Querycop runs as can mint a tokenaws rds generate-db-auth-token \ --hostname mydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com \ --port 3306 --region us-east-1 --username iam_user \ | head -c 80# Expected: an opaque pre-signed-URL-like string (~800 chars)Step 2: Configure Querycop
Section titled “Step 2: Configure Querycop”export GATEKEEPER_BACKEND_HOST=mydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.comexport GATEKEEPER_BACKEND_PORT=3306
export GATEKEEPER_BACKEND_TLS_MODE=verify-fullexport GATEKEEPER_BACKEND_TLS_CA_FILE=/etc/ssl/certs/rds-global-bundle.pem
# IAM token mint per connection. Note --port 3306 for MySQL.export GATEKEEPER_BACKEND_TOKEN_CMD='aws rds generate-db-auth-token \ --hostname "$QUERYCOP_BACKEND_HOST" \ --port 3306 \ --region us-east-1 \ --username "$QUERYCOP_BACKEND_USER"'
export GATEKEEPER_LISTEN_PORT=15336export GATEKEEPER_API_PORT=8080export ADMIN_API_KEY=$(openssl rand -hex 16)
querycopQuerycop enforces at startup that BACKEND_TOKEN_CMD ships with a
TLS mode of require / verify-ca / verify-full — prefer is
rejected because its plaintext-fallback path would leak the token.
See docs/configuration.md §1.6.
Step 3: Connect the client
Section titled “Step 3: Connect the client”The client points at Querycop with the IAM username; the password field is irrelevant (Querycop discards and replaces with the minted IAM token).
mysql --enable-cleartext-plugin \ -h 127.0.0.1 -P 15336 \ -u iam_user -p appdb# Password prompt: just press Enter (Querycop replaces with the IAM token)For tools that won’t accept an empty password:
MYSQL_PWD=ignored mysql --enable-cleartext-plugin \ -h 127.0.0.1 -P 15336 -u iam_user appdbIf you forget --enable-cleartext-plugin, the MySQL CLI logs
ERROR 2059 (HY000): Authentication plugin 'mysql_clear_password' cannot be loaded and exits before any packet reaches Querycop.
Other MySQL clients (JDBC, Python’s mysql.connector, Go’s
go-sql-driver/mysql) expose the same setting under different
names — see
AWS RDS IAM auth client docs
for per-driver flags.
Aurora-specific endpoint considerations
Section titled “Aurora-specific endpoint considerations”Identical to the Aurora PostgreSQL story. Quick recap:
| Endpoint | Hostname shape | Use for |
|---|---|---|
| Writer (cluster endpoint) | mydb.cluster-xxxxxxxx.<region>.rds.amazonaws.com | All writes + reads from primary |
| Reader (cluster reader endpoint) | mydb.cluster-ro-xxxxxxxx.<region>.rds.amazonaws.com | Reads from any healthy reader, automatic load-balance |
| Instance | mydb-instance-1.xxxxxxxx.<region>.rds.amazonaws.com | Pinning to a specific instance |
Querycop today runs one process against one endpoint; for
read/write splitting in Aurora MySQL, run two Querycop processes
(one per endpoint) and let the app’s connection pools route
accordingly. GATEKEEPER_BACKEND_ROLE is an advisory env that
gets logged.
Smoke test
Section titled “Smoke test”# Terminal 1: bring up Querycop with the env above.docker compose up -d # or `querycop` directly
# Terminal 2: connect via mysql.MYSQL_PWD=ignored mysql --enable-cleartext-plugin \ -h 127.0.0.1 -P 15336 -u iam_user appdb -e 'select 1'# +---+# | 1 |# +---+# | 1 |# +---+A green select 1 means proxy-side handshake, the IAM mint, the
mysql_clear_password handshake-response rewrite, and the backend
TLS all worked. If you see something else:
ERROR 2026 (HY000): SSL connection error→ the global-bundle CA file is missing or stale. Re-curl fromtruststore.pki.rds.amazonaws.com/global/global-bundle.pem.ERROR 2003 (HY000): Can't connect to MySQL server→ network reachability problem; check that 3306 is open from the Querycop host to the RDS endpoint.ERROR 1045 (28000): Access denied for user 'iam_user'@'…'→ either the IAM principal lacksrds-db:connect, the IAM auth wasn’t enabled on the instance/cluster, or the MySQL user wasn’t created withAWSAuthenticationPlugin AS 'RDS'.
Gotchas
Section titled “Gotchas”IAM token lifetime is 15 minutes
Section titled “IAM token lifetime is 15 minutes”Same shape as RDS PostgreSQL: aws rds generate-db-auth-token
returns a token that expires 15 minutes after issuance. Querycop
mints once per connection on the initial handshake. The 15-minute
window applies to the mint → handshake leg only — once the RDS
server accepts the token during auth, it does not re-check during
steady-state traffic, so a successfully-authed connection stays
alive for the TCP socket’s lifetime (hours, days), and does NOT get
torn down at the 15-minute mark.
- New connections after token expiry mint fresh; automatic.
- A connection that drops and reconnects gets a fresh mint; the
client should retry on
Lost connection to MySQL server. - If the IAM principal’s credentials expire (STS session ends, IRSA
rotation), all future mints fail with
backend token command failedin the WARN log.
In-connection token rotation isn’t implemented; operators with multi-hour MySQL connections should expect occasional reconnects.
verify-full against the RDS endpoint Just Works
Section titled “verify-full against the RDS endpoint Just Works”Unlike Cloud SQL / AlloyDB, AWS RDS’s cert SAN matches the public
DNS hostname of the endpoint (*.rds.amazonaws.com). Pointing
BACKEND_HOST at the DNS name (not an IP literal) means
verify-full passes hostname verification with no override needed.
If you point BACKEND_HOST at an IP literal — rare but it happens —
verify-full fail-fasts at startup. Either
use the DNS name (recommended) or drop to verify-ca if hostname
verification needs to be intentionally skipped.
If you’re connecting through a CNAME (e.g. db.internal.example.com
→ RDS endpoint), override BACKEND_TLS_SERVER_NAME to the RDS
endpoint hostname so verification matches the cert SAN:
export GATEKEEPER_BACKEND_HOST=db.internal.example.comexport GATEKEEPER_BACKEND_TLS_SERVER_NAME=mydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.comMySQL protocol coverage is partial
Section titled “MySQL protocol coverage is partial”Repeating the page-top warning because it’s the biggest operational
gotcha specific to MySQL: Querycop’s policy / risk-scoring engine
covers the text protocol (COM_QUERY) end-to-end. Other MySQL
protocol packets pass through but are not parsed for risk-scoring:
| Packet | Querycop coverage |
|---|---|
COM_QUERY (text protocol) | ✅ Full risk-scoring + policy |
COM_STMT_PREPARE / _EXECUTE (server-side prepared statements) | ❌ Pass-through, no risk-scoring |
COM_INIT_DB (database switch) | ✅ Tracked for audit |
COM_CHANGE_USER (re-auth on same TCP) | ⚠️ Pass-through; IAM-token mint not re-triggered |
COM_BINLOG_DUMP / replication packets | ❌ Pass-through |
What this means in practice:
- ORM / driver layers that send everything as
COM_QUERY(e.g. PHPPDO::ATTR_EMULATE_PREPARES=true, RailsActiveRecordwith prepared statements disabled) are fully covered. - Drivers using server-side prepared statements (JDBC default,
mysql2withprepared: true,mysqlclientin Python) bypass risk-scoring on the prepared path. If you rely on Querycop for guard-rails, either disable server-side prepares in your driver or accept the coverage gap. - The full list and rationale is in
docs/known-limitations.md.
This is independent of TLS / IAM auth — it’s about what Querycop’s parser knows about the wire protocol.
Client→proxy TLS vs proxy→DB TLS are SEPARATE legs
Section titled “Client→proxy TLS vs proxy→DB TLS are SEPARATE legs”| Leg | Configured by | Default |
|---|---|---|
| Client → Querycop | GATEKEEPER_PROXY_TLS_CERT / _KEY | OFF — plaintext unless you put TLS material in front |
| Querycop → RDS | GATEKEEPER_BACKEND_TLS_* (this page) | prefer (default for MySQL) — upgrade to verify-full per recipe |
In this cookbook the proxy→DB leg is verify-full. The client→proxy
leg is your call — mysql over plaintext to a localhost proxy is
fine for development; for production put a TLS cert on Querycop
(GATEKEEPER_PROXY_TLS_CERT / _KEY).
The IAM token never leaves Querycop, so even if the client→proxy
leg is plaintext, the token is only ever in flight on the
verify-full-protected backend leg.
AWS CA rotation
Section titled “AWS CA rotation”AWS rotates the RDS root CAs on a published schedule. When they do:
- Re-download
global-bundle.pemto the same path. - Restart Querycop (no in-process hot-reload of
BACKEND_TLS_CA_FILE).
global-bundle.pem includes both the outgoing and incoming roots
during the transition window, so re-downloading well in advance
avoids the cliff edge.
aws CLI must be on PATH where Querycop runs
Section titled “aws CLI must be on PATH where Querycop runs”The BACKEND_TOKEN_CMD shells out via sh -c; the spawned shell
inherits the parent’s PATH. On a
minimal container without aws, the command fails fast with
executable file not found in $PATH.
Either install aws-cli/v2 in the runtime container or pin an
absolute path:
BACKEND_TOKEN_CMD=/usr/local/bin/aws rds generate-db-auth-token ….
IRSA / Workload Identity / instance profile
Section titled “IRSA / Workload Identity / instance profile”The os.Environ() inheritance means AWS_PROFILE /
AWS_DEFAULT_REGION / AWS_ROLE_ARN /
AWS_WEB_IDENTITY_TOKEN_FILE all flow into the child shell —
provided you set them on the parent.
For EKS + IRSA, bind a Kubernetes service account to an IAM role
that holds rds-db:connect, deploy Querycop with that KSA, and
aws rds generate-db-auth-token picks up the federated credentials
automatically.
Cross-links
Section titled “Cross-links”docs/configuration.md§1.5 — backend TLS reference (covers MySQL)docs/configuration.md§1.6 —BACKEND_TOKEN_CMDreferencedocs/known-limitations.md— full MySQL protocol-coverage gap list- AWS docs: RDS IAM authentication
- AWS docs: RDS root CA bundles
- AWS docs: Aurora endpoints