Photo by Franck V. on Unsplash
ElastiCache encryption at rest and in transit shows up as AtRestEncryptionEnabled, TransitEncryptionEnabled and TransitEncryptionMode on each replication group and cluster, with AuthTokenEnabled and UserGroupIds for authentication. Serverless caches are always encrypted. In-transit encryption can be turned on in place for Valkey 7.2+ and Redis OSS 7+; at-rest encryption needs a backup and restore.
Caches hold session tokens, rate-limit counters and copies of database rows, so they end up in scope for the same audits as your databases. ElastiCache doesn’t treat its three security settings the same way, though: one can be switched on in place, one only at creation, and one depends on the engine.
This example is for engineers who need a single table of ElastiCache encryption at rest, in transit and authentication across every cache in a Region, with a clear note on what each gap takes to fix. It only reads, so a read-only profile is enough. If you’re also trimming spend, the companion script to find idle ElastiCache clusters tells you which caches aren’t worth fixing because nobody uses them.
How do ElastiCache encryption at rest and in transit differ by engine?
| Cache type | At rest | In transit (TLS) | Authentication |
|---|---|---|---|
| Serverless (any engine) | Always on; AWS owned key by default or your KMS key | Always on | Valkey and Redis OSS use RBAC user groups |
| Node-based Valkey 7.2+ / Redis OSS 4.0.10+ | Only at creation. Always on when durability is enabled | At creation, or later on Valkey 7.2+ and Redis OSS 7+ via preferred then required |
AUTH token (needs TLS) or RBAC user groups |
| Node-based Memcached | Not available; only serverless Memcached is encrypted at rest | Memcached 1.6.12+, only at creation | AUTH and RBAC are Valkey and Redis OSS features |
At-rest encryption covers data on disk during sync, backup and swap operations, and backups stored in Amazon S3. It also depends on the node type: current families such as R7g, M7g and T4g support it, while some older families don’t. In-transit encryption covers client connections and replication traffic between nodes. The Valkey TLS documentation explains how the engine runs its TLS and plaintext ports side by side, which is what ElastiCache’s preferred mode relies on during a migration.
What does TransitEncryptionMode preferred mean?
preferred accepts both TLS and plaintext connections. It exists so you can move clients over one at a time, then switch to required, which drops every unencrypted connection. A cache left on preferred looks encrypted in the console but still accepts plaintext, so the script reports it as a finding.
What does the script do?
- Reads replication groups
paginateDescribeReplicationGroupscovers Valkey and Redis OSS, cluster mode enabled or disabled, with encryption, TLS mode, AUTH and user groups. - Reads standalone clusters
paginateDescribeCacheClusterswithShowCacheClustersNotInReplicationGroupsreturns Memcached clusters and single-node caches that aren’t in a replication group. - Reads serverless caches
paginateDescribeServerlessCacheslists them with their storage encryption type and user group. - EvaluatesFlags no at-rest encryption, no TLS, TLS in
preferredmode, and Valkey or Redis OSS caches with neither AUTH nor RBAC. - Reports onlyPrints a table, writes a CSV with
--csv, and modifies nothing.
Prerequisites
- Node.js 18 or later, npm and
tsx, plus@aws-sdk/client-elasticache. - An AWS profile set up as in the guide to configure AWS SDK v3 credential providers.
- The engine version of each cache, if you plan to enable TLS in place. The
Enginecolumn shows it for standalone clusters; for replication groups, runaws elasticache describe-cache-clusters --cache-cluster-id <member>.
Which IAM permissions does it need?
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadCaches",
"Effect": "Allow",
"Action": [
"elasticache:DescribeReplicationGroups",
"elasticache:DescribeCacheClusters",
"elasticache:DescribeServerlessCaches"
],
"Resource": "*"
}
]
}
These describe calls return settings, never cache data or AUTH tokens. To confirm the list against the code, run the script through the IAM policy generator for TypeScript.
The script to find ElastiCache clusters without encryption
// find-elasticache-clusters-without-encryption.ts
// Report only. Lists ElastiCache replication groups, standalone clusters (including Memcached) and
// serverless caches in each Region, with encryption at rest, encryption in transit and authentication.
// Usage:
// npx tsx find-elasticache-clusters-without-encryption.ts [--regions us-east-1,eu-west-1] [--csv elasticache.csv]
import { writeFileSync } from "node:fs";
import {
ElastiCacheClient,
paginateDescribeCacheClusters,
paginateDescribeReplicationGroups,
paginateDescribeServerlessCaches,
} from "@aws-sdk/client-elasticache";
const args = process.argv.slice(2);
const flag = (name: string): string | undefined => {
const i = args.indexOf(name);
return i >= 0 ? args[i + 1] : undefined;
};
const csvPath = flag("--csv");
const regions = (flag("--regions") ?? process.env.AWS_REGION ?? "us-east-1")
.split(",")
.map((r) => r.trim())
.filter(Boolean);
interface Row {
Region: string;
Kind: string;
Name: string;
Engine: string;
AtRest: string;
InTransit: string;
Auth: string;
Findings: string;
}
interface Settings {
engine: string;
atRest: boolean | undefined;
transit: boolean | undefined;
mode: string | undefined; // "preferred" or "required" when transit encryption is on
authToken: boolean | undefined;
userGroups: string[];
}
function evaluate(s: Settings): Pick<Row, "AtRest" | "InTransit" | "Auth" | "Findings"> {
const memcached = s.engine === "memcached";
const findings: string[] = [];
// Node-based Memcached has no at-rest encryption option; only serverless Memcached is encrypted at rest.
const atRest = memcached ? "n/a" : s.atRest ? "yes" : "NO";
if (atRest === "NO") findings.push("no encryption at rest (rebuild from backup)");
let inTransit = "NO";
if (s.transit) inTransit = s.mode === "preferred" ? "preferred" : "required";
if (inTransit === "NO") findings.push("no TLS");
if (inTransit === "preferred") findings.push("TLS preferred: plaintext clients still accepted");
let auth = "none";
if (memcached) auth = "n/a";
else if (s.userGroups.length) auth = `RBAC (${s.userGroups.join(",")})`;
else if (s.authToken) auth = "AUTH token";
if (auth === "none") findings.push("no AUTH or RBAC");
return { AtRest: atRest, InTransit: inTransit, Auth: auth, Findings: findings.join("; ") || "ok" };
}
async function scanRegion(region: string): Promise<Row[]> {
const ec = new ElastiCacheClient({ region });
const rows: Row[] = [];
// Valkey and Redis OSS replication groups (cluster mode enabled or disabled).
for await (const page of paginateDescribeReplicationGroups({ client: ec }, {})) {
for (const rg of page.ReplicationGroups ?? []) {
const engine = rg.Engine ?? "redis";
rows.push({
Region: region,
Kind: "replication group",
Name: rg.ReplicationGroupId ?? "?",
Engine: engine,
...evaluate({
engine,
atRest: rg.AtRestEncryptionEnabled,
transit: rg.TransitEncryptionEnabled,
mode: rg.TransitEncryptionMode,
authToken: rg.AuthTokenEnabled,
userGroups: rg.UserGroupIds ?? [],
}),
});
}
}
// Clusters outside a replication group: Memcached clusters and single-node Valkey or Redis OSS clusters.
const standalone = paginateDescribeCacheClusters({ client: ec }, { ShowCacheClustersNotInReplicationGroups: true });
for await (const page of standalone) {
for (const c of page.CacheClusters ?? []) {
const engine = c.Engine ?? "?";
rows.push({
Region: region,
Kind: "cluster",
Name: c.CacheClusterId ?? "?",
Engine: `${engine} ${c.EngineVersion ?? ""}`.trim(),
...evaluate({
engine,
atRest: c.AtRestEncryptionEnabled,
transit: c.TransitEncryptionEnabled,
mode: c.TransitEncryptionMode,
authToken: c.AuthTokenEnabled,
userGroups: [],
}),
});
}
}
// Serverless caches always encrypt at rest and in transit, so only the key type and user group are shown.
for await (const page of paginateDescribeServerlessCaches({ client: ec }, {})) {
for (const s of page.ServerlessCaches ?? []) {
rows.push({
Region: region,
Kind: "serverless",
Name: s.ServerlessCacheName ?? "?",
Engine: s.Engine ?? "?",
AtRest: s.StorageEncryptionType ?? (s.KmsKeyId ? "sse-kms" : "yes"),
InTransit: "required",
Auth: s.UserGroupId ? `RBAC (${s.UserGroupId})` : "no user group",
Findings: "ok",
});
}
}
return rows;
}
function toCsv(rows: Row[]): string {
const cols = Object.keys(rows[0] ?? {}) as (keyof Row)[];
const cell = (v: string) => `"${v.replace(/"/g, '""')}"`;
return [cols.join(","), ...rows.map((r) => cols.map((c) => cell(r[c])).join(","))].join("\n") + "\n";
}
async function main(): Promise<void> {
const rows: Row[] = [];
for (const region of regions) {
try {
rows.push(...(await scanRegion(region)));
} catch (err) {
console.error(`${region}: ${err instanceof Error ? `${err.name}: ${err.message}` : String(err)}`);
}
}
const findings = rows.filter((r) => r.Findings !== "ok");
console.table(findings.length ? findings : rows);
console.log(`${rows.length} caches checked, ${findings.length} with findings`);
if (csvPath && rows.length) {
writeFileSync(csvPath, toCsv(rows));
console.log(`Wrote ${rows.length} rows to ${csvPath}`);
}
console.log("Report only: nothing was modified.");
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
How do you run it?
npm install @aws-sdk/client-elasticache
npm install --save-dev tsx typescript @types/node
AWS_PROFILE=readonly npx tsx find-elasticache-clusters-without-encryption.ts --regions us-east-1,eu-west-1 --csv elasticache.csv
Sample output
┌─────────┬─────────────┬─────────────────────┬───────────────┬────────────────────┬────────┬─────────────┬──────────────┬────────────────────────────────────────────────────────────────────────┐
│ (index) │ Region │ Kind │ Name │ Engine │ AtRest │ InTransit │ Auth │ Findings │
├─────────┼─────────────┼─────────────────────┼───────────────┼────────────────────┼────────┼─────────────┼──────────────┼────────────────────────────────────────────────────────────────────────┤
│ 0 │ 'us-east-1' │ 'replication group' │ 'sessions' │ 'redis' │ 'NO' │ 'NO' │ 'none' │ 'no encryption at rest (rebuild from backup); no TLS; no AUTH or RBAC' │
│ 1 │ 'us-east-1' │ 'replication group' │ 'rate-limits' │ 'valkey' │ 'yes' │ 'preferred' │ 'AUTH token' │ 'TLS preferred: plaintext clients still accepted' │
│ 2 │ 'eu-west-1' │ 'cluster' │ 'page-cache' │ 'memcached 1.6.22' │ 'n/a' │ 'NO' │ 'n/a' │ 'no TLS' │
└─────────┴─────────────┴─────────────────────┴───────────────┴────────────────────┴────────┴─────────────┴──────────────┴────────────────────────────────────────────────────────────────────────┘
9 caches checked, 3 with findings
Wrote 9 rows to elasticache.csv
Report only: nothing was modified.
Names are illustrative. rate-limits is the quick win: TLS is already on in preferred mode, so once every client connects over TLS you switch it to required. sessions needs a rebuild for at-rest encryption, and that rebuild is the moment to add TLS and authentication too. page-cache is Memcached, where TLS can only be set at creation.
How do you fix each finding?
Turn on TLS for an existing Valkey or Redis OSS cache
- Check the versionIn-place TLS needs Valkey 7.2 or later, or Redis OSS 7 or later. Upgrade the engine first if it’s older.
- Set preferred
aws elasticache modify-replication-group --replication-group-id rate-limits --transit-encryption-enabled --transit-encryption-mode preferred --apply-immediately. Clients can now use either. - Move clients to TLSEnable TLS in each client library and redeploy. Your client must support TLS connections.
- Set requiredRun the same command with
--transit-encryption-mode required. Plaintext connections are dropped from then on.
Add encryption at rest
You can’t modify a replication group to add it. Take a manual backup with aws elasticache create-snapshot, restore it into a new replication group with --at-rest-encryption-enabled (and --kms-key-id if you want your own key), point the application at the new endpoint, then delete the old group. Because the cache is new, turn on TLS and authentication at the same time.
Add authentication
RBAC has superseded AUTH tokens: create users and a user group, then attach it with modify-replication-group --user-group-ids-to-add. If you use an AUTH token instead, it needs TLS, it must be 16 to 128 printable characters, and you roll it out with --auth-token-update-strategy ROTATE before switching to SET, so clients without the token keep working in between. Keep the token in AWS Secrets Manager rather than in code; the guide to get a Secrets Manager secret value with AWS SDK v3 shows the client side.
Warning: the TLS certificate for an encrypted cache includes the cluster name, and AWS Certificate Manager records it in public Certificate Transparency logs. Don’t put customer or project code names in cache names.
Troubleshooting
- Clients time out right after you switch to
required. They’re still connecting without TLS. Go back topreferred, find them, and move them over. - The TLS option is greyed out in the console. The engine version is older than Valkey 7.2 or Redis OSS 7, so in-place modification isn’t supported.
- A cache has AtRest
yesbut you didn’t choose it. Clusters with durability enabled always have at-rest encryption, and for Valkey theAtRestEncryptionEnabledparameter defaults to true. - Security groups matter as much as TLS. Encryption and TLS don’t limit who can connect; the cache’s security group does, and an overly wide inbound rule exposes it to more hosts than needed. Use the check to find security groups open to the internet on common ports, and find unused security groups left behind by old caches.
For the rest of your data stores, run the scripts to find unencrypted RDS instances and Aurora clusters and to find unencrypted EBS volumes. If you restore caches with a customer managed key, check that KMS key rotation is enabled: disabling that key would make the cache unrecoverable.
Ask ChatWithCloud instead
To check one Region quickly, ask ChatWithCloud “Which ElastiCache replication groups in us-east-1 don’t have encryption in transit?” It writes AWS SDK for JavaScript v2 code, runs it with your profile on your machine and explains the result, one profile and Region per session. Since SDK v2 reached end of support in September 2025, newer fields may be missing from its answers; how ChatWithCloud runs AWS code locally explains the loop. It runs changes without asking, so connect ChatWithCloud to a read-only AWS profile, and try the questions in analyze your AWS security posture from the terminal.
Frequently asked questions
Can I enable ElastiCache encryption at rest on an existing cluster?
No. Take a manual backup, restore it into a new replication group with at-rest encryption enabled, move your application to the new endpoint, then delete the old group.
Can I enable in-transit encryption without downtime?
On Valkey 7.2+ and Redis OSS 7+, yes: set the transit encryption mode to preferred, move clients to TLS, then set it to required. Older versions and Memcached need a new cluster.
Is ElastiCache Serverless encrypted?
Yes. Every serverless cache has encryption at rest and in transit enabled, using an AWS owned key unless you choose a customer managed KMS key.
Does Memcached support encryption at rest in ElastiCache?
Only on serverless caches. Node-based Memcached clusters support in-transit encryption from version 1.6.12, set when the cluster is created.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud