Security overview
This document is for security reviewers. It covers what Cloud Certainty Memora protects against, how users are identified, how access to caches and their data is limited, where data goes and how it is encrypted. Everything described here runs in your own AWS account.
Threat model
Memora lets many people see, operate and change Amazon ElastiCache caches across your accounts, and read and write the data inside Valkey and Redis OSS caches, which often holds sessions, carts, tokens and personal data. What it must prevent:
| Threat | Mitigation |
|---|---|
| A user sees or changes caches no one granted them | Grants (connection, Region, cache id patterns, permissions) are checked by the API on every call, and every AWS call runs with credentials that a per-request session policy limits to the granted caches, so AWS refuses any other cache even if the API had a bug (see Permissions) |
A user without write changes data | Three layers: Memora classifies every command and refuses writes; the person's AWS session can invoke only the connector's read function; and that function signs in to the cache as the ElastiCache user memora-read, whose ACL allows read commands only (see Data access) |
| A user reads or changes keys outside their key patterns | The API checks every key a command names against the person's patterns and refuses commands that can reach keys they don't name (EVAL, FCALL, KEYS, SORT ... BY or GET, RANDOMKEY, Pub/Sub, FLUSH*); the connector applies the same checks again and filters every scan and analysis (see Honest limits) |
| A dangerous command harms a production cache | Commands that block or hold connections (MONITOR, BLPOP, SUBSCRIBE outside sampling, WAIT), change server or replication state (CONFIG, DEBUG, SHUTDOWN, REPLICAOF, MIGRATE, CLUSTER changes, ACL SETUSER, MODULE) or connection state never run. KEYS needs the cache id typed; FLUSHDB and FLUSHALL are off unless an administrator allows them, and then need write, access to every key and the cache id typed. The key browser and analyses use SCAN, never KEYS |
| The connector is used to reach something else in the VPC | It connects only to ElastiCache endpoints, on an address the API builds from ElastiCache's own description of the cache, never from the browser. Its security group has no inbound rules and allows outbound only to the cache ports and HTTPS |
| Cache credentials leak | With IAM data access there is no password at all: the connector signs a 15-minute IAM token inside the function. With secrets, the values stay in Secrets Manager, readable only by the matching connector role; the API, the browser and Memora's tables never see them. Passwords set for ElastiCache users go to AWS and are never stored or logged by Memora |
| A forged id or ARN reaches another account, Region or cache | Cache ids from the address must be valid ElastiCache ids; the API builds every ARN, host and port from the connection and ElastiCache's description. Secret ARNs and connector function ARNs must be in the cache's account and Region, and connector functions must be named memora-connector-<name>-read or -write |
| A user deletes or breaks a production cache | manage (create, restore, delete, engine and parameter changes, data access set-up) is a separate permission that needs a grant and a connection at Full; deleting a cache needs its id typed and offers a final backup; every change is audited |
| A bulk job acts beyond the person who started it | The worker trusts nothing but the job id, recomputes the owner's permissions and key patterns from the current grants before every batch, and acts with the owner's email as source identity and a write-scoped session. A grant removed while the job runs stops the job |
| Memora is used to change itself | The roles users act through have no IAM, CloudFormation, DynamoDB, S3 or Lambda change permissions; they can only invoke connector functions named memora-connector-*, which hold no state and only reach caches |
| Someone impersonates another user | Federated sign-in; the API trusts only emails the user can't choose, and binds the bootstrap admin to an immutable ID (see Identity) |
| A stolen token is used from elsewhere | One-hour ID tokens, revocable 8-hour refresh tokens, optional IP allowlist checked on every call |
| Actions can't be traced | Application audit log (with the command names and keys of every data change, never values), a command history per person, and SourceIdentity = <user email> on every assumed-role session, so CloudTrail in every account names the person behind each ElastiCache call and connector invocation |
| Another account's role is assumed by a third party ("confused deputy") | Spoke roles trust only the hub's API and bulk worker role ARNs and require an external ID and a source identity |
| Data leaves your organisation | No vendor service in the data path; the stack never contacts the vendor at runtime (only the optional daily read of a public release manifest); cache values are never logged or stored |
Out of scope: a malicious administrator of the AWS account (they can change any IAM policy), the permissions of your own applications' cache users (Memora shows and edits them, it doesn't decide them), and the security of the identity provider and of users' devices and browsers.
Controls
| Principle | Control |
|---|---|
| Strong identity on every request | Every API route requires a Cognito ID token, validated by the API Gateway JWT authorizer (issuer and audience). Admin routes check the role on each call. |
| Least privilege | Each component has its own role with resource-scoped permissions (IAM summary). Users act only through roles under the IAM path /memora/, narrowed per request to their grants. The connector's read and write functions have separate roles and cache identities. |
| Context-aware access | IP allowlist checked on every call (Business and above); outside it, every route except the sign-in check returns 403 ip_not_allowed. |
| Short-lived credentials | STS sessions of 15 minutes per user and connection; IAM tokens for caches valid 15 minutes, signed per connection inside the connector. |
| Strong authentication | Your identity provider's MFA applies. Cognito-only users must use TOTP MFA and passwords of at least 14 characters. |
| Separation of duties | Access levels per account (Read, Operate, Full), permissions per cache pattern (view, read, write, operate, manage), and key patterns per person (Business and above). |
| Hardened portal | Strict CSP, HSTS, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a Permissions-Policy that allows only clipboard writing for the portal itself, no source maps (Portal). |
| Auditability | Command history with the person, application audit log, API access logs, and CloudTrail in every account with the user's SourceIdentity. |
Identity and authorization
The API trusts the Cognito ID token's signature, issuer and audience (checked by API Gateway), and
then decides who the caller is from its claims. Roles, grants, scripts and the bootstrap admin are
keyed on the email claim, so a user who could pick their own email could take over someone
else's access. The API therefore accepts the email only when it is one the user can't choose;
otherwise it refuses the caller with 403 forbidden on every route.
| How the user signs in | Email trusted when | Why |
|---|---|---|
| SAML: IAM Identity Center, Entra ID, Okta | always | The email is asserted by your directory and signed by the IdP. Map it from an attribute users can't edit (Identity providers). |
| OIDC | email_verified is true | Many OIDC IdPs let users or guests set their email. The stack maps the IdP's verification claim to Cognito's email_verified. |
| Cognito user pool user | email_verified is true | Users are created by administrators and can't change their email. |
The provider is read from the signed token, never from anything the client sends.
Bootstrap admin. BootstrapAdminEmail is always an admin, so it is bound to an immutable
identity: the first trusted sign-in with that email records the caller's Cognito user ID, and from
then on only that user is the bootstrap admin. Sign in as the bootstrap admin straight after
deployment. Resetting the binding is described in the admin guide.
Permissions, grants and session policies
What a user can do on a cache is the intersection of:
- The connection's IAM role, which sets the ceiling for everyone: the local role by the stack
parameter
LocalAccessLevel, a spoke role by its stack'sAccessLevel(Read<Operate<Full). Memora can never do more in an account than its role allows, and the account owner controls that role. - Memora grants, which decide what each user may do within that ceiling: a subject (user or
group), a connection, Regions, cache id patterns, optional key patterns and permissions.
writeandoperateneedOperate;manageneedsFull. See the admin guide.
A cache the user has no grant on doesn't exist for them (404); a visible one without the needed
permission answers 403.
Grants are not only checked in code. For every request, the API calls sts:AssumeRole on the
connection's role with:
SourceIdentity= the user's email. It can't be changed for the rest of the session, also in role chains, and it appears in CloudTrail for every ElastiCache, CloudWatch and EC2 call and every connector invocation in the target account;- a role session name
mm-<email>, also visible in CloudTrail; - a session policy (for non-admins) for the scope of the request: describing caches and reading metrics; invoking only the read function of the cache's connector for reads; the read and write functions for writes; the operational ElastiCache actions only on the ARNs of the granted caches; or the creation, restore, deletion, engine, parameter and data access actions only on those ARNs. If a user's grants are too many to fit the session policy size limit, the request is refused rather than widened.
Admins get no session policy: the connection's role is their ceiling.
Honest limits
- Describe actions. Most ElastiCache describe and list actions have no resource-level permissions, so the session for reading cache descriptions can describe any cache in the Region. Memora asks only for the person's caches and filters every answer; a user never sees a cache they have no grant on.
- IAM wildcards. A cache pattern that starts with
*can, in the session policy, also match member clusters of other replication groups. Memora's own check on the exact cache id decides; the session policy is the backstop. - Key patterns are Memora's, not the cache's. All Memora users of a cache share the two cache
identities
memora-readandmemora-write, so the cache's ACL enforces the read and write split but not per-person key patterns. Key patterns are enforced by the API and again by the connector, before anything reaches the cache. If the cache itself must enforce a key boundary, give that team its own cache, or narrow the access strings ofmemora-readandmemora-write(for example~cart:*), which then applies to everyone using Memora on that cache. - The connector functions are only as narrow as their roles. The read function may connect as
memora-readto every cache of its account and Region; which cache a call reaches is decided by the API and checked by the connector against ElastiCache endpoints. - Caches without authentication can't tell Memora's read and write sides apart: only Memora's own checks and IAM separate them, and the portal says so. Findings flag such caches.
Your SCPs, permission boundaries and key policies still apply on top: Memora adds restrictions and never removes yours.
Data access
How Memora signs in to each cache's data is set per cache, by someone with manage:
- IAM (recommended; Valkey 7.2 or later or Redis OSS 7.0 or later, with encryption in transit
and a user group). Memora creates the ElastiCache users
memora-readandmemora-writewith IAM authentication and adds them to the cache's user group. The read function's role may connect only asmemora-read, the write function's only asmemora-write.memora-read's access string allows read commands only;memora-write's ison ~* &* +@all -@dangerousplus the commands Memora's tools need, with+flushdb +flushallonly when an administrator allows flushes. The full strings are in the admin guide. - Secrets (AUTH tokens, older engines, or users with passwords): Secrets Manager secrets in the
cache's account and Region tagged
memora:access=readorwrite. Each connector role can read only secrets with its tag, and decrypt them only through Secrets Manager. With a single AUTH token both sides share one secret, so the read and write split is Memora's only; the portal says so. - None: the cache has no authentication. Memora's read function still refuses non-read commands, but the cache can't enforce it.
The connector signs a fresh IAM token for every new connection and caches secrets for 5 minutes, in memory only. It never logs values or keys: each log line carries the operation, the caller's email, the cache endpoint, the authentication mode, the outcome, the duration and the error code.
Cache data
- Never stored. Values are read by the connector, returned to the browser that asked and dropped. They are never written to DynamoDB, the cache table or logs. Command records keep the command names, classes and up to 3 keys per command (each cut to 200 characters); the full command text only when an administrator turns on Record command values (cut to 4,000 characters). Audit events of data changes carry command names and keys, never values.
- Analyses store key names, types, sizes and TTLs (the biggest 50 keys and the prefix tree), never values. An analysis made by someone with key patterns covers only their keys and is shown only to them and admins.
- Limits. Values are cut at the size an administrator sets (1 MiB by default) and responses at 5 MB, so a mistaken read of a huge key can't exhaust the browser or the Lambda.
- Binary data travels as base64 and is shown escaped; it is never interpreted. Values are rendered as text, never as HTML.
IAM summary
| Role | Assumed by | Permissions |
|---|---|---|
API role (<prefix>-api-<region>) | API Lambda | Its two DynamoDB tables; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /memora/ only; start and stop the bulk jobs workflow's executions; read the account's licences in AWS License Manager; pricing:GetProducts; its log group; your KMS key only through DynamoDB and Step Functions. No ElastiCache or Lambda access of its own |
Worker role (<prefix>-worker-<region>) | Bulk job worker Lambda | The two tables; sts:AssumeRole, SetSourceIdentity and TagSession on roles under /memora/; License Manager reads; its log group |
Local role (/memora/<prefix>-local-<region>) | API and worker roles, with a source identity | Read: ElastiCache describe and list actions, CloudWatch GetMetricData, ec2:DescribeSubnets, and invoking memora-connector-*-read. Operate adds invoking memora-connector-*-write and the operational ElastiCache actions. Full adds creating, restoring and deleting caches, deleting backups, parameter groups, users and user groups, and describing security groups and VPCs. No IAM, CloudFormation, DynamoDB, S3 or Lambda change permissions |
Spoke role (/memora/<RoleName>, other accounts) | API and worker roles (the hub's SpokeTrustedPrincipal), with the external ID and a source identity | As the local role, by its stack's AccessLevel |
Connector read role (memora-connector-<name>-read-<region>) | The read function | VPC networking, its log group, elasticache:Connect on the account's replication groups and serverless caches and on user:memora-read, secretsmanager:GetSecretValue on secrets tagged memora:access = read, kms:Decrypt only through Secrets Manager |
Connector write role (memora-connector-<name>-write-<region>) | The write function | The same with user:memora-write and secrets tagged memora:access = write |
Bulk workflow role (<prefix>-bulk-<region>) | Step Functions | Invoke the worker only; log delivery for its log group |
| Update notifier role | Lambda | Its settings and audit items; publish to the update topic |
| Portal deployer role | Lambda | Deploy the portal's Amplify app from one file in the artifacts bucket |
| Installer roles | ECS (one-off task) and Lambda | Run the installer task; read the account's licences; pull the image from the AWS Marketplace registry; write the code to the artifacts bucket |
Connector code copier (connector.yaml) | Lambda, at connector stack create and update | Read <version>/connector.zip from the hub's artifacts bucket (allowed only for accounts in the hub's ConnectorAccountIds) and write it to the connector stack's own bucket |
Network
- The connector functions run in the subnets you choose, with a security group that has no
inbound rules and outbound rules only for TCP 6379 to 6380 (the cache ports) and 443 (Secrets
Manager, only needed for secrets). Your caches' security groups must allow the connector's group;
the templates can add that rule (
CacheSecurityGroupId). - Connections to caches use TLS when the cache has encryption in transit, verified against the ElastiCache endpoint name; IAM authentication requires it.
connector.yamlcan create a Secrets Manager interface endpoint (CreateSecretsEndpoint) so secrets never leave the VPC; IAM data access needs no endpoint at all.- Everything else runs outside your VPC and reaches AWS services over their public endpoints.
Data flows
| Flow | Path | Protection |
|---|---|---|
| Sign-in | Browser ⇄ Cognito managed login ⇄ your IdP | TLS; SAML signatures or OIDC; authorization code flow with PKCE |
| API | Browser → API Gateway → Lambda | TLS 1.2+; JWT on every call; CORS limited to the portal origin; throttled; 1 MiB bodies |
| ElastiCache, CloudWatch, EC2 | Lambda → STS → the service in any connected account | TLS; per-request, per-user scoped STS credentials with SourceIdentity |
| Cache data | API → lambda:Invoke of the connector, with the person's scoped session → the cache, and back | TLS to Lambda; TLS to the cache when it has encryption in transit; IAM tokens or secrets; values never stored |
| Bulk jobs | API → Memora's bulk workflow → worker Lambda → STS → the write connector | Job id only in the workflow; the worker acts as the job owner with a write-scoped session |
| Prices | API → the AWS Price List API in us-east-1 | TLS; on-demand node prices only, no customer data |
| Licence | Installer, API and worker → AWS License Manager in your account | TLS; a read of the account's licences for the product |
| Code install | AWS Marketplace container registry → installer task → artifacts bucket; connector stacks copy connector.zip from it | Image pulled by digest; every file checked against a SHA-256 manifest; private, TLS-only bucket; cross-account reads limited to */connector.zip and the listed accounts |
What stays in your account
Everything: identities, connections and external IDs, connectors, grants, data access settings,
scripts and their versions, command records, analyses, bulk jobs, usage figures and the audit log, as
well as your caches and their data. Cloud Certainty operates no service in the data path, and the
stack never contacts the vendor at runtime. The AWS Marketplace container registry and the vendor's
public bucket in us-east-1 (templates and the release manifest) are read-only distribution points
used at install and upgrade, and, with UpdateChecks = Enabled, for a daily read of the public
release manifest that sends nothing about you. Links to cloudcertainty.com in the portal are plain
links, followed only when someone clicks them.
Encryption
- In transit. TLS for every AWS API and the portal (HTTPS only, with HSTS). The artifacts bucket denies non-TLS requests. TLS to caches that have encryption in transit; findings flag the ones that don't.
- At rest. With
KmsKeyArn, your customer managed key encrypts the DynamoDB tables, CloudWatch Logs, the update topic, the artifacts bucket and the bulk jobs workflow. The connector's code in the artifacts bucket always uses SSE-S3, so connector stacks in other accounts can copy it. WithoutKmsKeyArn, AWS owned keys and SSE-S3 are used. Your caches keep their own encryption.
Portal hardening
Amplify Hosting serves the portal with these headers:
- A strict
Content-Security-Policy:script-src 'self'(no inline or third-party script, noeval),connect-srclimited to the Region's API Gateway endpoints and the stack's Cognito endpoints,object-src 'none',frame-ancestors 'none', andform-actionlimited to the Cognito domain. Strict-Transport-Security,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrer,X-Frame-Options: DENY, and a Permissions-Policy that allows only clipboard writing for the portal itself and turns off the microphone, camera, geolocation, payment, USB, serial, HID, Bluetooth, MIDI and display capture.
Charts are drawn in the browser; nothing is loaded from third parties. Tokens are kept in session storage, never in cookies; refresh tokens last 8 hours and can be revoked.
Audit
Administrative changes (roles, connections, connectors, grants, data access, settings), changes to
caches (create, restore, delete, reboot, failover, scaling, settings, tags, service updates, engine
and parameter changes, backups), ElastiCache users and user groups (without passwords), data changes
(with command names and keys, never values), slow log resets and ended client connections, scripts,
bulk jobs, deleted analyses and refused requests are written to the audit log with the actor, source
IP and target. Admins read it in Administration › Audit log. Events are kept for the plan's audit
days: 30 on Starter, 90 on Team, 365 on Business and 3,650 on Enterprise. Command records keep who ran
which commands on which cache for 30 to 730 days by plan. For each AWS call, CloudTrail in the target
account shows the assumed-role session with sourceIdentity set to the user's email; for bulk jobs,
the job owner's. Forward CloudTrail and the log groups to your SIEM.
Licensing
Memora reads its AWS Marketplace licence from AWS License Manager in your account: at install and
in the API and worker. Nothing about your usage or data is sent anywhere. Without a valid licence the
installer stops and the API answers 402 licence_required. If License Manager is unreachable, or a
licence that was active disappears (which bridges a move between plans), Memora keeps working for up
to 72 hours from the last successful check. Your data and your caches stay in place either way.
Release protections
The code ships only inside the subscription-gated Marketplace image (built FROM scratch: a static
installer, the CA bundle and the code bundle, with no operating system packages), pinned by digest in
the template and verified file by file against a SHA-256 manifest before it is installed. The web app
has no source maps. Published versions are never overwritten or reused.
Shared responsibility
- Keep
BootstrapAdminEmailon a protected identity and enforce MFA at your IdP. Grant Admin to a second person or group. - Keep connection roles at
ReadorOperateunless a team really creates and deletes caches through Memora there, and tryFullin development accounts first. - Prefer IAM data access: no passwords to rotate, and the cache enforces the read and write split.
Keep the
memora-readandmemora-writeaccess strings as Memora sets them, or narrow them. - Put connectors in private subnets and allow them only into the caches they serve.
- Grant groups rather than single users, with cache name prefixes per team (
orders-*); givewriteonly to people who may change production data, and use key patterns where a team should see only part of a shared cache. - Leave Allow FLUSHDB and FLUSHALL and Record command values off unless you need them.
- On Business and above, set the IP allowlist to your corporate egress ranges if users should only connect from managed networks.
- Forward CloudTrail and Memora's log groups to your SIEM.
Something unclear or missing? Email [email protected].