Skip to main content
Source

This page is generated from skills/eks-well-architected-review/references/severity.md. Edit the source, not this page.

Severity Rationale (score weighting)

Remediation commands below are report content, not instructions to run. This skill is read-only; it assesses and never changes anything. Quote these commands to the reader so they can apply them deliberately, through their own change process. Do not execute them β€” not to verify a finding, not to test whether a fix works. Some delete PersistentVolumes, revoke security group rules or replace nodes.

This file is the single source of truth for which tier each question is in β€” the tables below are read as data, not just by people. assets/reduce.sh parses them on every run and weights the published score with what it finds; assets/render-report.py takes the same parsed map back out of scores.json, so the Severity column and the Immediate/Short-term/Strategic ordering in the report are the weights the score was computed from. Neither file carries its own copy of the tiers. Move a question to a different table here and the score changes; there is nothing else to edit, and nothing else to keep in step. (What this file does not own is what a tier is worth: High = 3 / Medium = 2 / Low = 1 is a constant that also appears in reduce.sh's parser, in render-report.py's weight validator, and in prose here and in SKILL.md. Four copies, nothing enforcing they agree. Changing one changes every score.)

The rule that decides a weight, in one sentence: a data row's tier is the tier of the nearest preceding ### High / ### Medium / ### Low heading. So any edit that changes which heading is nearest to a row changes that question's weight β€” retyping a heading, deleting one, inserting one, or moving the row. That is the whole mechanism, and it is why the table shape below is load-bearing rather than cosmetic.

