Skip to content

AWS RDS / Aurora MySQL Community

Status: tested against querycop HEAD 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 uses mysql2/mysqljs with prepared statements, JDBC with useServerPrepStmts=true, or PHP’s mysqli prepared bindings, audit what coverage gap that creates before relying on Querycop for guard-rails on those flows. See docs/known-limitations.md for the full list.

For an Aurora MySQL cluster writer endpoint with IAM auth, the production-ready config is:

Env varValue
GATEKEEPER_BACKEND_HOSTmydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com
GATEKEEPER_BACKEND_PORT3306
GATEKEEPER_BACKEND_TLS_MODEverify-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_CMDaws 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.

  • 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:connect for the resource ARN arn:aws:rds-db:<region>:<account>:dbuser:<resource-id>/iam_user.

  • Network reachability from the Querycop host to port 3306 on the RDS endpoint.

  • aws CLI 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.

The same global bundle that RDS PostgreSQL uses — it chains every RDS engine’s root CA across all regions:

Terminal window
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 ≥ 1

AWS 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.

Terminal window
# Required: where to find the backend
export GATEKEEPER_BACKEND_HOST=mydb.cxxxxxxxxxxxxxxxx.us-east-1.rds.amazonaws.com
export GATEKEEPER_BACKEND_PORT=3306
# Required: full TLS verification against the AWS-published CA
export GATEKEEPER_BACKEND_TLS_MODE=verify-full
export 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 runtime
export GATEKEEPER_LISTEN_PORT=15336 # convention: 1xxxx for MySQL too
export GATEKEEPER_API_PORT=8080
export ADMIN_API_KEY=$(openssl rand -hex 16)
querycop
Terminal window
mysql -h 127.0.0.1 -P 15336 -u appuser -p appdb
# Password prompt → enter the password set in RDS

verify-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.

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”
Terminal window
# 1. IAM auth enabled on the instance / cluster
aws 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 AWSAuthenticationPlugin
mysql -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 token
aws 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)
Terminal window
export GATEKEEPER_BACKEND_HOST=mydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com
export GATEKEEPER_BACKEND_PORT=3306
export GATEKEEPER_BACKEND_TLS_MODE=verify-full
export 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=15336
export GATEKEEPER_API_PORT=8080
export ADMIN_API_KEY=$(openssl rand -hex 16)
querycop

Querycop enforces at startup that BACKEND_TOKEN_CMD ships with a TLS mode of require / verify-ca / verify-fullprefer is rejected because its plaintext-fallback path would leak the token. See docs/configuration.md §1.6.

The client points at Querycop with the IAM username; the password field is irrelevant (Querycop discards and replaces with the minted IAM token).

Terminal window
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:

Terminal window
MYSQL_PWD=ignored mysql --enable-cleartext-plugin \
-h 127.0.0.1 -P 15336 -u iam_user appdb

If 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.

Identical to the Aurora PostgreSQL story. Quick recap:

EndpointHostname shapeUse for
Writer (cluster endpoint)mydb.cluster-xxxxxxxx.<region>.rds.amazonaws.comAll writes + reads from primary
Reader (cluster reader endpoint)mydb.cluster-ro-xxxxxxxx.<region>.rds.amazonaws.comReads from any healthy reader, automatic load-balance
Instancemydb-instance-1.xxxxxxxx.<region>.rds.amazonaws.comPinning 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.

Terminal window
# 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:

  1. ERROR 2026 (HY000): SSL connection error → the global-bundle CA file is missing or stale. Re-curl from truststore.pki.rds.amazonaws.com/global/global-bundle.pem.
  2. 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.
  3. ERROR 1045 (28000): Access denied for user 'iam_user'@'…' → either the IAM principal lacks rds-db:connect, the IAM auth wasn’t enabled on the instance/cluster, or the MySQL user wasn’t created with AWSAuthenticationPlugin AS 'RDS'.

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 failed in 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:

Terminal window
export GATEKEEPER_BACKEND_HOST=db.internal.example.com
export GATEKEEPER_BACKEND_TLS_SERVER_NAME=mydb.cluster-xxxxxxxx.us-east-1.rds.amazonaws.com

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:

PacketQuerycop 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. PHP PDO::ATTR_EMULATE_PREPARES=true, Rails ActiveRecord with prepared statements disabled) are fully covered.
  • Drivers using server-side prepared statements (JDBC default, mysql2 with prepared: true, mysqlclient in 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”
LegConfigured byDefault
Client → QuerycopGATEKEEPER_PROXY_TLS_CERT / _KEYOFF — plaintext unless you put TLS material in front
Querycop → RDSGATEKEEPER_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 rotates the RDS root CAs on a published schedule. When they do:

  1. Re-download global-bundle.pem to the same path.
  2. 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.