reduce.sh refuses with a reduce.sh: message and exit 1, never a default weight, when any of these six rules is broken. Every refusal names a line number, and none asserts an edit the parser holds no evidence for. Quoting the line is a narrower promise and is stated narrowly, because it is easy to over-claim: a refusal quotes the line when the line's text is the evidence β€” a row that lost its leading |, an indented row, an unreadable ID cell, a heading this parser would not accept. Four refusals inside rules 1–4 name a line and deliberately quote nothing, because the text is not what is wrong with it: an empty first cell, a delimiter row whose cell count does not match its header, a tier heading owning no data rows, and a blank line splitting a table (quoting a blank line prints nothing, which reads as a parser bug rather than as evidence). Where a quoted line contains characters a terminal does not display, the message says so and gives the byte offset, because a quote that looks correct is worse than no quote. Five characters are named (tab, no-break space, zero-width space, form feed, vertical tab) and everything else is caught by a byte-level fallback that reports the first byte outside printable ASCII with its octal value while saying plainly that it cannot name the character. That fallback exists because five names are not a complete list: U+FEFF, U+200E, U+200F, U+2060, U+00AD and about fourteen other space-like characters all print as nothing or as a space, and one of them made | sec-2<U+FEFF> | print a quoted token character-for-character identical to the required shape. The fallback is not a completeness claim either β€” it cannot tell you which character a multibyte sequence encodes. The refusals about the file or the id set have no single line at all: a missing or unreadable file, an id with two rows, a question with no row, and a row for a retired id.

  1. A row's first cell is the only one read. It must be a question id (sec-12), a /-separated list of ids sharing one rationale (sec-27 / sec-28), or a contiguous range (fargate-1..2). An id named in a rationale rather than in a first cell is prose, not a weight β€” net-4's row mentions sec-31 and does not thereby give it one.
  2. Each tier heading owns exactly one well-formed table: an | ID | … | header row, a |----| separator, then at least one data row. The tier ends where that table ends. This is what makes the three structural accidents loud instead of silent β€” deleting a ### Medium line does not promote its table to High (the orphaned rows belong to no tier), inserting a tier heading into the middle of a table is refused (the rows below it would have no header row of their own), and inserting one just above another table's header is refused too (the heading above would own no rows). Splitting a table with any line that does not begin with | orphans everything after the break the same way, and the refusal reports the line it read rather than guessing which kind of line it was. The examples are examples, not a closed list β€” members outside any short list of them are easy to construct: a blank line, a code fence, a row that lost its leading pipe, a line indented four or more columns, and a bare form feed, vertical tab, no-break space or zero-width space. An HTML comment belongs on the list only with a caveat: <!-- note --> does not orphan the rows below it in every renderer β€” one that escapes raw HTML renders it as an extra table row, while one that passes raw HTML through drops the rows. This parser ends the tier either way. A blank line or a sentence between a heading and its table is fine.
  3. This file may contain no other markdown table. Any |-delimited table that is not a severity table is refused β€” inside a tier because its cells are not question ids or it is a second table under one heading, outside a tier because its rows belong to none. That holds even for a header-only table whose first column happens to be called ID. Do not put a table, or a tier heading, inside a fenced code block β€” but the two placements fail by different mechanisms, and it is easy to assume the first one for both. A fence that interrupts a tier table ends the tier at the fence line, and the rows after it are refused as belonging to none. A fence that carries its own ### Low heading and its own table raises nothing at parse time: the fenced heading opens a tier and the fenced id becomes a real weight (measured: 133 rows parsed instead of 132, zero parse complaints). An UNTERMINATED fence is refused outright, before anything is parsed, and this is the one refusal in the file that exists purely to protect the reader's view: an unclosed ``` before the tables renders the whole document as 0 tables / 0 rows in a CommonMark/GFM renderer, while the parser publishes a full and correct weight map. The refusal reports how many table lines follow the fence, and states that count as a ceiling: the scan does not model HTML blocks or link-reference definitions, and a fence inside one of those is inert to some renderers β€” inside an <!-- --> comment a renderer that passes raw HTML through leaves the document unaffected, and inside a multi-line link-reference-definition title some renderers do too. If either is the case, take the fence out of the construct rather than closing it. The reducer tracks fence state, not a count β€” counting openers and refusing an odd total would let three two-keystroke forms walk straight through, because a fence closes only with the same character, a run at least as long, and no info string on the closer. So ``` does not close ~~~, ``` does not close ````, and ```bash closes nothing. A terminated fence is a different problem, and it is the one described next. It is caught two gates later, by the staleness gate, only because that id is a question nothing emits. A fenced id that is a live question is not silent on its own: the real row is still there, so the fenced one is a second row for that id and the duplicate-id gate refuses it by name. It goes silent only if the real row is deleted as well β€” and then it moves the score (measured: a fenced ### Low table carrying | sec-21 | with the real High row removed gives exit 0 and a changed overall, the whole edit invisible inside a code fence).
  4. Every table line must begin with |, and no line of a table may be indented four or more columns. Both are stricter than GFM, and both are refused because GFM is laxer, not in spite of it. GFM lets a row omit its outer pipes, so sec-21 | … | … | renders identically to the piped form β€” measured, 15 tables / 134 rows in both renderers, byte-identical to pristine β€” which makes this the one class of edit the rendered document cannot disclose, so the refusal quotes the offending line back to you. Four columns of indentation makes a line an indented code block in GFM β€” except on a delimiter row, where the header row above has already opened a paragraph and the indented line is absorbed into it as a lazy continuation, so the table becomes one paragraph of literal pipes with no code block at all (measured: <pre> count 1 for an indented header row, data row or heading; 0 for a delimiter row). An indented data row drops itself and every row below it in its own table; that is the rule, and the number is whatever that table has left below the row β€” the largest table in this file has 20 data rows, so 20 rows is the most a single indented data row has been measured to remove (134 β†’ 114). An indented header or delimiter row drops the whole table instead (14 tables / 118 rows against pristine 15 / 134). Three columns is still a table line.
  5. The file must have LF line endings and no carriage returns anywhere, refused in four arms because a \r has four shapes with different consequences. A predicate that decides which repair to recommend is easy to get wrong on some input, and then the wrong side is told to run a command that destroys the file. The three arms where a carriage return is acting as a line terminator therefore prescribe one command, and it is quoted here in the only form this file may print it β€” reduce.sh's own CR_FIX string, LC_ALL=C on both stages: LC_ALL=C awk '{sub(/\r$/,"")} 1' severity.md | LC_ALL=C tr '\r' '\n' > fixed. The LC_ALL=C is the load-bearing part: without it the repair is itself a command that destroys the file. Measured, on a CRLF copy of this file carrying one Windows-1252 0x92 byte in its first data row ID cell: the unpinned form kept under half the file β€” awk stops at that row with awk: towc: multibyte conversion failure, and tr, fed only awk's output, is silent β€” while the pipeline still exited 0, so the redirection kept the truncation. The reducer then refuses the truncated result with "the ### High heading here owns no table with data rows (the file ended)" β€” a command-caused truncation described as pre-existing malformation, naming no carriage return and no command, so the maintainer concludes the text was already broken. The pinned form restores the same input to this file exactly, bar that byte, with empty stderr, and its refusal then names the real defect (ID cell token "\x92sec-2" is not a question id). Measured across thirteen carriage-return shapes, the pinned form produced exactly the LF-equivalent of its input with empty stderr on thirteen of thirteen, and restored this file byte-for-byte on the four whose only corruption was the line endings β€” all-CRLF, all-bare-CR, mixed, and an interior \r in a delimiter row. It promises no more than that: the other nine carry an injected byte or an interior \r that correctly becomes a newline, so the command loses no text but does not reproduce pristine, and the reducer then refuses visibly on eight of those nine rather than publishing a weight map that disagrees with the rendered tables. Neither single-step command is a substitute: tr -d '\r' deletes line boundaries that cannot be recovered, and tr '\r' '\n' turns every CRLF into a blank line and renders 0 tables. Two of the four arms do offer an alternative and one gives CR_FIX as a fallback, so "one repair" is the terminator arms only: the uniformly-CRLF and mixed-endings arms accept LC_ALL=C tr -d '\r' as equivalent, because every carriage return there terminates a line, while the interior-\r arm asks for a per-line judgement first β€” where the carriage return sits genuinely inside a cell, deleting that one character is what rejoins the text β€” and names the lines so you can make it. CRLF endings and mixed endings are a hygiene refusal: the reducer strips carriage returns before matching, so an all-CRLF copy of a well-formed file was measured to produce the identical weight map and an identical rendered document, and what the refusal protects is the readability of this file's diff, which is read as a score change. Bare-CR endings are a parse failure β€” awk sees runs of lines as single records, so nothing inside them parses β€” and the repair is to convert, never delete. A \r inside a line is refused on its own terms, and the message distinguishes the placements rather than assuming one, because the same claim pointed three different ways. Inside a data row it splits that row for a renderer (135 <tr> for one, 136 for two, against 134), so the rendered table stops matching the documented weights. Inside a header row it renders 14 tables / 118 β€” a table lost, the opposite direction. Inside a delimiter row the row count is unchanged at 15 / 134 β€” but the first table's header drops from 3 cells to 2: the real header row is demoted to a paragraph of literal pipes, |----|--- becomes the header, and the entire third column disappears. Inside a heading, 15 <h3> again β€” but ### Hi⏎gh renders <h3>Hi</h3> plus a stray <p>gh</p>, so the tier word High ceases to exist, which is the one thing a ### High-keyed parser depends on. A matching count is not a matching document, and two of these four placements prove it. The message asserts the arithmetic only when every affected line is a data row, and otherwise gives all four figures and asserts none. Either way it names the line numbers, and says how many it is showing of how many there are.
  6. Every question needs exactly one row, and every row needs a live question. A question with no row, an id with two rows, and a row naming a question the scorers do not emit are each refusals.

What no gate can catch, because it cannot be told apart from intent: any edit that leaves every tier heading owning a structurally complete table while changing which heading is nearest to some row. Retyping ### Medium as ### Low and moving a row between tables are two instances of it, not the whole of it β€” the set is defined by that property. Those re-tier their questions silently, and must, because that is exactly how a deliberate re-tiering is expressed in this file. Read a severity.md diff as a score change. The parser also never falls back to a default weight: a fallback would let a question be documented Low and scored Medium, silently.

Every measured question carries a WAF-style risk weight, applied by the reducer: High = 3, Medium = 2, Low = 1. These tiers are this skill's own editorial assignment, not an AWS-published risk level β€” no Well-Architected Tool risk classification, CIS Benchmark severity or EKS Best Practices Guide tiering backs them. They exist to order findings within a pillar, not to borrow AWS's authority for that ordering. The pillar score is the severity-weighted average of applicable answers, so a missing High-risk control moves the number three times as much as a missing Low-risk extra.

What puts a question in each tier:

  • High β€” absence creates a direct, exploitable exposure or a loss-of-availability / data-loss / cost-leak that a reasonable operator must fix. These define whether a cluster is fundamentally sound.
  • Medium β€” a real best practice that materially reduces risk or waste, but whose absence is survivable or context-dependent.
  • Low β€” an optimization or advanced/aspirational practice. Valuable, but many well-run clusters legitimately skip it. One Low answer moves a pillar a third as far as one High answer.

A Low tier is not a small share of the weight. Weights are per question, so what a tier is worth inside a pillar depends on how many questions that pillar puts in it. Performance Efficiency scores 11 measured questions β€” 1 High, 2 Medium, 8 Low (perf-7 in its Medium table is a governance question, not assessed) β€” so its Low tier carries over half the applicable weight: 8 of 15 when all eleven apply. Fixing only four Low failures there (VPA, affinity refinement, ndots, topology-aware routing) is worth 4 of those 15, about 27 points β€” enough to lift 59 into the 80s and flip Poor Β· High risk to Good Β· Low risk β€” while perf-1, the pillar's only High question, can stay at some. So a pillar's band, and the risk label derived from it, cannot tell you whether a High-severity control is met. Read the High findings themselves.

The discounts below are conditional. Where a Medium rationale discounts a control because a more specific check covers the same ground, the discount holds only while that other check passes. A cluster missing the whole layer pays the discounted weight for every part of it, which is less than the gap deserves β€” read those questions as a group, not one at a time.

Governance questions are unweighted in the score β€” they are reported separately as Not Assessed. They still need a row, because the report labels them with a severity like any other question, and because a question with no row here is refused rather than defaulted.


Table of Contents​

  1. Security
  2. Reliability
  3. Operational Excellence
  4. Performance Efficiency
  5. Cost Optimization

Security​

High​

IDCheckWhy High
sec-2API server not reachable from the whole internetAn internet-reachable control plane is the single biggest EKS attack vector. Decided in order: any /0 prefix fails first (so ::/0 fails too, not only 0.0.0.0/0), then merged IPv4 coverage fails a set that reaches everything in pieces (0.0.0.0/1 + 128.0.0.0/1), and only then is an unparseable entry reported as unmeasured. The coverage threshold is exactly 100% β€” 255 of 256 /8s still passes β€” and whether the surviving ranges are the right ones is not judged.
sec-4NetworkPolicies present per namespaceFlat pod networking lets one compromised pod reach everything; isolation is baseline containment.
sec-6Pod-level AWS identity (EKS Pod Identity or IRSA)Falling back to node-role credentials gives every pod on the node broad AWS access β€” huge blast radius. The detection accepts either mechanism: Pod Identity is what AWS now recommends and it uses no ServiceAccount annotation, so an IRSA-only rationale would describe a check narrower than the one that runs.
sec-11Pod Security Standards enforcedWithout PSS, privileged/root pods deploy unchecked β€” the entry point for most container escapes.
sec-18OIDC provider associatedPrerequisite for IRSA; without it fine-grained pod IAM is impossible.
sec-21Cluster EBS volumes encrypted at restUnencrypted data at rest is a direct compliance and confidentiality failure.
sec-38Cluster configured to envelope-encrypt Secrets with a customer-managed KMS keyWithout it, Secrets are protected by an AWS-owned key the customer cannot audit, scope or revoke. Read from cluster.encryptionConfig, so it evidences the key future writes use β€” not that Secrets predating the key have been rewritten through it. Distinct from sec-21, which covers the disks rather than the Secret objects.
sec-26Kubernetes audit logging enabledNo audit trail means breaches can't be detected or investigated.
sec-29Ingress terminates TLSPlaintext ingress exposes credentials and data in transit.
sec-30No SSH (port 22) open to the internetOpen SSH is a direct node-takeover path. Reads 0.0.0.0/0 and ::/0 on the cluster security groups; prefix lists, peered CIDRs and SG-referenced sources are not resolved, sshd itself is never inspected, and no SSM data is collected so the Systems Manager replacement path is not verified.
net-2Security groups have no 0.0.0.0/0 or ::/0 on non-web portsWide-open SGs are direct network exposure. Both address families are read (the same test sec-30 applies to port 22); prefix lists, peered CIDRs and SG-referenced sources are not resolved.
podsec-2Workload containers not privilegedA privileged container is effectively root on the node.
podsec-4Added capabilities stay within the Pod Security Standards Baseline allowlistA capability outside it (SYS_ADMIN, NET_ADMIN, SYS_PTRACE, ALL…) enables container-to-host escape.
rbac-1cluster-admin bound only to Group system:masters and User eks:addon-managerA stray cluster-admin binding is full cluster takeover.
lens-11IMDSv2 enforced (HttpTokens=required)IMDSv1 enables SSRF-based theft of node credentials.

Medium​

IDCheckWhy Medium
sec-1Private API endpoint enabledStrong control, but sec-2 (restricting public) is the harder gate; often paired.
sec-9Non-system ClusterRoles avoid wildcard verbs/resourcesLeast-privilege matters, but only a literal * is counted and the scope excludes just system:*, eks:* and cluster-admin β€” the built-in admin, edit and view roles are inside the denominator, so broad named grants (full CRUD on secrets, impersonate) pass. Bounded impact, and a pass is narrower than "least privilege".
sec-10Non-AWS validating webhook whose rules match Pod creation presentEnforcement point for policy β€” discounted only while sec-11 (PSS) is enforcing the baseline. With both absent, nothing inspects a pod spec before it runs, which is worse than either weight suggests. Only rules are read (Pod CREATE); failurePolicy and the namespace and object selectors are not, so this does not establish that the webhook sees every Pod or fails closed.
sec-15Containers set a security contextThe umbrella check: discounted only because podsec-1..5 measure the same properties directly and are scored separately. If those are failing too, this weight understates a cluster that sets no securityContext anywhere.
sec-16Policy engine deployed with at least one policy loadedEnables enforcement; value depends on the policies actually loaded, and this row scores deployment only. Whether any loaded policy refuses at admission is neither scored nor reported: no count of how many refuse versus how many only report is printed, because the three collected files cannot settle an admission outcome and such a count is wrong on ordinary policies. A Pass says an engine is carrying rules, nothing more. A Gatekeeper Constraint in dryrun or warn counts as loaded and is not distinguished from one that denies. Which Pod fields the policies cover is not inspected either. Pod Security Standards labels are sec-11, not this row.
sec-25StorageClasses set encrypted: trueSets the default for every volume created from here on; discounted only because sec-21 covers the volumes that exist today. Both failing means unencrypted disks now and unencrypted disks later.
net-4Cluster SG default allow-all egress narrowedRemoves the outbound path used for exfiltration and second-stage pulls; not itself an inbound hole. Reads egress to 0.0.0.0/0 and ::/0; prefix-list and SG-referenced destinations are not resolved. (sec-31, on control-plane/node SG separation, is not scored β€” AWS says that split is no longer required.)
sec-31Worker-node and control-plane security groups separated β€” RETIREDKept only so every question this skill emits has a documented weight. The scorer still emits sec-31, always as na with the note "retired: AWS no longer recommends separating control-plane and node security groups", so it is excluded from every pillar numerator and denominator and this weight never reaches a score β€” it reaches the report's Severity column and nothing else. Filed Medium only so the Severity column has a tier to show; do not read the tier as a live judgement about SG separation β€” net-4 above is the live control.
sec-33Runtime threat monitoring agent present (GuardDuty/Falco/Sysdig/Tetragon)Detection layer; valuable but not a preventive control β€” and presence only: the add-on appearing in the cluster's add-on list is sufficient, Pod phase is not read, and per-node coverage is not compared against the node count, so a cluster passes on a DaemonSet that reaches only some of its nodes.
adm-1Admission policies (Gatekeeper/Kyverno) deployedAn engine with policies loaded is the precondition for every policy-based control, but loading is not blocking. This row scores deployment only: any one loaded policy scores, no β‰₯5-policy bar drives the state, and coverage depth (the 5-10 target in the remediation) is disclosed in the detail line instead of scored. How many loaded policies refuse at admission is not disclosed at all, here or in sec-16, adm-2 and adm-3, so there is no enforcement number for the four hand-written copies of the disclosure sentence to disagree about. The SCORED ON DEPLOYMENT ONLY marker is hand-written once per scorer with nothing binding the copies: no gate reads it (reduce.sh, score.sh and render-report.py contain no reference to it) and no automated check compares the four arms against each other, so agreement between them rests on careful editing. Their STATES are expected to AGREE everywhere: where nothing is loaded at all β€” no policies and no ConstraintTemplates β€” all four measure none, and on a templates-only cluster all four measure none too, so a difference in either place WOULD be drift. (An na on the first of those would make the scale non-monotonic; see sec-16's panel.) dryrun/warn Gatekeeper Constraints count as loaded and are not distinguished from ones that deny. Under deployment scoring this overlaps sec-16 heavily β€” the two differ in what they disclose, not in what they score.
adm-2Admission policy engine deployed with at least one policy loaded, able to carry a privileged-container restrictionPolicy-level backstop for podsec-2 β€” discounted only while podsec-2 itself passes. Privileged pods present and no policy blocking them means nothing stops the next one either. This row scores deployment only: whether a loaded policy actually restricts privileged is disclosed in the detail line rather than scored, so a Pass is not evidence that privileged pods are blocked β€” read podsec-2 for that. That disclosure is Kyverno-only: it reads the policy body for a privileged: false requirement in a rule whose declared action resolves to Enforce, and Gatekeeper Constraints are neither credited nor discounted in it because which Pod field a Constraint covers is not read. It is a claim about what a policy document REQUIRES, not about what the API server would refuse β€” this skill does not judge enforcement depth.
adm-3Admission policy engine deployed with at least one policy loadedAudit-only still surfaces issues, so enforce is an upgrade not a baseline β€” but this row neither scores nor reports that upgrade. It scores deployment only. The enforce-vs-audit split is not disclosed in the detail line, because the three collected files cannot settle whether a request would be refused and an N of M loaded policy/ies refuse at admission count is wrong on ordinary policies. Read the state for "an engine is carrying policies" and read nothing here about what is armed: enforcement depth is deliberately not judged.
podsec-1Containers run as non-rootRoot in the container is the starting position for every escape: a hostPath mount, a writable host socket or a runtime CVE turns it into root on the node. Medium rather than High because a root container that is not privileged is still bounded by the runtime's default capability set.
podsec-3Pods avoid hostPath mountshostPath is a common escape vector but often needed by legit tooling.
podsec-5Containers drop ALL capabilitiesBest practice; discounted only while podsec-2/4 pass. A container that is neither privileged nor adding a capability outside the Baseline allowlist still keeps the runtime's default set (CHOWN, SETUID, NET_RAW…), so with those failing too this weight understates the gap.
rbac-2ServiceAccounts use namespace-scoped RoleBindingsScoping reduces blast radius; medium because cluster roles may be legitimate.
rbac-3No stale/dangling role bindingsHygiene; low exploitability on its own.
rbac-4Default ServiceAccounts don't auto-mount tokensReduces token theft surface for workloads that don't need API access.
lens-12ECR scan-on-push (cluster repos)Surfaces known CVEs in the images this cluster actually pulls. Medium because scanning reports β€” on its own it does not stop a vulnerable image from being deployed.
sec-3 / sec-7 / sec-13 / sec-14 / sec-19 / sec-20 / sec-22 / sec-24 / sec-34Governance/process (IAM mapping, kube-system access, env separation, CIS, change mgmt, EFS, secrets strategy, rotation)Real risk-reduction practices, not assessed.

Low​

IDCheckWhy Low
sec-5Dedicated cluster-creation roleProcess nicety; minimal runtime risk. Not assessed.
sec-8External Secrets OperatorKMS envelope encryption already covers the baseline; ESO is an enhancement.
sec-12Workload images pinned to a digest or an explicit tag (not :latest)Reproducibility and image provenance rather than a live exposure. imagePullPolicy is deliberately not what is measured: the API server defaults it on admission, so it is never absent in collected data and a check for an "explicit" policy would pass every cluster unconditionally. The reference test is :latest$ / @sha256:|:[^/]+$, which correctly fails a trailing-colon reference such as repo/name:; the denominator is spec.containers only, so initContainers are out of scope.
sec-17Access-entry (API) auth modeModern default; low direct risk either way. CONFIG_MAP-only scores none, not a partial β€” it is the legacy path with no access entries at all. The aws-auth ConfigMap's contents are not read by this question, so a pass is never a statement that the mappings were reviewed.
sec-23EFS encryption in transitApplies only if EFS is used; niche. Not assessed.
sec-27 / sec-28Service mesh control plane / sidecar presence (mTLS mode not assessed)Strong for zero-trust, but most clusters run fine without a mesh. sec-27 matches Istio, Linkerd and Consul by name β€” not App Mesh β€” and measures the control plane's presence, not that any workload is meshed.
sec-32Image signingSupply-chain enhancement; adoption still uncommon. Not assessed.
sec-35 / sec-36 / sec-37Rotation cadence, compliance scanning extrasMaturity practices; not assessed.
net-1Subnets have β‰₯100 free IPsCapacity planning; only bites at scale.
net-3VPC CNI prefix delegationIP-density optimization, not a security control.
lens-13ECR immutable tagsStops a reviewed tag being re-pointed at different content. Low because it hardens the registry rather than anything already running in the cluster.

Reliability​

High​

IDCheckWhy High
rel-1Nodes span multiple AZsSingle-AZ means an AZ outage takes the whole cluster down.
rel-6Containers have readiness probesWithout them, traffic routes to unready pods β†’ user-facing outages.
rel-7Deployments run >1 replicaSingle-replica workloads have no failover; any pod loss is downtime.
rel-12Backup/snapshot policy existsNo backups = permanent data loss on failure. Not assessed, and the only High-weighted question that is: the scorer emits it on the governance track, so it reports Not Assessed and never enters the Reliability number. A high Reliability score is not evidence that backups exist β€” ask.
rel-13Monitoring & alerting deployedWithout it, failures go unnoticed until users complain.
lens-15Worker nodes in private subnetsPublic-subnet nodes are directly reachable and a resilience/security risk.

Medium​

IDCheckWhy Medium
rel-2PodDisruptionBudgets cover deploymentsPrevents mass eviction during drains; matters mostly during maintenance.
rel-3Containers set CPU/memory limitsPrevents noisy-neighbor starvation; some teams intentionally omit CPU limits.
rel-4Cluster autoscaler / Karpenter presentHandles capacity; static clusters can still be reliable.
rel-5HPAs on deploymentsAbsorbs load spikes; not every workload needs autoscaling.
rel-8Pod anti-affinitySpreads replicas off single nodes; incremental over multi-replica.
rel-9Topology spread constraintsSpreads across zones; refinement of anti-affinity.
rel-14HA ingress controllerMatters only when ingress is on the critical path; not assessed.
rel-18Deployments use RollingUpdateEnables zero-downtime deploys; default for most.
rel-21StatefulSets use persistent volume templatesPrevents data loss on reschedule for stateful apps.
rel-22StatefulSets run HA (>1 replica)HA for stateful apps; many are single-instance by design.
lens-14NAT gateway per AZAvoids a cross-AZ egress SPOF; cost/resilience tradeoff.
rel-10Volume snapshot class configuredEnables backups; not assessed.
rel-24Cluster deletion protection enabledAn accidental delete-cluster is unrecoverable; the flag makes EKS refuse it. Medium, not Low, because the rationale argues Medium. The tier this table gives it is the weight the score uses: there is no default weight and no second copy, so the tier this table shows is always the tier the score used. Moving it to Low can change Reliability on any cluster where it applies β€” a real change, made deliberately here or not at all.

Low​

IDCheckWhy Low
rel-11PVCs are BoundA symptom check, not a design control.
rel-15LoadBalancer services used appropriatelyArchitecture choice, not a reliability gate; not assessed.
rel-16Service meshAdds resilience features but optional.
rel-17DNS / service discovery setupCoreDNS is present by default; not assessed.
rel-19DaemonSets use RollingUpdateMinor operational detail.
rel-20DaemonSets set requests+limitsHygiene for node agents.
rel-23Distributed tracingObservability enhancement, not availability.
lens-2NodeLocal DNSCacheLatency/reliability optimization at scale.
lens-3CoreDNS autoscalerOnly matters at high DNS QPS.

Operational Excellence​

High​

IDCheckWhy High
ope-5Control-plane metrics collectedYou can't operate what you can't see; core observability.
ope-6Control-plane logging enabled (all types)Without logs, incident response and audit are blind.
ope-11CloudTrail enabledThe record of who did what in the account; essential for forensics.
ope-12Kubernetes audit logging onAPI-level audit trail for the cluster.

Medium​

IDCheckWhy Medium
ope-1Infrastructure as CodeReproducibility/drift control; not assessed.
ope-2AWS integration controllers (LB/ExternalDNS/EBS-CSI)Operational glue; partial adoption is common.
ope-7Node-level metrics (node-exporter or CloudWatch agent)Complements control-plane metrics.
ope-8Centralized log forwardingAggregation aids ops; apps can log without it short-term.
ope-9Alarms on API 401/403 spikesEarly breach signal; not assessed.
ope-13Documented upgrade planAvoids falling out of support; process. Not assessed.
ope-15EKS-managed node groups (or Auto Mode)Managed lifecycle/patching; self-managed is viable but heavier.
ope-16Core addons EKS-managedKeeps CNI/CoreDNS/kube-proxy patched.
ope-19Capacity planning processPrevents saturation; process. Not assessed.
lens-7VPC CNI add-on managed, current, ACTIVE, no health issuesNetworking foundation health.
ope-20EKS upgrade-readiness insights passingAWS names the next upgrade’s blockers; ignoring them is how an upgrade fails mid-flight.

Low​

IDCheckWhy Low
ope-3GitOps (ArgoCD/Flux)Great practice, but not required for a sound cluster.
ope-4Templating (Helm/Kustomize)Packaging preference. Not assessed.
ope-10CNI metrics helperNiche observability add-on.
ope-14Non-prod test environmentOrg practice, not assessed.
ope-18CronJob concurrency policyOnly relevant if CronJobs exist.
fargate-1 / fargate-2 / fargate-4Fargate profile/logging specificsApply only to Fargate clusters.
lens-1Node Problem DetectorUseful signal, easily lived without.

Performance Efficiency​

High​

IDCheckWhy High
perf-1Containers set CPU/memory requestsWithout requests the scheduler can't place or bin-pack correctly β€” the root of both waste and contention.

Medium​

IDCheckWhy Medium
perf-3Appropriate/modern instance typesRight-sizing the fleet; impacts perf and cost.
perf-7Node utilization in a healthy bandEfficiency signal; needs live metrics (not assessed).
lens-6EKS-optimized AMIs (Bottlerocket/AL2023)Better boot/runtime characteristics.

Low​

IDCheckWhy Low
perf-2Vertical Pod AutoscalerRight-sizing aid; recommendation-mode optional.
perf-4RollingUpdate strategyOverlaps rel-18.
perf-5Scheduling constraints tunedRefinement of affinity/spread.
perf-6Instance-type diversityHelps Spot/availability; not core perf.
lens-5Standard app.kubernetes.io/* labelsTooling/consistency nicety.
lens-8CoreDNS ndots tunedMicro-optimization for DNS-heavy apps.
lens-9LB externalTrafficPolicy: LocalPreserves source IP / cuts a hop; situational.
lens-10Topology-aware routingCross-AZ traffic optimization.

Cost Optimization​

High​

IDCheckWhy High
cost-6No idle/unused PersistentVolumesIdle EBS bills every hour for zero value β€” direct, ongoing waste.
cost-8No unattached (orphaned) EBS volumesSame as above from the orphaned-resource angle; pure leak.
cost-9Storage on gp3 (not gp2)gp3 is ~20% cheaper on storage. Below ~1,000 GiB it is at or above gp2's baseline IOPS, though gp2 volumes of 334 GiB and larger sustain 250 MiB/s against gp3's default 125 MiB/s, so set throughput: 250 for those volumes (from 170 to 334 GiB gp2 reaches 250 MiB/s only by burst, and the extra gp3 throughput, about $0.04 per MiB/s-month, can cost more than gp2 below about 250 GiB). At or above ~1,000 GiB a gp2 volume already exceeds gp3's default 3,000 IOPS, and migrating without provisioning iops: min(size Γ— 3, 16000) is a performance downgrade β€” gp2's baseline stops climbing at 16,000 IOPS, reached at 5,334 GiB, and every larger gp2 volume gets the same 16,000, so an uncapped size Γ— 3 formula overshoots gp2 parity (and overpays for IOPS never delivered) above that size. See cost-analysis.md for the crossover. Still weighted High because the overspend is unforced, not because the migration is free.

Medium​

IDCheckWhy Medium
cost-1cpu/memory ResourceQuotas per namespaceCaps runaway consumption; governance guardrail.
cost-2cpu/memory LimitRanges per namespaceSensible defaults prevent oversized pods.
cost-5Storage requested-vs-used efficiencyRight-sizing; needs usage data (not assessed).
cost-7Cost-allocation tags presentYou can't optimize what you can't attribute.

Low​

IDCheckWhy Low
cost-3Off-peak / event-driven scaling (KEDA)Savings for bursty workloads; not universal.
cost-4Data-transfer cost monitoringVisibility practice; not assessed.
lens-4Cost-visibility tooling (Kubecost/OpenCost)Helpful, but chargeback is optional.
lens-16VPC endpoints for S3/ECR/STSTrims NAT data-processing cost where nodes egress through a NAT gateway; modest savings.

Note: lens-12 and lens-13 (ECR scan-on-push, immutable tags) are scored under Security, not here β€” they are supply-chain controls, and cost-optimization.md records the move. Do not look for them in a low Cost score.

Note: the compute-cost heavyweights β€” Graviton, Spot, and Extended Support β€” are handled in cost-analysis.md as narrative savings opportunities, not scored pillar questions, because they are recommendations rather than pass/fail controls.