<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>JSTGTECH</title><description>JSTGTECH — writing, projects, and things I build with technology.</description><link>https://jstgtech.com/</link><item><title>Cloud roundup: macOS Screen Sharing bug now under attack</title><link>https://jstgtech.com/blog/2026-08-16-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-16-cloud-roundup/</guid><description>A patched macOS Screen Sharing flaw is being exploited to plant crypto miners, a Windows Defender bypass has no fix yet, and EC2 gets built-in app health checks.</description><pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Two security items today, both worth checking against your own fleet rather than waiting for the next patch cycle, plus an AWS monitoring feature that quietly kills a homegrown workaround a lot of us are running.&lt;/p&gt;
&lt;h2&gt;macOS Screen Sharing flaw is being exploited to plant Monero miners&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CVE-2026-65400&lt;/strong&gt;, a bug in macOS&apos;s built-in Screen Sharing (VNC over TCP 5900) that let a network attacker connect without valid credentials, is now under active exploitation. The Netherlands&apos; NCSC says it&apos;s seen abuse on multiple systems with port 5900 open to the internet — in every case the attacker landed root and dropped a Monero miner (&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/hackers-exploit-macos-screen-sharing-flaw-to-deploy-monero-miner/&quot;&gt;BleepingComputer&lt;/a&gt;). Apple actually fixed this back on August 6 in macOS Tahoe 26.6.1, Sequoia 15.7.9, and Sonoma 14.8.9, but patch uptake on Macs sitting outside MDM is always spotty, and this is exactly the kind of service that gets enabled once for a support session and never turned off. If you manage any Macs — dev workstations, kiosk boxes, build machines — check whether Screen Sharing is on (System Settings → General → Sharing) and either disable it or confirm the update is installed. A crypto miner is the visible symptom; unauthenticated root access is the actual problem.&lt;/p&gt;
&lt;h2&gt;ShieldBreak: an unpatched Windows Defender bypass with a public PoC&lt;/h2&gt;
&lt;p&gt;Researcher Nightmare Eclipse published a technique called ShieldBreak that defeats a fix Microsoft shipped for an earlier Defender flaw (RoguePlanet), using a user-mode callback hook to tamper with file contents mid-scan via the Cloud Filter API (&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/new-microsoft-defender-shieldbreak-zero-day-grants-system-privileges/&quot;&gt;BleepingComputer&lt;/a&gt;). It affects Windows 10, Windows 11 25H2, and Windows Server, the PoC reportedly hits 100% on tested systems, and there&apos;s no CVE or patch yet — Microsoft says it&apos;s still investigating. It requires Defender to be enabled and running, so this isn&apos;t remote-code-execution-from-nothing, but it is a working local privilege-escalation path with no fix on the horizon. Nothing to patch today, but worth flagging to whoever owns endpoint security on your Windows fleet so it&apos;s on their radar before a patch lands, not after.&lt;/p&gt;
&lt;h2&gt;EC2 gets built-in application health checks&lt;/h2&gt;
&lt;p&gt;Amazon EC2 now has Application Status Checks — instance-level monitoring that periodically hits an HTTP/HTTPS port and path you define and confirms it returns the response code you expect, on a 60-second interval (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-ec2-application-status-checks/&quot;&gt;AWS&lt;/a&gt;). It sits alongside the existing infrastructure status checks and plugs straight into Auto Scaling, which can replace an instance whose app-level check fails even if the instance itself looks perfectly healthy — a stopped web server, a crashed Docker daemon, a broken app dependency. If you&apos;ve ever bolted a sidecar or a custom CloudWatch alarm onto an ASG just to catch &quot;the instance is up but the app is dead,&quot; this replaces that homegrown setup with a native, tag-or-instance-ID-scoped check. Available in every commercial region plus GovCloud, no extra infrastructure required.&lt;/p&gt;
&lt;h2&gt;Also worth a look&lt;/h2&gt;
&lt;p&gt;AWS Direct Connect has had degraded connectivity since 13:17 UTC yesterday for customers connected at the Equinix FR5 facility in Frankfurt — a co-location partner facility issue, not an AWS-side fault, with AWS recommending VPN failover in the interim. If you&apos;ve got a DX connection through FR5 and no redundant path through another location, that&apos;s worth checking on today.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;Neither security item needs a fire drill, but both are worth five minutes: confirm Screen Sharing is off or patched on any internet-reachable Mac, and make sure Windows Defender&apos;s ShieldBreak gap is on your endpoint team&apos;s watchlist. The EC2 health check feature is the one to actually go try — it&apos;s a straightforward win if you&apos;re currently monitoring app health with anything homegrown.&lt;/p&gt;
</content:encoded></item><item><title>Cutting NAT gateway costs with VPC endpoints that actually help</title><link>https://jstgtech.com/blog/2026-08-16-vpc-endpoints-nat-gateway-cost/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-16-vpc-endpoints-nat-gateway-cost/</guid><description>How gateway and interface VPC endpoints replace NAT gateway traffic for AWS API calls, what they cost instead, and which traffic still has to go through NAT.</description><pubDate>Sun, 16 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A NAT gateway bills $0.045/hour just to exist (about $32/month per AZ) plus
$0.045/GB processed, in every region, for every private subnet that needs
outbound internet access. Most of that traffic, on a typical workload, isn&apos;t
actually going to the internet — it&apos;s a Lambda function calling S3, an ECS
task writing to DynamoDB, or an EC2 instance pulling a Secrets Manager value,
all routed out through NAT and back into AWS because nothing told the VPC a
shorter path existed. VPC endpoints are that shorter path. They don&apos;t replace
NAT gateways outright, but on most accounts they eliminate the majority of
the traffic NAT was ever pushing.&lt;/p&gt;
&lt;h2&gt;Two different mechanisms, and it matters which one you reach for&lt;/h2&gt;
&lt;p&gt;&quot;VPC endpoint&quot; covers two unrelated implementations that happen to share a
name:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gateway endpoints&lt;/strong&gt; — S3 and DynamoDB only. A gateway endpoint is a
route table entry, not a network interface. Traffic to the service&apos;s
address range is routed directly within AWS&apos;s network instead of out
through the internet gateway or NAT. There&apos;s no hourly charge and no
per-GB charge — it&apos;s free.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Interface endpoints&lt;/strong&gt; (AWS PrivateLink) — everything else: DynamoDB (as
an alternative), Secrets Manager, SSM, SQS, SNS, ECR, CloudWatch Logs,
Bedrock, and most other AWS services. An interface endpoint provisions an
elastic network interface with a private IP &lt;strong&gt;in your subnet&lt;/strong&gt;, and AWS
gives it a DNS name that resolves in place of the public service endpoint
(when you enable private DNS). These cost $0.01/hour &lt;strong&gt;per AZ&lt;/strong&gt; you deploy
into, plus $0.01/GB processed — not free, but usually far cheaper than the
NAT traffic it replaces once you look at the actual GB numbers.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The pricing gap is why the order of operations matters: turn on gateway
endpoints for S3 and DynamoDB first, always — there&apos;s no cost trade-off to
evaluate, they&apos;re strictly free traffic that used to route through paid NAT.
Interface endpoints are the ones that need a per-service cost comparison
before you add them.&lt;/p&gt;
&lt;h2&gt;Adding a gateway endpoint&lt;/h2&gt;
&lt;p&gt;Gateway endpoints attach to specific route tables, not the whole VPC, so
private subnets need the association explicitly:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aws ec2 create-vpc-endpoint \
  --vpc-id vpc-0123456789abcdef0 \
  --service-name com.amazonaws.us-east-1.s3 \
  --route-table-ids rtb-0a1b2c3d4e5f6a7b8 rtb-1a2b3c4d5e6f7a8b9 \
  --vpc-endpoint-type Gateway
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In Terraform:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource &quot;aws_vpc_endpoint&quot; &quot;s3&quot; {
  vpc_id            = aws_vpc.main.id
  service_name      = &quot;com.amazonaws.us-east-1.s3&quot;
  vpc_endpoint_type = &quot;Gateway&quot;
  route_table_ids   = [for rt in aws_route_table.private : rt.id]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Check that it actually took effect with a route table describe — you&apos;re
looking for a &lt;code&gt;pl-&lt;/code&gt; (prefix list) destination pointing at the endpoint&apos;s ID,
not a &lt;code&gt;0.0.0.0/0&lt;/code&gt; route to NAT for S3 traffic:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aws ec2 describe-route-tables --route-table-ids rtb-0a1b2c3d4e5f6a7b8 \
  --query &apos;RouteTables[0].Routes[?DestinationPrefixListId!=`null`]&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Adding an interface endpoint&lt;/h2&gt;
&lt;p&gt;Interface endpoints need a subnet placement (one ENI per AZ you list) and a
security group, since they&apos;re a real network interface that other resources
connect to:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource &quot;aws_vpc_endpoint&quot; &quot;secretsmanager&quot; {
  vpc_id              = aws_vpc.main.id
  service_name        = &quot;com.amazonaws.us-east-1.secretsmanager&quot;
  vpc_endpoint_type   = &quot;Interface&quot;
  subnet_ids          = aws_subnet.private[*].id
  security_group_ids  = [aws_security_group.vpc_endpoints.id]
  private_dns_enabled = true
}

resource &quot;aws_security_group&quot; &quot;vpc_endpoints&quot; {
  vpc_id = aws_vpc.main.id
  ingress {
    from_port       = 443
    to_port         = 443
    protocol        = &quot;tcp&quot;
    security_groups = [aws_security_group.app.id]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;private_dns_enabled = true&lt;/code&gt; is what makes this transparent to application
code: the SDK still calls &lt;code&gt;secretsmanager.us-east-1.amazonaws.com&lt;/code&gt;, but
Route 53 Resolver answers with the endpoint&apos;s private IP instead of the
public one, inside the VPC. Nothing in your application config changes —
which is also why it&apos;s easy to add one and not notice it&apos;s doing nothing,
covered below.&lt;/p&gt;
&lt;h2&gt;Where NAT still has to stay&lt;/h2&gt;
&lt;p&gt;VPC endpoints only cover AWS service APIs that have a PrivateLink or gateway
implementation. NAT (or an internet gateway with a public IP, for public
subnets) is still required for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Calls to &lt;strong&gt;non-AWS third-party APIs&lt;/strong&gt; — Stripe, Datadog, npm/PyPI
registries during a build, any SaaS webhook target. There&apos;s no PrivateLink
endpoint for the general internet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS services without a PrivateLink endpoint&lt;/strong&gt; in your region yet — check
the &lt;a href=&quot;https://docs.aws.amazon.com/vpc/latest/privatelink/aws-services-privatelink-support.html&quot;&gt;AWS PrivateLink service list&lt;/a&gt;
per-region before assuming coverage; newer or smaller services lag.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-region calls to AWS services&lt;/strong&gt; — an interface endpoint is
regional; a us-east-1 subnet calling an S3 bucket in eu-west-1 through a
gateway endpoint still needs a path out, because the endpoint only covers
same-region traffic patterns for most services (S3 gateway endpoints are
a partial exception via cross-region access points, but don&apos;t assume it
works until you&apos;ve checked the specific service).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Don&apos;t decommission your NAT gateway because you added endpoints for your
top three services — audit what&apos;s actually calling out first (see below),
because the leftover traffic is usually smaller but never zero.&lt;/p&gt;
&lt;h2&gt;Finding out what&apos;s actually costing you, before you guess&lt;/h2&gt;
&lt;p&gt;Don&apos;t add endpoints speculatively for every service AWS offers — at
$0.01/hour/AZ each, a dozen unused interface endpoints across 3 AZs is
$26/month for nothing. VPC Flow Logs tell you what&apos;s actually flowing
through NAT right now. Enable them on the NAT gateway&apos;s ENI (or the whole
VPC) and query the destination:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fields dstAddr, bytes
| filter srcAddr like /^10\./
| stats sum(bytes) as totalBytes by dstAddr
| sort totalBytes desc
| limit 20
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cross-reference the top destination IPs against AWS&apos;s published &lt;a href=&quot;https://ip-ranges.amazonaws.com/ip-ranges.json&quot;&gt;IP address
ranges&lt;/a&gt; for your region —
&lt;code&gt;jq &apos;.prefixes[] | select(.region==&quot;us-east-1&quot;) | .service&apos; ip-ranges.json&lt;/code&gt;
gives you the service name per CIDR block. If a destination IP resolves to
&lt;code&gt;DYNAMODB&lt;/code&gt; or &lt;code&gt;S3&lt;/code&gt;, that traffic is a candidate for a free gateway endpoint
right now. If it resolves to &lt;code&gt;SECRETSMANAGER&lt;/code&gt; or &lt;code&gt;ECR&lt;/code&gt; and represents a
meaningful share of your NAT &lt;code&gt;bytes&lt;/code&gt; total, run the $0.01/GB endpoint cost
against what that GB volume currently costs at NAT&apos;s $0.045/GB — the
endpoint usually wins by a wide margin, but a low-traffic service isn&apos;t
worth the flat hourly charge across every AZ, since that&apos;s a &lt;strong&gt;fixed&lt;/strong&gt; cost
you pay whether or not it&apos;s used, unlike NAT&apos;s per-GB-only marginal cost for
that same traffic.&lt;/p&gt;
&lt;h2&gt;The gotcha: adding the endpoint doesn&apos;t guarantee it gets used&lt;/h2&gt;
&lt;p&gt;An interface endpoint with private DNS enabled changes DNS resolution, but
only for resolvers that actually query the VPC&apos;s Route 53 Resolver. Three
ways this silently fails to redirect traffic, leaving you paying for both
the endpoint and unchanged NAT usage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;A custom DNS server&lt;/strong&gt; configured in the VPC&apos;s DHCP options set that
doesn&apos;t forward to the AWS-provided &lt;code&gt;.2&lt;/code&gt; resolver — the private DNS record
never gets seen, so the SDK still resolves the public IP and routes out
through NAT anyway.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A hardcoded regional or FIPS endpoint URL&lt;/strong&gt; in application config
(&lt;code&gt;https://s3.dualstack.us-east-1.amazonaws.com&lt;/code&gt; or a client explicitly
configured with a non-default endpoint) bypasses the standard hostname
the private DNS record matches.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cached negative DNS lookups&lt;/strong&gt; from before the endpoint existed — some
container base images or runtimes cache resolver failures more
aggressively than successes; a task that started before the endpoint went
live may need a restart, not just time, to pick up the new record.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Confirm it&apos;s actually working, don&apos;t assume it: &lt;code&gt;dig&lt;/code&gt; the service hostname
from inside a resource in the private subnet and check the answer is a
private (10.x/172.16.x/192.168.x) address, then watch the endpoint&apos;s own
CloudWatch metrics (&lt;code&gt;BytesProcessed&lt;/code&gt; on the interface endpoint) climb while
the NAT gateway&apos;s &lt;code&gt;BytesOutToDestination&lt;/code&gt; correspondingly flattens. If the
endpoint&apos;s traffic metric stays near zero after deployment, something in
the DNS resolution path above is still routing around it.&lt;/p&gt;
&lt;h2&gt;What to actually do&lt;/h2&gt;
&lt;p&gt;Turn on S3 and DynamoDB gateway endpoints everywhere, immediately — they&apos;re
free and there&apos;s no scenario where they make things worse. For everything
else, pull a week of VPC Flow Logs against the NAT gateway, resolve the top
destination IPs against AWS&apos;s IP ranges, and add interface endpoints only
for the services showing real GB volume, checking each one actually took
over the traffic afterward rather than assuming the Terraform apply was
enough. On a typical account this drops NAT gateway processing charges
sharply without touching NAT&apos;s role for genuine third-party internet
traffic, which is the traffic it was actually built for.&lt;/p&gt;
</content:encoded></item><item><title>Cloud roundup: S3 finally names the policy that denied you</title><link>https://jstgtech.com/blog/2026-08-14-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-14-cloud-roundup/</guid><description>AWS S3 access-denied errors now name the exact policy ARN, Client VPN gets a scriptable CLI, and OpenAI ships authorized offensive-security models on Bedrock.</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Quieter news day, no fresh KEV entries or breaking CVEs in the last 24 hours — so today&apos;s roundup is AWS-only, and it&apos;s a genuinely useful batch: a debugging quality-of-life fix, an infra-as-code-friendly VPN client, and Bedrock picking up dedicated offensive-security models.&lt;/p&gt;
&lt;h2&gt;S3 access-denied errors now name the exact policy ARN&lt;/h2&gt;
&lt;p&gt;Amazon S3 now includes the specific IAM or AWS Organizations policy ARN in HTTP 403 responses for explicit-deny cases — across SCPs, RCPs, identity-based policies, session policies, and permission boundaries (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/s3-additional-policy-details-access-denied-error-messages/&quot;&gt;AWS&lt;/a&gt;). If you&apos;ve ever gotten a bare &quot;Access Denied&quot; and had to manually walk every SCP and permission boundary in the chain to find the one deny statement, this removes that entire step — the error just tells you which policy did it. No opt-in, no cost, live in every region including GovCloud and China. This is the kind of change that saves real debugging time without anyone having to change a line of code.&lt;/p&gt;
&lt;h2&gt;AWS Client VPN ships a scriptable CLI and centralized policy controls&lt;/h2&gt;
&lt;p&gt;The Client VPN desktop app got a rebuild (v6.0.x, OpenVPN3-based) that adds a CLI with full feature parity to the GUI, so VPN connections can be scripted into CI pipelines and IaC workflows instead of requiring a human to click through the app (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/aws-client-vpn-cli/&quot;&gt;AWS&lt;/a&gt;). It also adds admin controls to scope VPN profiles to specific users or push a global profile to every device, plus faster connection establishment on Windows, macOS, and Linux. It&apos;s a free upgrade, backward-compatible with existing endpoints. Worth grabbing if you&apos;ve ever had to walk a remote contractor through manually configuring VPN client settings — that&apos;s now something you can push as a managed profile instead.&lt;/p&gt;
&lt;h2&gt;Bedrock adds OpenAI&apos;s Daybreak Red and Blue for authorized security work&lt;/h2&gt;
&lt;p&gt;AWS added two purpose-built cybersecurity models to Bedrock: Daybreak Blue (GPT-5.6 Sol) for defensive workflows like vulnerability discovery, detection engineering, and incident response, and Daybreak Red (GPT-5.6 Cyber) for advanced authorized work — vulnerability research, exploit reproduction, mitigation development (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/openai-daybreak-red-and-blue-on-amazon-bedrock/&quot;&gt;AWS&lt;/a&gt;). Access isn&apos;t self-serve — it requires enrolling in Daybreak access through OpenAI or your AWS account team, and it&apos;s US East (N. Virginia) only for now. Inference data isn&apos;t used for training and isn&apos;t shared back to OpenAI by default. If your security team has been prototyping LLM-assisted detection engineering or authorized pentest tooling with general-purpose models, this is worth evaluating once you can get enrolled — it&apos;s a narrower, more accountable access model than &quot;give the whole team an API key.&quot;&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;Nothing urgent to patch today. The S3 error message change is the one to actually notice — it&apos;ll quietly save you time the next time you&apos;re chasing down a deny across a dozen SCPs.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: Amazon Cognito user pools vs identity pools</title><link>https://jstgtech.com/blog/2026-08-14-amazon-cognito/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-14-amazon-cognito/</guid><description>Amazon Cognito is two different services wearing one name — user pools for authentication, identity pools for AWS credentials — and the pricing tiers that trip teams up.</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Half the confusion I&apos;ve seen around &lt;strong&gt;Cognito&lt;/strong&gt; isn&apos;t about how it works —
it&apos;s that &quot;Cognito&quot; is the marketing name for two separate services that
happen to share a console tab. Get the two confused and you either build
an auth flow that can&apos;t touch AWS resources, or wire up AWS credentials
for users who were never actually authenticated.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;User pools&lt;/strong&gt; are the authentication half: a managed user directory that
handles sign-up, sign-in, password policies, MFA, and email/SMS
verification, and issues &lt;strong&gt;JWTs&lt;/strong&gt; (ID, access, and refresh tokens) on
successful login. They can front their own hosted UI or federate to
social IdPs and SAML/OIDC providers, so &quot;login with Google&quot; and
&quot;login with your corporate Okta&quot; both terminate in the same user pool.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Identity pools&lt;/strong&gt; (Cognito Federated Identities) are the authorization
half: given a token — from a Cognito user pool, a social IdP, SAML, or
even an unauthenticated &quot;guest&quot; request — an identity pool exchanges it
for &lt;strong&gt;temporary AWS credentials via STS&lt;/strong&gt;, scoped by an IAM role. That&apos;s
the piece that lets a mobile app upload directly to S3 or call DynamoDB
without a backend in the middle, using credentials that expire instead of
a long-lived API key baked into the app.&lt;/p&gt;
&lt;p&gt;The two compose but aren&apos;t interchangeable: a user pool alone gives you
&quot;who is this person,&quot; not &quot;what can they touch in AWS.&quot; An identity pool
alone will happily hand out credentials to unauthenticated guests if you
let it — it doesn&apos;t do authentication itself, it just brokers whatever
token you hand it.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You skip building and hardening your own auth service.&lt;/strong&gt; Password
hashing, MFA enrollment, account recovery, and JWT issuance are the kind
of thing that&apos;s deceptively easy to get 90% right and dangerously easy
to get the last 10% wrong (timing attacks on password comparison,
token replay, session fixation). Cognito&apos;s user pools cover that
surface area so it&apos;s not your team&apos;s to maintain.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct-to-AWS access without a backend proxy.&lt;/strong&gt; A static site or
mobile app can let an authenticated user write straight to an S3 prefix
scoped to their identity (&lt;code&gt;${cognito-identity.amazonaws.com:sub}&lt;/code&gt; in the
IAM policy) instead of routing every upload through an API Gateway +
Lambda pair whose only job is forwarding bytes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Federation is handled once, centrally.&lt;/strong&gt; Add a SAML IdP or a social
login provider to the user pool and every app using it gets that login
option — you&apos;re not reimplementing OAuth handshakes per client.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where it goes wrong in practice&lt;/h2&gt;
&lt;p&gt;The tier structure is the sharpest edge. Cognito&apos;s current pricing splits
user pools into &lt;strong&gt;Lite, Essentials, and Plus&lt;/strong&gt; feature plans, and features
that look like they should be table stakes — SAML/OIDC federation,
advanced security features like compromised-credential and
risk-based adaptive authentication — only exist on Essentials or Plus,
billed per &lt;strong&gt;monthly active user (MAU)&lt;/strong&gt;. A team that prototypes on Lite
because the free allowance looks generous, then adds &quot;sign in with our
IdP&quot; for an enterprise customer, discovers that single feature moves the
&lt;em&gt;entire pool&apos;s&lt;/em&gt; MAU count onto the paid tier, not just the new users using
it. Check which tier a feature needs before you promise it in a sprint.&lt;/p&gt;
&lt;p&gt;The other recurring mistake is treating identity pool credentials as a
substitute for real authorization. STS credentials scoped to an
identity-pool IAM role are still full IAM credentials — if the attached
role is too broad, an authenticated (or worse, guest-enabled)
&lt;code&gt;unauthenticated&lt;/code&gt; identity can reach far more than &quot;upload your own
profile picture.&quot; Audit identity pool roles the same way you&apos;d audit any
IAM role handed to untrusted clients, with &lt;code&gt;Condition&lt;/code&gt; blocks scoping
access to the caller&apos;s own identity ID, not just a wide S3 prefix.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Before adding a Cognito feature to a design doc, check the &lt;a href=&quot;https://aws.amazon.com/cognito/pricing/&quot;&gt;feature plan
comparison&lt;/a&gt; for the tier it
actually requires — &quot;advanced security&quot; and third-party federation are
the two that most often get assumed as free defaults and aren&apos;t. And if
you&apos;re issuing AWS credentials via an identity pool, run &lt;code&gt;aws sts&lt;/code&gt;
&lt;code&gt;get-caller-identity&lt;/code&gt; with a test unauthenticated identity&apos;s credentials
before shipping, to confirm guest access is scoped as tightly as you
think it is rather than as tightly as you meant it to be.&lt;/p&gt;
</content:encoded></item><item><title>The Capital One breach: an SSRF bug into 100M records</title><link>https://jstgtech.com/blog/2026-08-14-imds-ssrf-cloud-metadata/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-14-imds-ssrf-cloud-metadata/</guid><description>A misconfigured WAF and an SSRF bug let an attacker reach the AWS metadata service and steal role credentials — exposing 100M+ Capital One records.</description><pubDate>Fri, 14 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In March 2019, an attacker used a server-side request forgery (SSRF) bug
in a misconfigured web application firewall to reach a single internal
URL — &lt;code&gt;http://169.254.169.254/&lt;/code&gt; — running behind a Capital One-hosted
application in AWS. That one request was enough to pull temporary IAM
credentials for a role with broad S3 read access, and from there exfiltrate
more than 100 million credit applications and 140,000 Social Security
numbers (&lt;a href=&quot;https://www.justice.gov/usao-wdwa/pr/seattle-tech-worker-arrested-data-theft-involving-large-capital-one-data-breach&quot;&gt;DOJ indictment&lt;/a&gt;). No malware, no phishing, no zero-day in AWS
itself — just a web app that could be tricked into fetching an internal
URL on the attacker&apos;s behalf, and an internal URL that handed over live
credentials to anyone who asked. Capital One paid an $80 million OCC fine
and settled a $190 million class action over it (&lt;a href=&quot;https://www.reuters.com/business/finance/capital-one-pay-190-million-settle-lawsuit-over-2019-data-breach-2022-09-13/&quot;&gt;Reuters&lt;/a&gt;). Seven years
later, IMDSv1 — the version that made this possible — is still the
default reachable endpoint on plenty of EC2 instances, because &quot;enabled&quot;
is not the same as &quot;enforced.&quot;&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;Every EC2 instance can reach a link-local address, &lt;code&gt;169.254.169.254&lt;/code&gt;,
which serves the Instance Metadata Service (IMDS) — instance ID, AMI
info, user-data, and critically, temporary security credentials for
whatever IAM role is attached to the instance. It&apos;s how an EC2 instance
gets AWS credentials without anyone hardcoding a key. In the original
version of that service, IMDSv1, the endpoint answers any plain HTTP GET
request with no authentication step at all — if a request reaches that
IP from the instance, it gets an answer. That design was fine as long as
nothing running on the box could be tricked into making arbitrary
outbound requests on an attacker&apos;s behalf. But web applications routinely
do exactly that: fetch a URL from a query parameter, proxy a request,
render a remote image, validate a webhook. The former engineer charged in
the case had reportedly discovered a misconfigured ModSecurity WAF rule
in front of a Capital One web application that could be coerced into
requesting an arbitrary URL and returning the response — a textbook
SSRF (&lt;a href=&quot;https://www.justice.gov/usao-wdwa/pr/seattle-tech-worker-arrested-data-theft-involving-large-capital-one-data-breach&quot;&gt;indictment&lt;/a&gt;). Point that SSRF at
&lt;code&gt;169.254.169.254/latest/meta-data/iam/security-credentials/&amp;lt;role-name&amp;gt;&lt;/code&gt;
and IMDSv1 hands back an access key, secret key, and session token for
whatever role the instance was running as — no extra proof that the
request came from a legitimate, non-hijacked process.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;The stolen role&apos;s permissions, not the SSRF bug itself, set the ceiling
on the damage. In this case the role had read access to S3 buckets used
for a range of Capital One&apos;s credit-card and loan-application data,
compiled from six years of applications going back to 2005 — 106 million
individuals across the US and Canada, including Social Security numbers,
bank account numbers, and credit scores (&lt;a href=&quot;https://www.capitalone.com/digital/facts2019/&quot;&gt;Capital One&apos;s disclosure&lt;/a&gt;).
Stolen IMDS credentials are functionally indistinguishable from
legitimate ones to everything downstream — CloudTrail logs the calls as
coming from the role, IAM enforces exactly the policy attached to that
role, and nothing about the request signature reveals it originated from
an SSRF payload rather than the application itself. That&apos;s the general
shape of every IMDS-credential-theft incident: the SSRF or RCE is the
entry point, but the actual damage is bounded entirely by how
over-permissioned the instance role was. A role scoped to one bucket
prefix limits an attacker to that prefix; a role with &lt;code&gt;s3:*&lt;/code&gt; across the
account hands over the account&apos;s data.&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;AWS&apos;s direct answer, shipped in November 2019 a few months after this
breach became public, is &lt;strong&gt;IMDSv2&lt;/strong&gt; — it requires a session to be
established with a &lt;code&gt;PUT&lt;/code&gt; request that returns a token, and every
subsequent metadata request must carry that token in a header
(&lt;a href=&quot;https://aws.amazon.com/blogs/security/defense-in-depth-open-firewalls-reverse-proxies-ssrf-vulnerabilities-ec2-instance-metadata-service/&quot;&gt;AWS&lt;/a&gt;).
Critically, IMDSv2&apos;s &lt;code&gt;PUT&lt;/code&gt; requires HTTP with no redirects followed and
sets a configurable hop limit (default 1) on the response&apos;s TTL — which
means a proxy or SSRF payload relaying a simple &lt;code&gt;GET&lt;/code&gt; typically can&apos;t
complete the handshake or forward the token past one network hop, while a
process running natively on the instance can. That single change would
have broken the Capital One attack path as described. Enforcement is the
part teams skip: IMDSv2 has been opt-in at the instance level since
launch, so an unpatched fleet stays exploitable indefinitely unless
someone acts. Three concrete steps: set &lt;code&gt;HttpTokens: required&lt;/code&gt; (not just
&lt;code&gt;optional&lt;/code&gt;) on every instance and in every launch template, ideally via
an &lt;strong&gt;AWS Config rule&lt;/strong&gt; (&lt;code&gt;ec2-imds-v2-check&lt;/code&gt;) that flags drift; use an
**SCP or Config rule to deny new instance launches without IMDSv2
enforced**, since a one-time fleet fix doesn&apos;t stop the next Terraform
apply from reintroducing IMDSv1; and independently, scope every instance
role to least privilege — the hop-limit defense is layer two, not a
replacement for making sure a stolen credential is only ever worth as
much as the narrowest policy you could get away with attaching.&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;This wasn&apos;t an AWS vulnerability — IMDS behaved exactly as designed, and
the WAF misconfiguration was Capital One&apos;s, not Amazon&apos;s. But it&apos;s the
canonical case study for why cloud metadata endpoints deserve the same
scrutiny as any credential store: anything that can make an outbound
HTTP request from inside your VPC is a potential path to
&lt;code&gt;169.254.169.254&lt;/code&gt;, and IMDSv1&apos;s design assumed that path would never
exist. IMDSv2&apos;s token-and-hop-limit model closes the SSRF-relay case
specifically, but it only helps the instances where someone actually
flipped &lt;code&gt;HttpTokens&lt;/code&gt; to &lt;code&gt;required&lt;/code&gt; — which is why the Config rule and the
SCP matter as much as the setting itself. If your fleet still allows
&lt;code&gt;HttpTokens: optional&lt;/code&gt;, the difference between &quot;we have IMDSv2 available&quot;
and &quot;we&apos;re actually protected&quot; is one unenforced launch template away
from mattering.&lt;/p&gt;
</content:encoded></item><item><title>Cloud roundup: 153GB LiteLLM breach exposes cloud secrets</title><link>https://jstgtech.com/blog/2026-08-13-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-13-cloud-roundup/</guid><description>A leaked 153GB credential archive ties March&apos;s LiteLLM breach to 2,488 orgs including AWS and Cisco, plus new EKS control-plane and OpenSearch pricing changes.</description><pubDate>Thu, 13 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The big story today isn&apos;t a new breach — it&apos;s the bill coming due on an old one. A researcher got hold of the actual haul from March&apos;s LiteLLM supply-chain attack, and the scale is worse than the original disclosure suggested. On the AWS side, two changes worth planning around: more control over your EKS control plane, and a pricing shift that quietly doubles some OpenSearch costs.&lt;/p&gt;
&lt;h2&gt;153GB of stolen credentials surface from the March LiteLLM attack&lt;/h2&gt;
&lt;p&gt;Back in March, attackers compromised the Trivy scanner and used it to slip two malicious versions of LiteLLM onto PyPI for about 40 minutes — long enough for a &lt;code&gt;.pth&lt;/code&gt; file to start harvesting secrets from every CI run that installed it. Hudson Rock has now obtained and analyzed the actual stolen archive: 153GB, 433,909 files, with 118,829 CI runner dumps attributable to 2,488 corporate domains — including AWS, Cisco, Samsung, Salesforce, Microsoft, and dozens of other large enterprises (&lt;a href=&quot;https://www.helpnetsecurity.com/2026/08/13/litellm-breach-stolen-credentials-leak/&quot;&gt;Help Net Security&lt;/a&gt;). The dumps include AWS secret access keys, Salesforce client secrets, Slack signing secrets, and Azure environment variables captured mid-pipeline. The uncomfortable part: any credential that was live in a CI runner during that 40-minute window is still valid until someone actively rotates it — removing the malicious package did nothing to invalidate what it already stole. If you had LiteLLM anywhere in a CI/CD dependency tree back in March, this is worth a credential audit today, not a &quot;we&apos;ll get to it&quot; item — check for AWS keys, Slack tokens, and cloud provider secrets that predate March 19 and haven&apos;t been rotated since.&lt;/p&gt;
&lt;h2&gt;Amazon EKS now lets you tune scheduler, controller manager, and API server parameters&lt;/h2&gt;
&lt;p&gt;AWS added support for configuring Kubernetes control plane parameters directly on EKS — scheduler behavior, controller-manager settings, and API server options that used to require self-managing the control plane to touch (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-eks-control-plane-configuration-parameters/&quot;&gt;AWS&lt;/a&gt;). The scheduler example AWS calls out is a good one: switching the node resource fit strategy from the default &lt;code&gt;LeastAllocated&lt;/code&gt; (spread pods across nodes) to &lt;code&gt;MostAllocated&lt;/code&gt; (pack nodes tightly) can meaningfully cut node count for workloads where you don&apos;t need the spare headroom. It&apos;s available in every region EKS runs in, no migration required. Worth a look if you&apos;ve ever wanted more control over pod-placement or autoscaling responsiveness on EKS without giving up the managed control plane.&lt;/p&gt;
&lt;h2&gt;OpenSearch/Elasticsearch Extended Support surcharge is about to double&lt;/h2&gt;
&lt;p&gt;AWS extended the security-patch window for older Elasticsearch (1.5–7.8) and OpenSearch (1.0–1.2, 2.3–2.9) versions through November 7, 2027 — but from November 7, 2026, the Extended Support surcharge jumps to equal your instance cost, effectively doubling the price of running those old versions (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-opensearch-service-additional-upgrade-runway-support-dates/&quot;&gt;AWS&lt;/a&gt;). If you&apos;ve got domains sitting on a pre-7.9 Elasticsearch or early-2.x OpenSearch version because upgrading was never urgent, this is the forcing function: budget for either the upgrade or the doubled bill before November.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;The LiteLLM fallout is the one to act on today — a credential rotation check costs you an hour and closes a door that&apos;s been open since March. The AWS items are both &quot;plan now, act before the deadline&quot; — EKS control plane tuning whenever you get to it, OpenSearch versions before the November surcharge hits.&lt;/p&gt;
</content:encoded></item><item><title>Cloud roundup: Metabase CVSS 10 SQLi confirmed exploited</title><link>https://jstgtech.com/blog/2026-08-12-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-12-cloud-roundup/</guid><description>A CVSS 10 Metabase SQLi is now confirmed exploited and in CISA KEV, plus an exploited Cisco ASA/FTD DoS bug and Microsoft&apos;s Patch Tuesday zero-day.</description><pubDate>Wed, 12 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Another security-heavy day: the Metabase bug I flagged Monday as a zero-day with no CVE now has one and is confirmed under active exploitation, Cisco&apos;s VPN appliances picked up an exploited DoS flaw, and Microsoft&apos;s August Patch Tuesday landed with an already-exploited zero-day of its own.&lt;/p&gt;
&lt;h2&gt;Metabase&apos;s CVSS 10 SQL injection now has a CVE and is in CISA KEV&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CVE-2026-72898&lt;/strong&gt; is an unauthenticated SQL injection in Metabase&apos;s password-reset endpoint (&lt;code&gt;POST /api/session/reset_password&lt;/code&gt;) that lets an attacker inject arbitrary SQL into the application database and walk away with admin access — no auth, no user interaction required, CVSS 10.0 (&lt;a href=&quot;https://bishopfox.com/blog/critical-sql-injection-in-metabase-via-password-reset-cve-2026-72898&quot;&gt;Bishop Fox&lt;/a&gt;). CISA added it to the Known Exploited Vulnerabilities catalog on August 11 (&lt;a href=&quot;https://www.cisa.gov/news-events/alerts/2026/08/11/cisa-adds-three-known-exploited-vulnerabilities-catalog&quot;&gt;CISA&lt;/a&gt;). It affects a wide version range — 0.58 through 0.63.4 and the matching Enterprise 1.x builds — so if you&apos;re running self-hosted Metabase anywhere, including a &quot;just for internal dashboards&quot; instance, patch it now and don&apos;t wait for the next maintenance window. Admin takeover on a BI tool usually means access to every data source it&apos;s connected to, which for most of us is production databases and warehouses.&lt;/p&gt;
&lt;h2&gt;Cisco ASA and FTD hit with an actively exploited VPN DoS flaw&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CVE-2026-20349&lt;/strong&gt; (CVSS 8.6) is a heap inspection bug in Cisco Secure Firewall ASA and FTD&apos;s Remote Access SSL VPN service — a crafted, unauthenticated HTTP request can crash the device and force a reload (&lt;a href=&quot;https://thehackernews.com/2026/08/cisco-asa-and-ftd-flaw-exploited-in.html&quot;&gt;The Hacker News&lt;/a&gt;). Cisco confirmed active exploitation and CISA added it to KEV in the same August 11 batch as the Metabase bug, with a federal remediation deadline of August 14. If you&apos;ve got ASA/FTD devices with SSL VPN listeners facing the internet, this is a &quot;patch this week&quot; item — a device that keeps rebooting under attacker control is its own kind of outage, on top of whatever else the attacker is probing for while it&apos;s down.&lt;/p&gt;
&lt;h2&gt;Microsoft&apos;s August Patch Tuesday: ~400 fixes, one already exploited&lt;/h2&gt;
&lt;p&gt;Microsoft&apos;s August Patch Tuesday shipped fixes for roughly 400 vulnerabilities including three zero-days, and confirmed one — &lt;strong&gt;CVE-2026-68820&lt;/strong&gt;, a use-after-free elevation-of-privilege bug in the Windows Ancillary Function Driver for WinSock — is already being exploited in the wild (&lt;a href=&quot;https://www.bleepingcomputer.com/news/microsoft/microsoft-august-2026-patch-tuesday-fixes-400-flaws-3-zero-days/&quot;&gt;BleepingComputer&lt;/a&gt;). AFD.sys bugs are a recurring privilege-escalation path once an attacker has any foothold, so this is the one to prioritize on your Windows fleet — EC2 Windows instances, jump boxes, and anything else running Windows — even though the initial-access vector is elsewhere.&lt;/p&gt;
&lt;h2&gt;Also worth a look&lt;/h2&gt;
&lt;p&gt;Amazon Connect added a performance dashboard for Cases, giving managers case-volume, resolution-trend, and SLA-attainment views without building custom reporting on top of the Cases API — a small but welcome addition if you&apos;re running support workflows through Connect.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;Two unauthenticated, actively-exploited bugs landed in KEV on the same day — Metabase SQLi and the Cisco ASA/FTD DoS — so triage internet-facing instances of either today. The Windows AFD zero-day is a &quot;patch it in the normal cycle, but don&apos;t push the normal cycle out&quot; item.&lt;/p&gt;
</content:encoded></item><item><title>Cloud roundup: New ransomware exploits N-central bug</title><link>https://jstgtech.com/blog/2026-08-11-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-11-cloud-roundup/</guid><description>Microsoft ties new StormEncryptor ransomware to an N-able N-central auth bypass, plus new AWS EC2 health checks and DRS UEFI support for Linux failovers.</description><pubDate>Tue, 11 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&apos;re still triaging the N-central saga from earlier this week, there&apos;s a new wrinkle: it&apos;s now ransomware. Otherwise it&apos;s a lighter AWS day — two operational upgrades worth bookmarking rather than dropping everything for.&lt;/p&gt;
&lt;h2&gt;StormEncryptor ransomware ties back to the N-central auth bypass&lt;/h2&gt;
&lt;p&gt;Microsoft disclosed that Storm-1175, a financially motivated China-linked actor that previously ran Medusa ransomware, has moved to a new strain called StormEncryptor — and the likely entry point is &lt;strong&gt;CVE-2026-18577&lt;/strong&gt;, the N-able N-central authentication bypass CISA added to KEV earlier this week (&lt;a href=&quot;https://thehackernews.com/2026/08/china-linked-hackers-deploy-new.html&quot;&gt;The Hacker News&lt;/a&gt;). Microsoft assesses that CVE-2026-18577 is actually a patch bypass for the earlier CVE-2026-18556, meaning the first fix didn&apos;t fully close the door. Once in, Storm-1175 moves fast — AnyDesk or SimpleHelp for persistence, Advanced IP Scanner for discovery, Mimikatz for credential dumping, then exfiltration and encryption within days of initial access. If you&apos;re an MSP, or downstream of one running N-central, this is the concrete &quot;why&quot; behind all the patch-immediately warnings from earlier this week: confirm you&apos;re on the latest hotfix (not just the first one), and hunt for AnyDesk/SimpleHelp installs you didn&apos;t put there yourself.&lt;/p&gt;
&lt;h2&gt;EC2 gets application-level health checks, not just instance-level&lt;/h2&gt;
&lt;p&gt;AWS launched Application Status Checks for EC2 — you configure a protocol, port, path, and expected response code, and EC2 polls it every 60 seconds, feeding the result into the same status-check signal Auto Scaling already watches (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/amazon-ec2-application-status-checks&quot;&gt;AWS&lt;/a&gt;). It&apos;s live in all commercial regions plus GovCloud. The gap this closes: instance and system status checks tell you the VM is up, not that nginx crashed or the app inside stopped responding — that&apos;s traditionally been a custom health-check script or an ALB target group doing double duty. If you run EC2 outside an ALB, or want faster detection than an ALB&apos;s own health-check interval, this is a built-in way to get unhealthy instances replaced without writing your own watcher.&lt;/p&gt;
&lt;h2&gt;AWS DRS now preserves UEFI boot mode on Linux failover&lt;/h2&gt;
&lt;p&gt;AWS Elastic Disaster Recovery now carries UEFI boot mode through to recovered Linux instances automatically, instead of dropping them into legacy BIOS mode and leaving you to fix boot config after a failover (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/aws-drs-linux-uefi&quot;&gt;AWS&lt;/a&gt;). No configuration change needed, no extra cost. It&apos;s a small thing until the day you actually fail over and discover your UEFI-dependent app won&apos;t boot — worth knowing about before you need it, not during an actual DR event.&lt;/p&gt;
&lt;h2&gt;Also worth a look&lt;/h2&gt;
&lt;p&gt;AWS brought U7in-24TB high-memory EC2 instances to São Paulo for SAP HANA/Oracle-scale workloads, and bumped OpenSearch Serverless from 1,500 to 10,000 collections per collection group — useful if you&apos;re running dense multi-tenant search and were bumping into the old ceiling.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;The N-central story just got a lot more concrete: it&apos;s now actively being used to deploy ransomware, so if you haven&apos;t confirmed your hotfix level, do that today. On the AWS side, the two operational items — EC2 app-level health checks and DRS&apos;s UEFI fix — are both worth turning on now, quietly, before you need them.&lt;/p&gt;
</content:encoded></item><item><title>Cloud roundup: LoadMaster RCE hits its KEV deadline today</title><link>https://jstgtech.com/blog/2026-08-10-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-cloud-roundup/</guid><description>A critical unauthenticated Progress LoadMaster RCE hits its federal KEV remediation deadline today, plus a CVSS 10 Metabase zero-day and AWS supply chain security news.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you run a Progress/Kemp LoadMaster load balancer, today&apos;s the deadline. Beyond that, it&apos;s a quieter day — another max-severity zero-day to know about, a bad update on last week&apos;s N-central saga, and AWS chipping away at supply chain risk.&lt;/p&gt;
&lt;h2&gt;Progress LoadMaster command injection hits its remediation deadline today&lt;/h2&gt;
&lt;p&gt;CISA added &lt;strong&gt;CVE-2026-8037&lt;/strong&gt; (CVSS 9.6), an unauthenticated command injection flaw in Progress (Kemp) LoadMaster, to the KEV catalog on August 7 after eSentire and watchTowr Labs both reported active exploitation attempts — 792 reported attempts by one count (&lt;a href=&quot;https://thehackernews.com/2026/08/progress-kemp-loadmaster-flaw-hits-cisa.html&quot;&gt;The Hacker News&lt;/a&gt;). The bug lives in a function called &lt;code&gt;escape_quotes()&lt;/code&gt; and lets an unauthenticated attacker send unsanitized input to a LoadMaster API endpoint and get arbitrary command execution on the appliance. Progress shipped the fix back in June (GA 7.2.63.2 / LTSF 7.2.54.18), so this is really about finding the load balancers your org forgot to patch. Federal civilian agencies have until &lt;strong&gt;today, August 10&lt;/strong&gt;, to remediate under BOD 26-04 — if you run LoadMaster anywhere with its management API exposed, that&apos;s your action item regardless of whether you&apos;re a federal shop.&lt;/p&gt;
&lt;h2&gt;A CVSS 10 zero-day in Metabase, no CVE yet&lt;/h2&gt;
&lt;p&gt;Metabase — a popular open-source BI/dashboarding tool a lot of teams point at production databases — has a maximum-severity, unauthenticated SQL injection zero-day being actively exploited in the wild, including against Metabase Cloud itself (&lt;a href=&quot;https://thehackernews.com/2026/08/metabase-zero-day-exploited-in-wild.html&quot;&gt;The Hacker News&lt;/a&gt;). An attacker can inject SQL through the app database with no credentials and land admin access, which means the credentials for every database Metabase is connected to are exposed too. It affects the 1.58 through 1.63 release lines; patched builds are out (1.58.24 through 1.63.5). If you can&apos;t patch immediately, block the &lt;code&gt;/api/session/reset_password&lt;/code&gt; endpoint at your edge as a stopgap, then rotate the DB credentials Metabase held once you&apos;re patched — assume they were seen.&lt;/p&gt;
&lt;h2&gt;N-able N-central: the first patch wasn&apos;t enough&lt;/h2&gt;
&lt;p&gt;An update to the N-central story from earlier this week: N-able shipped &lt;strong&gt;Hotfix 2&lt;/strong&gt; after finding attackers who&apos;d exploited CVE-2026-18577 were registering Cloudflare Tunnel connections on compromised managed endpoints to keep access even after the N-central server itself got locked down (&lt;a href=&quot;https://thehackernews.com/2026/08/n-central-attackers-reach-managed.html&quot;&gt;The Hacker News&lt;/a&gt;). If you patched with Hotfix 1 and called it done, it&apos;s worth another look — N-able is now shipping expanded IOCs and a detection template, and explicitly warning that a clean scan doesn&apos;t guarantee you weren&apos;t hit. Good reminder that &quot;authentication bypass&quot; bugs on management platforms deserve a persistence hunt, not just a patch-and-move-on.&lt;/p&gt;
&lt;h2&gt;AWS Security Hub Extended adds supply chain security&lt;/h2&gt;
&lt;p&gt;AWS added supply chain security as the tenth category in Security Hub Extended, with Chainguard and Socket as the curated partners, aimed at catching malicious code in open-source dependencies before it gets built into your app (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/aws-security-hub-extended-adds-supply-chain-security/&quot;&gt;AWS&lt;/a&gt;). It&apos;s pay-as-you-go with no commitment, and findings land in the same OCSF-normalized dashboard as everything else in Security Hub. If you&apos;re already paying for Extended and doing dependency scanning with a separate standalone tool, worth comparing — consolidating that signal into the same place as your other findings is generally a win for whoever&apos;s triaging.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;Patch LoadMaster today if you run one exposed. If you&apos;re on Metabase, patch and rotate credentials — assume compromise until proven otherwise. And if you touched N-central last week, go back and hunt for persistence rather than trusting the first patch.&lt;/p&gt;
</content:encoded></item><item><title>CI/CD for a static site: build, sync, and invalidate</title><link>https://jstgtech.com/blog/2026-08-10-cicd-static-site-s3-cloudfront/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-cicd-static-site-s3-cloudfront/</guid><description>How to structure a GitHub Actions pipeline that builds a static site, syncs it to S3 with correct cache headers, and invalidates CloudFront without overpaying.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Deploying a static site sounds like it should be a solved problem: build it,
copy the files to a bucket, done. In practice the pipeline has three separate
failure modes that don&apos;t show up until production — stale HTML served from
cache, orphaned objects left in the bucket after a rename, and an
invalidation bill that surprises you if you get cache-control wrong. This
site&apos;s own deploy pipeline (&lt;code&gt;.github/workflows/deploy.yml&lt;/code&gt;) hits all three
concerns in about 20 lines, so I&apos;ll use it as the working example rather than
a hypothetical.&lt;/p&gt;
&lt;h2&gt;The three stages, and why order matters&lt;/h2&gt;
&lt;p&gt;A static-site deploy is really three distinct jobs glued together:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt; — turn source into a &lt;code&gt;dist/&lt;/code&gt; directory of static files.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sync&lt;/strong&gt; — reconcile that directory with what&apos;s in S3, including deleting
anything that&apos;s no longer there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Invalidate&lt;/strong&gt; — tell CloudFront&apos;s edge caches to stop serving the old
versions of whatever changed.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;They have to run in that order and each has to fully succeed before the next
starts — a partial sync followed by an invalidation just serves a broken mix
of old and new files faster. Keep them as separate steps (not one giant
shell script) so a failure at step 2 shows up clearly in the Actions log
instead of buried in a 40-line &lt;code&gt;run:&lt;/code&gt; block.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- run: npm run build

- name: Sync to S3
  run: aws s3 sync ./dist &quot;s3://my-bucket&quot; --delete

- name: Invalidate CloudFront
  run: |
    aws cloudfront create-invalidation \
      --distribution-id &quot;$DISTRIBUTION_ID&quot; \
      --paths &quot;/*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s the naive version. It works, but it&apos;s wrong in two specific ways —
neither obvious until you look closely at cache-control and &lt;code&gt;--delete&lt;/code&gt;
together.&lt;/p&gt;
&lt;h2&gt;Sync: cache-control headers aren&apos;t optional&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;aws s3 sync&lt;/code&gt; doesn&apos;t set a useful &lt;code&gt;Cache-Control&lt;/code&gt; header by default — S3
serves objects with no explicit caching directive, which browsers and
CloudFront interpret conservatively. For a static site built by a bundler
(Astro, Vite, Next static export, etc.), you want two very different caching
policies in the same deploy:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fingerprinted assets&lt;/strong&gt; (&lt;code&gt;/_astro/chunk-a1b2c3.js&lt;/code&gt;, hashed CSS, images) —
the filename changes when the content changes, so it&apos;s safe to cache these
&lt;em&gt;forever&lt;/em&gt;. &lt;code&gt;public, max-age=31536000, immutable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTML, sitemap, RSS&lt;/strong&gt; — the URL stays the same (&lt;code&gt;/blog/index.html&lt;/code&gt;) but
the content changes on every deploy. These need &lt;code&gt;max-age=0,&lt;/code&gt;
&lt;code&gt;  must-revalidate&lt;/code&gt; so a browser or CDN edge always re-checks before serving
a cached copy.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can&apos;t set one blanket &lt;code&gt;--cache-control&lt;/code&gt; flag for the whole sync and get
both right, so split it into two sync calls with complementary
&lt;code&gt;--exclude&lt;/code&gt;/&lt;code&gt;--include&lt;/code&gt; filters:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Sync to S3
  run: |
    aws s3 sync ./dist &quot;s3://$BUCKET&quot; \
      --delete \
      --exclude &quot;*.html&quot; \
      --exclude &quot;*.xml&quot; \
      --cache-control &quot;public,max-age=31536000,immutable&quot;

    aws s3 sync ./dist &quot;s3://$BUCKET&quot; \
      --delete \
      --exclude &quot;*&quot; \
      --include &quot;*.html&quot; \
      --include &quot;*.xml&quot; \
      --cache-control &quot;public,max-age=0,must-revalidate&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is exactly what this site&apos;s &lt;code&gt;deploy.yml&lt;/code&gt; does. The &lt;code&gt;--exclude&lt;/code&gt;/
&lt;code&gt;--include&lt;/code&gt; pairs are inverses of each other on purpose — every object in
&lt;code&gt;dist/&lt;/code&gt; is claimed by exactly one of the two calls, never both, never
neither.&lt;/p&gt;
&lt;h2&gt;The &lt;code&gt;--delete&lt;/code&gt; gotcha: it respects filters, both ways&lt;/h2&gt;
&lt;p&gt;Here&apos;s the part that isn&apos;t obvious from the CLI docs&apos; one-line description.
&lt;code&gt;aws s3 sync --delete&lt;/code&gt; removes destination objects that aren&apos;t present in
the source &lt;em&gt;and match the command&apos;s own filters&lt;/em&gt;. It does &lt;strong&gt;not&lt;/strong&gt; delete
everything in the bucket that the source lacks — it only considers objects
within the scope defined by &lt;code&gt;--exclude&lt;/code&gt;/&lt;code&gt;--include&lt;/code&gt; for that specific
invocation.&lt;/p&gt;
&lt;p&gt;That&apos;s exactly why running the two-pass sync above is safe: the first call
(non-HTML) only ever deletes stale non-HTML objects, and the second call
(HTML/XML) only ever deletes stale HTML/XML objects. Neither pass can
accidentally delete the other&apos;s files, because each treats them as excluded
and therefore invisible.&lt;/p&gt;
&lt;p&gt;The gotcha is what happens if your two filter sets &lt;em&gt;aren&apos;t&lt;/em&gt; exact
complements — say you add a new file extension to the build output (a
&lt;code&gt;.webmanifest&lt;/code&gt;, a &lt;code&gt;.txt&lt;/code&gt;) and forget to add it to either pass&apos;s include
list. It won&apos;t get deleted when removed (each &lt;code&gt;--delete&lt;/code&gt; ignores it), but it
also won&apos;t get the cache-control header you intended on either pass — it
silently falls through both filters. Test a rename/removal locally against a
scratch bucket (&lt;code&gt;aws s3 sync --dryrun&lt;/code&gt;) whenever you touch the exclude
patterns, not just when you add new ones.&lt;/p&gt;
&lt;h2&gt;Invalidate: scoped paths vs. wildcard, and the pricing surprise&lt;/h2&gt;
&lt;p&gt;The instinct once you&apos;ve fixed caching is to invalidate narrowly — pass the
exact paths that changed instead of &lt;code&gt;/*&lt;/code&gt;, on the theory that a full
wildcard invalidation is expensive because it touches every object at the
edge. That instinct is backwards for CloudFront specifically, and it&apos;s worth
knowing why before you build a diff-based &quot;only invalidate what changed&quot;
step.&lt;/p&gt;
&lt;p&gt;CloudFront invalidation pricing is **per path string submitted in the
request, not per object matched**. The first 1,000 paths per month are
free; after that it&apos;s $0.005 per path. &lt;code&gt;--paths &quot;/*&quot;&lt;/code&gt; is &lt;em&gt;one&lt;/em&gt; path as far
as billing is concerned, regardless of how many thousands of objects it
actually clears. Compare that to a &quot;smart&quot; pipeline that diffs the build and
submits one path per changed file — a typical content update touching 15
files costs the same order of magnitude as 15 separate wildcard deploys, and
if you ever invalidate per-object on a big rebuild (hundreds of pages) you
can burn through the free tier in a single deploy.&lt;/p&gt;
&lt;p&gt;For a low-traffic personal site or portfolio, &lt;code&gt;/*&lt;/code&gt; on every deploy is both
simpler and, counter-intuitively, usually cheaper than trying to be
clever about scoping. It&apos;s the right default. The one place scoping earns
its complexity is a high-frequency deploy pipeline (many deploys per hour,
e.g. a CMS with instant-publish) where wildcard invalidations would
otherwise queue up and a &lt;code&gt;--paths &quot;/blog/*&quot; &quot;/index.html&quot;&lt;/code&gt; pattern targeting
only the collections that actually changed keeps the queue from backing up —
CloudFront processes invalidations from a single distribution somewhat
serially, and a backlog of full-site wildcards delays the one that matters.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;- name: Invalidate CloudFront
  run: |
    aws cloudfront create-invalidation \
      --distribution-id &quot;$DISTRIBUTION_ID&quot; \
      --paths &quot;/*&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you do scope it, don&apos;t hand-roll the diff from &lt;code&gt;git diff --name-only&lt;/code&gt; —
map source paths to &lt;em&gt;routes&lt;/em&gt;, not files. A change to a shared layout
component invalidates every page that uses it, not the one file that
changed; a naive file-based diff will under-invalidate and leave stale pages
in cache with no error to tell you it happened.&lt;/p&gt;
&lt;h2&gt;Rolling this out&lt;/h2&gt;
&lt;p&gt;Add the two-pass sync and cache-control split first, deploy once, and check
response headers with &lt;code&gt;curl -I&lt;/code&gt; against a fingerprinted asset and against
&lt;code&gt;/&lt;/code&gt; to confirm the immutable/must-revalidate split landed correctly before
you touch the invalidation step. Then switch the invalidation from whatever
ad-hoc scoping you had to &lt;code&gt;/*&lt;/code&gt; and watch a billing cycle — for most personal
and small-business traffic levels you&apos;ll stay inside the free 1,000-path
tier for months. Only reach for path-scoped invalidations once you have
actual evidence (a CloudFront invalidation queue backing up, or genuinely
exceeding the free tier) rather than optimizing against an assumption about
cost that, for this specific service, runs the opposite direction from most
people&apos;s intuition.&lt;/p&gt;
</content:encoded></item><item><title>CloudFront invalidations without nuking your AWS bill</title><link>https://jstgtech.com/blog/2026-08-10-cloudfront-invalidation-strategies/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-cloudfront-invalidation-strategies/</guid><description>How CloudFront invalidation pricing actually works, why fingerprinted asset filenames let you skip it almost entirely, and when surgical invalidation is still required.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If your deploy pipeline ends every push with `aws cloudfront create-invalidation
--paths &quot;/*&quot;`, it works, but it&apos;s the CDN equivalent of clearing your entire
browser cache because one tab was stale. It&apos;s slow to propagate, it costs
money past a fairly small free tier, and — the part people miss — it doesn&apos;t
actually need to exist for most of your assets if you name your files right.
This site&apos;s own deploy (&lt;code&gt;deploy.yml&lt;/code&gt;) does a scoped invalidation of exactly
two paths, not a wildcard, and here&apos;s the reasoning and the mechanics behind
that choice.&lt;/p&gt;
&lt;h2&gt;How invalidation pricing actually works&lt;/h2&gt;
&lt;p&gt;CloudFront gives you **1,000 free invalidation path requests per month, per
account**. Past that, each additional path costs $0.005. The part that trips
people up is what counts as &quot;one path&quot;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;/*&lt;/code&gt; — a single wildcard path — counts as &lt;strong&gt;one path&lt;/strong&gt;, no matter how many
objects it matches at the edge. Nuking your entire distribution with one
wildcard call costs the same as invalidating one specific file.&lt;/li&gt;
&lt;li&gt;An explicit list of paths in a single &lt;code&gt;create-invalidation&lt;/code&gt; call is billed
&lt;strong&gt;per path in the list&lt;/strong&gt;. &lt;code&gt;aws cloudfront create-invalidation --paths&lt;/code&gt;
&lt;code&gt;  &quot;/index.html&quot; &quot;/blog/index.html&quot; &quot;/rss.xml&quot;&lt;/code&gt; bills three paths, not one call.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So a single &lt;code&gt;/*&lt;/code&gt; is actually the &lt;em&gt;cheapest&lt;/em&gt; way to invalidate by path-count —
the trap isn&apos;t cost from the wildcard itself, it&apos;s what a wildcard does
operationally: it forces CloudFront to revalidate every object in the
distribution against the origin on the next request, which means a burst of
origin requests to S3 right after every deploy, and it invalidates objects
that never changed. On a low-traffic personal site that&apos;s harmless. On
anything with real traffic or a slow/rate-limited origin, a full-distribution
wildcard after every deploy is the thing that actually costs you — in origin
load and in cache-miss latency for visitors who hit the edge in the seconds
after the invalidation lands, not in the $0.005-per-path line item.&lt;/p&gt;
&lt;p&gt;The free tier resets monthly and is shared across all distributions in the
account, so if you run several sites or a multi-tenant setup off one AWS
account, a chatty pipeline on one distribution eats the free allowance for
all of them.&lt;/p&gt;
&lt;h2&gt;The real fix: stop invalidating, start fingerprinting&lt;/h2&gt;
&lt;p&gt;Invalidation is a workaround for a caching mistake: telling CloudFront to
cache a URL for a long time when the content behind that URL can change.
Fingerprinted (content-hashed) filenames remove the mistake instead of
compensating for it. Astro&apos;s build already does this for you — run &lt;code&gt;npm run&lt;/code&gt;
&lt;code&gt;build&lt;/code&gt; and look at &lt;code&gt;dist/_astro/&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;_astro/client.a1b2c3d4.js
_astro/index.e5f6a7b8.css
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hash is derived from the file&apos;s content. Change one character of source,
the hash changes, the URL changes. Because the URL is different, there&apos;s
nothing to invalidate — the old URL still exists at the edge serving old
content (fine, nothing references it anymore), and the new URL is a cache
miss exactly once, everywhere, the first time each edge location requests it.
This is why you can safely set:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Cache-Control: public, max-age=31536000, immutable
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;on everything under &lt;code&gt;_astro/&lt;/code&gt; (or &lt;code&gt;/assets/&lt;/code&gt;, however your bundler names it).
A year-long &lt;code&gt;max-age&lt;/code&gt; plus &lt;code&gt;immutable&lt;/code&gt; tells both browsers and CloudFront
&quot;never revalidate this, ever&quot; — and that&apos;s true, because the filename itself
guarantees the content can&apos;t change out from under that URL. This is the
single biggest lever for cutting invalidation traffic to near zero: the
majority of a static site&apos;s bytes (JS, CSS, hashed images) never need an
invalidation call in their entire lifetime.&lt;/p&gt;
&lt;p&gt;Set this at the S3 origin via object metadata (Astro/most bundlers don&apos;t set
&lt;code&gt;Cache-Control&lt;/code&gt; on upload themselves — your sync step has to), or override it
at the CloudFront cache behavior level with a policy scoped to the hashed
asset path pattern:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aws s3 sync ./dist s3://my-bucket/ \
  --exclude &quot;*&quot; --include &quot;_astro/*&quot; \
  --cache-control &quot;public, max-age=31536000, immutable&quot; \
  --metadata-directive REPLACE
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Run this pass before the pass that uploads everything else, so the
hashed-assets rule doesn&apos;t get clobbered by a broader default &lt;code&gt;Cache-Control&lt;/code&gt;
applied later in the sync.&lt;/p&gt;
&lt;h2&gt;Where you still need surgical invalidation&lt;/h2&gt;
&lt;p&gt;Fingerprinting only works for files whose &lt;em&gt;name&lt;/em&gt; changes when their &lt;em&gt;content&lt;/em&gt;
changes. Two categories of file don&apos;t get that treatment, and those are the
only things worth invalidating:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;index.html&lt;/code&gt; and any other non-hashed HTML entrypoint.&lt;/strong&gt; The URL
&lt;code&gt;/blog/index.html&lt;/code&gt; (or &lt;code&gt;/blog/&lt;/code&gt; via the pretty-URL rewrite) has to stay
stable — it&apos;s what&apos;s in every bookmark, backlink, and search index entry —
but its content changes every time you publish. Give these a short
&lt;code&gt;max-age&lt;/code&gt; (or &lt;code&gt;no-cache&lt;/code&gt; so CloudFront/browsers always revalidate against
origin) and invalidate them explicitly on deploy.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Non-hashed static assets you can&apos;t rename&lt;/strong&gt;, like &lt;code&gt;favicon.ico&lt;/code&gt;,
&lt;code&gt;robots.txt&lt;/code&gt;, &lt;code&gt;sitemap-index.xml&lt;/code&gt;, or &lt;code&gt;rss.xml&lt;/code&gt; — anything a spec or a
client expects at a fixed path.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For this site, that means the deploy step invalidates a short, explicit list,
not a wildcard:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aws cloudfront create-invalidation \
  --distribution-id &quot;$CF_DISTRIBUTION_ID&quot; \
  --paths &quot;/index.html&quot; &quot;/blog/*&quot; &quot;/rss.xml&quot; &quot;/sitemap-index.xml&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/blog/*&lt;/code&gt; is doing real work here — every post and tag page under &lt;code&gt;/blog/&lt;/code&gt;
is HTML with a stable, non-hashed URL, so a scoped wildcard on just that
subtree is the right call. It&apos;s still one billed path (wildcards always are),
but more importantly it only forces revalidation on the part of the site that
actually changes on every deploy, leaving the immutable hashed assets alone.&lt;/p&gt;
&lt;h2&gt;The gotcha: invalidations aren&apos;t instant, and they queue&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;create-invalidation&lt;/code&gt; returns immediately with a status of &lt;code&gt;InProgress&lt;/code&gt; — the
CLI call succeeding does not mean the content is gone from every edge
location yet. Full propagation across all of CloudFront&apos;s edge locations
typically completes within a few minutes, but there&apos;s no SLA guaranteeing a
specific time, and it&apos;s not uncommon to see stale content served from one
edge location after another has already updated. If your deploy pipeline
runs a post-deploy smoke test that curls the live URL immediately after
&lt;code&gt;create-invalidation&lt;/code&gt; returns, don&apos;t assert on content freshness right away —
poll &lt;code&gt;get-invalidation&lt;/code&gt; for &lt;code&gt;Status: Completed&lt;/code&gt; first, or just accept some
propagation lag in the check:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;aws cloudfront get-invalidation \
  --distribution-id &quot;$CF_DISTRIBUTION_ID&quot; \
  --id &quot;$INVALIDATION_ID&quot; \
  --query &apos;Invalidation.Status&apos;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The second, less obvious gotcha: **invalidation requests queue per
distribution**, and each request can list at most 3,000 paths (or 15 with
wildcards). If a script fires invalidations in a tight loop — say, a bulk
content migration that invalidates per-file instead of batching — later
requests sit &lt;code&gt;InProgress&lt;/code&gt; behind earlier ones rather than running in
parallel, so a burst of small invalidations can take noticeably longer to
fully clear than one batched call with the same total path count. Batch your
paths into as few &lt;code&gt;create-invalidation&lt;/code&gt; calls as the file-list limits allow,
rather than looping a call per file.&lt;/p&gt;
&lt;h2&gt;What to actually do&lt;/h2&gt;
&lt;p&gt;Fingerprint everything your bundler can fingerprint and set &lt;code&gt;max-age=31536000,&lt;/code&gt;
&lt;code&gt;immutable&lt;/code&gt; on it — that&apos;s most of your bytes and it needs zero invalidation
calls, ever, for the life of the file. For the small set of non-hashed
entrypoints (HTML, &lt;code&gt;robots.txt&lt;/code&gt;, feeds), use a short &lt;code&gt;max-age&lt;/code&gt; and a scoped
explicit-path invalidation on deploy, not &lt;code&gt;/*&lt;/code&gt;. You&apos;ll stay comfortably
inside the free 1,000-path monthly allowance even with several deploys a day,
and — the bigger win — your origin only gets hit for the handful of objects
that actually changed, not your entire distribution.&lt;/p&gt;
</content:encoded></item><item><title>Ditch IAM access keys: GitHub Actions OIDC to AWS</title><link>https://jstgtech.com/blog/2026-08-10-github-actions-oidc-aws/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-github-actions-oidc-aws/</guid><description>A step-by-step guide to replacing long-lived IAM access keys in GitHub Actions with short-lived OIDC credentials, including the trust policy gotchas that bite people.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If your GitHub Actions workflows still authenticate to AWS with a stored
&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; / &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; pair, you&apos;re carrying a
long-lived credential that can leak from a log, a fork&apos;s pull_request_target
run, or a compromised dependency — and it keeps working until someone
remembers to rotate it. OpenID Connect (OIDC) federation gets rid of that
entirely: GitHub mints a short-lived, workflow-scoped identity token, AWS STS
trades it for temporary credentials, and there&apos;s nothing sitting in your repo
secrets for an attacker to steal. This is the same pattern I use for this
site&apos;s own deploy pipeline, so what follows is the working setup, not just
the AWS docs paraphrased.&lt;/p&gt;
&lt;h2&gt;How the trust actually works&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;GitHub Actions exposes an OIDC provider at
&lt;code&gt;https://token.actions.githubusercontent.com&lt;/code&gt;. Every workflow run can
request a signed JWT from it (via &lt;code&gt;id-token: write&lt;/code&gt; permission) with
claims describing the repo, branch, and workflow.&lt;/li&gt;
&lt;li&gt;You register that provider as an IAM OIDC identity provider in your AWS
account, once.&lt;/li&gt;
&lt;li&gt;You create an IAM role whose trust policy says &quot;I&apos;ll accept tokens from
that provider, but only if the &lt;code&gt;sub&lt;/code&gt; claim matches this specific repo and
ref.&quot;&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;aws-actions/configure-aws-credentials&lt;/code&gt; action exchanges the JWT for
temporary STS credentials scoped to that role, valid for the run only.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;No secret ever leaves GitHub&apos;s control plane. Nothing to rotate, nothing to
revoke except the role&apos;s trust policy.&lt;/p&gt;
&lt;h2&gt;Step 1: create the OIDC provider&lt;/h2&gt;
&lt;p&gt;Do this once per AWS account (Terraform, since you&apos;re presumably managing
the rest of your IAM this way too):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource &quot;aws_iam_openid_connect_provider&quot; &quot;github_actions&quot; {
  url             = &quot;https://token.actions.githubusercontent.com&quot;
  client_id_list  = [&quot;sts.amazonaws.com&quot;]
  thumbprint_list = [&quot;6938fd4d98bab03faadb97b34396831e3780aea1&quot;]
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That thumbprint is GitHub&apos;s OIDC endpoint CA thumbprint. &lt;strong&gt;Gotcha:&lt;/strong&gt; AWS
actually ignores this field for GitHub&apos;s provider now (it validates via the
standard TLS CA bundle instead), but the argument is still required by the
resource — don&apos;t spend time trying to keep it &quot;current,&quot; it&apos;s a legacy
requirement AWS kept for backward compatibility.&lt;/p&gt;
&lt;h2&gt;Step 2: write a trust policy scoped tighter than you think you need&lt;/h2&gt;
&lt;p&gt;This is where most setups go wrong. The &lt;code&gt;sub&lt;/code&gt; claim format is
&lt;code&gt;repo:&amp;lt;org&amp;gt;/&amp;lt;repo&amp;gt;:&amp;lt;qualifier&amp;gt;&lt;/code&gt;, and it&apos;s tempting to wildcard it into
uselessness:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Principal&quot;: {
    &quot;Federated&quot;: &quot;arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com&quot;
  },
  &quot;Action&quot;: &quot;sts:AssumeRoleWithWebIdentity&quot;,
  &quot;Condition&quot;: {
    &quot;StringEquals&quot;: {
      &quot;token.actions.githubusercontent.com:aud&quot;: &quot;sts.amazonaws.com&quot;
    },
    &quot;StringLike&quot;: {
      &quot;token.actions.githubusercontent.com:sub&quot;: &quot;repo:my-org/jstgtech-web:ref:refs/heads/main&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things to get right:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Always set the &lt;code&gt;aud&lt;/code&gt; condition.&lt;/strong&gt; Without it, any GitHub Actions run
anywhere that requests a token for &lt;code&gt;sts.amazonaws.com&lt;/code&gt; audience and
happens to match your &lt;code&gt;sub&lt;/code&gt; pattern can assume the role. &lt;code&gt;aud&lt;/code&gt; is cheap
insurance and AWS&apos;s own quickstart includes it — don&apos;t skip it because the
console wizard makes it feel optional.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Scope &lt;code&gt;sub&lt;/code&gt; to the exact ref, not just the repo.&lt;/strong&gt; &lt;code&gt;repo:my-org/my-repo:*&lt;/code&gt;
lets a PR from a fork-turned-branch, a tag push, or an &lt;code&gt;environment:&lt;/code&gt;
deployment all assume the same role your production deploy uses. If a
workflow only needs to deploy from &lt;code&gt;main&lt;/code&gt;, pin &lt;code&gt;sub&lt;/code&gt; to
&lt;code&gt;repo:my-org/jstgtech-web:ref:refs/heads/main&lt;/code&gt;. If you use GitHub
Environments for a manual-approval gate, scope to
&lt;code&gt;repo:my-org/jstgtech-web:environment:production&lt;/code&gt; instead — that ties the
AWS role to the same approval gate protecting your environment secrets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a PR-only workflow (say, &lt;code&gt;terraform plan&lt;/code&gt; on pull requests, no write
access), use a separate, more restrictive role with &lt;code&gt;sub&lt;/code&gt; matching
&lt;code&gt;repo:my-org/jstgtech-web:pull_request&lt;/code&gt; and a read-only policy attached —
don&apos;t reuse your deploy role&apos;s trust policy with a broader condition &quot;just
for now.&quot;&lt;/p&gt;
&lt;h2&gt;Step 3: use it in the workflow&lt;/h2&gt;
&lt;pre&gt;&lt;code&gt;permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-site-deploy
          aws-region: us-east-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;permissions: id-token: write&lt;/code&gt; is not optional — it&apos;s what lets the runner
request the JWT in the first place, and GitHub defaults this to &lt;code&gt;none&lt;/code&gt; at
the org or repo level on newer accounts. If you get `Error: Not authorized
to perform sts:AssumeRoleWithWebIdentity`, check this before anything else;
it&apos;s the single most common cause, ahead of trust-policy typos.&lt;/p&gt;
&lt;h2&gt;Trade-offs worth knowing before you migrate&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Session duration is capped by the role, not the token.&lt;/strong&gt; The GitHub JWT
is short-lived by nature, but your role&apos;s &lt;code&gt;MaxSessionDuration&lt;/code&gt; still
governs how long the assumed credentials last. Keep it at the default
(1 hour) or lower for deploy roles — there&apos;s no reason a CI job needs an
8-hour session.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-account deploys need per-account providers.&lt;/strong&gt; The OIDC provider
and trust policy live in the AWS account being deployed &lt;em&gt;into&lt;/em&gt;, not in
some central account. If you deploy to three accounts (dev/stage/prod),
you need the provider registered in each, with role names and trust
conditions matched to that account&apos;s own audience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-hosted runners change the token issuer.&lt;/strong&gt; If you ever move a
workflow to self-hosted runners inside your own VPC, the token still comes
from &lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt; (GitHub issues it, not the
runner), so this setup doesn&apos;t need to change — but it&apos;s worth confirming
if you&apos;re debugging a runner migration and OIDC suddenly stops working for
an unrelated reason (usually a network path to GitHub&apos;s OIDC endpoint from
the runner).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You still need least-privilege on the role&apos;s permissions policy.&lt;/strong&gt;
OIDC only fixes &lt;em&gt;how&lt;/em&gt; the workflow authenticates, not &lt;em&gt;what&lt;/em&gt; it&apos;s allowed
to do once authenticated. A perfectly scoped trust policy attached to a
role with &lt;code&gt;AdministratorAccess&lt;/code&gt; is still one compromised Action away from
a bad day — scope the permissions policy to exactly the S3 bucket,
CloudFront distribution, or Terraform state path the workflow touches.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rolling it out without a big-bang cutover&lt;/h2&gt;
&lt;p&gt;If you&apos;re migrating an existing pipeline off access keys, run both in
parallel for one deploy cycle: add the OIDC role, switch
&lt;code&gt;configure-aws-credentials&lt;/code&gt; to &lt;code&gt;role-to-assume&lt;/code&gt;, watch a real deploy
succeed, &lt;em&gt;then&lt;/em&gt; delete the IAM user and its access keys. Don&apos;t delete the
old credentials in the same PR that introduces the new role — if the trust
policy&apos;s &lt;code&gt;sub&lt;/code&gt; condition is wrong, you want a fallback for the next deploy
instead of a broken pipeline and no way back in until you fix IAM by hand.&lt;/p&gt;
</content:encoded></item><item><title>IAM Identity Center: kill per-account IAM users for good</title><link>https://jstgtech.com/blog/2026-08-10-iam-identity-center-multi-account-sso/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-iam-identity-center-multi-account-sso/</guid><description>A practical guide to setting up AWS IAM Identity Center for multi-account SSO with Terraform, including the SCIM sync and provisioning-delay gotchas that catch people.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you&apos;re running more than one or two AWS accounts, you&apos;ve probably got the
same mess I inherited: a pile of IAM users, one per human, duplicated in
every account they need access to, each with its own password, its own MFA
device, and — if you&apos;re unlucky — its own long-lived access keys sitting in
someone&apos;s &lt;code&gt;~/.aws/credentials&lt;/code&gt;. Nobody remembers to deprovision the ex-
contractor&apos;s user in the &lt;code&gt;staging&lt;/code&gt; account. Nobody&apos;s rotated the access keys
on the &lt;code&gt;prod-readonly&lt;/code&gt; user since 2023. IAM Identity Center (the renamed AWS
SSO service) fixes this by moving identity out of individual accounts
entirely: you log in once, in one place, and get temporary credentials into
whichever accounts and roles you&apos;re assigned to. This is the setup I run
across a management account plus workload accounts, with the actual
Terraform and the parts that don&apos;t work the way the docs imply.&lt;/p&gt;
&lt;h2&gt;What Identity Center actually replaces&lt;/h2&gt;
&lt;p&gt;Per-account IAM users have three structural problems: identity is
duplicated per account (N users × M accounts to manage), credentials are
usually long-lived (passwords, access keys), and there&apos;s no single place to
see &quot;what can this person get into.&quot; IAM Identity Center centralizes all of
that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One identity source&lt;/strong&gt; — either Identity Center&apos;s own built-in directory,
or federated from an external IdP (Okta, Entra ID/Azure AD, Google
Workspace, or on-prem AD via AD Connector) over SCIM.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permission sets&lt;/strong&gt; — reusable IAM policy bundles (think &quot;IAM role
templates&quot;) that get provisioned as actual IAM roles in target accounts
when you assign them.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Account assignments&lt;/strong&gt; — a mapping of (user or group) × (permission set)
× (account), managed centrally from the Identity Center console/API in
your management account or a delegated administrator account.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The end state: a human logs into the Identity Center portal URL once,
sees a tile for every account/role combination they&apos;ve been granted, clicks
one, and gets temporary STS credentials. No IAM user, no password in that
account, no access key to leak.&lt;/p&gt;
&lt;h2&gt;Setting it up&lt;/h2&gt;
&lt;p&gt;Identity Center is enabled once per AWS Organization, in one region (it&apos;s
free-tier, but the instance and its permission sets live in a single home
region even though assignments apply org-wide). I run mine out of
&lt;code&gt;us-east-1&lt;/code&gt; in the management account, alongside the org&apos;s other org-wide
resources.&lt;/p&gt;
&lt;h3&gt;1. Choose your identity source&lt;/h3&gt;
&lt;p&gt;If you already run Okta or Entra ID, use it — don&apos;t stand up a second
identity directory to maintain. Enable SCIM provisioning in Identity Center
(Settings → Identity source → Automatic provisioning), which gives you an
SCIM endpoint URL and a bearer token. Paste those into your IdP&apos;s SCIM app
config, and your IdP becomes the source of truth for users and groups;
Identity Center just mirrors them.&lt;/p&gt;
&lt;p&gt;If you don&apos;t have an external IdP yet, the built-in Identity Center
directory is fine to start with — you can migrate to an external IdP later
without re-doing your permission sets or assignments, since those reference
group IDs, not the identity source itself.&lt;/p&gt;
&lt;h3&gt;2. Create permission sets&lt;/h3&gt;
&lt;p&gt;A permission set is either a collection of AWS managed policies, a custom
inline policy, or both, plus a session duration. I manage mine in Terraform
using the &lt;code&gt;aws_ssoadmin_permission_set&lt;/code&gt; resource — this is the piece people
skip because the console makes it feel like a one-off, and then six months
later nobody remembers what&apos;s in the &lt;code&gt;Billing-ReadOnly&lt;/code&gt; permission set or
why.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data &quot;aws_ssoadmin_instances&quot; &quot;this&quot; {}

resource &quot;aws_ssoadmin_permission_set&quot; &quot;read_only&quot; {
  name             = &quot;ReadOnlyAccess&quot;
  instance_arn     = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  session_duration = &quot;PT4H&quot;
}

resource &quot;aws_ssoadmin_managed_policy_attachment&quot; &quot;read_only&quot; {
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.read_only.arn
  managed_policy_arn = &quot;arn:aws:iam::aws:policy/ReadOnlyAccess&quot;
}

resource &quot;aws_ssoadmin_permission_set&quot; &quot;billing_admin&quot; {
  name             = &quot;BillingAdmin&quot;
  instance_arn     = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  session_duration = &quot;PT1H&quot;
}

resource &quot;aws_ssoadmin_permission_set_inline_policy&quot; &quot;billing_admin&quot; {
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.billing_admin.arn
  inline_policy = jsonencode({
    Version = &quot;2012-10-17&quot;
    Statement = [{
      Effect   = &quot;Allow&quot;
      Action   = [&quot;aws-portal:*Billing&quot;, &quot;aws-portal:*Usage&quot;, &quot;ce:*&quot;]
      Resource = &quot;*&quot;
    }]
  })
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;session_duration&lt;/code&gt; is an ISO 8601 duration, and it&apos;s the actual cap on
credential lifetime for anyone assigned this permission set — not
overridable at assumption time. I keep &lt;code&gt;BillingAdmin&lt;/code&gt; at &lt;code&gt;PT1H&lt;/code&gt; and
&lt;code&gt;ReadOnlyAccess&lt;/code&gt; at &lt;code&gt;PT4H&lt;/code&gt; since read-only browsing sessions are lower risk
than anything that touches spend controls.&lt;/p&gt;
&lt;h2&gt;How permission sets map to account assignments&lt;/h2&gt;
&lt;p&gt;This is the part that trips people up coming from single-account IAM
thinking: a permission set is a &lt;em&gt;template&lt;/em&gt;, not a role. Nothing exists in a
member account until you create an &lt;strong&gt;account assignment&lt;/strong&gt; — at that point
Identity Center provisions an actual IAM role in the target account, named
&lt;code&gt;AWSReservedSSO_&amp;lt;permission-set-name&amp;gt;_&amp;lt;hash&amp;gt;&lt;/code&gt;, with a trust policy that
allows the Identity Center service to assume it. You&apos;ll see these roles
show up in IAM → Roles in every account you&apos;ve assigned; don&apos;t hand-edit
them, they&apos;re managed by Identity Center and your edits will get silently
reverted on the next provisioning sync.&lt;/p&gt;
&lt;p&gt;Assignment is a three-way link — group, permission set, account:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;resource &quot;aws_ssoadmin_account_assignment&quot; &quot;readonly_staging&quot; {
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.read_only.arn

  principal_id   = data.aws_identitystore_group.engineers.group_id
  principal_type = &quot;GROUP&quot;

  target_id   = &quot;222233334444&quot; # staging account
  target_type = &quot;AWS_ACCOUNT&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Walkthrough: one group, three accounts&lt;/h2&gt;
&lt;p&gt;Say I want the &lt;code&gt;Engineers&lt;/code&gt; group to have &lt;code&gt;ReadOnlyAccess&lt;/code&gt; in &lt;code&gt;dev&lt;/code&gt;,
&lt;code&gt;staging&lt;/code&gt;, and &lt;code&gt;prod&lt;/code&gt;, but a separate &lt;code&gt;PowerUser&lt;/code&gt; permission set only in
&lt;code&gt;dev&lt;/code&gt;. First, look up the group by its SCIM-synced display name:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;data &quot;aws_identitystore_group&quot; &quot;engineers&quot; {
  identity_store_id = tolist(data.aws_ssoadmin_instances.this.identity_store_ids)[0]

  alternate_identifier {
    unique_attribute {
      attribute_path  = &quot;DisplayName&quot;
      attribute_value = &quot;Engineers&quot;
    }
  }
}

locals {
  readonly_accounts = {
    dev     = &quot;111122223333&quot;
    staging = &quot;222233334444&quot;
    prod    = &quot;333344445555&quot;
  }
}

resource &quot;aws_ssoadmin_account_assignment&quot; &quot;readonly&quot; {
  for_each = local.readonly_accounts

  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.read_only.arn
  principal_id       = data.aws_identitystore_group.engineers.group_id
  principal_type     = &quot;GROUP&quot;
  target_id          = each.value
  target_type        = &quot;AWS_ACCOUNT&quot;
}

resource &quot;aws_ssoadmin_account_assignment&quot; &quot;poweruser_dev&quot; {
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.power_user.arn
  principal_id       = data.aws_identitystore_group.engineers.group_id
  principal_type     = &quot;GROUP&quot;
  target_id          = local.readonly_accounts.dev
  target_type        = &quot;AWS_ACCOUNT&quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Apply that and everyone in &lt;code&gt;Engineers&lt;/code&gt; sees three tiles for &lt;code&gt;ReadOnlyAccess&lt;/code&gt;
(one per account) and one extra tile for &lt;code&gt;PowerUser&lt;/code&gt; in &lt;code&gt;dev&lt;/code&gt;, in the
Identity Center portal. If you&apos;d rather click through the console: Identity
Center → Permission sets → select one → nothing happens there for
assignment; you actually go to Multi-account permissions → select
account(s) → select the permission set → select the group. It&apos;s easy to
land on the wrong page first time because assignment lives under the
account list, not the permission set page.&lt;/p&gt;
&lt;h2&gt;CLI usage: &lt;code&gt;aws sso login&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Once assigned, configure a named profile pointing at the SSO session:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# ~/.aws/config
[sso-session my-org]
sso_start_url = https://my-org.awsapps.com/start
sso_region = us-east-1
sso_registration_scopes = sso:account:access

[profile staging-readonly]
sso_session = my-org
sso_account_id = 222233334444
sso_role_name = ReadOnlyAccess
region = us-east-1
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;aws sso login --profile staging-readonly
aws sts get-caller-identity --profile staging-readonly
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;aws sso login&lt;/code&gt; opens a browser, you authenticate against your IdP (or the
built-in directory), and the CLI caches temporary credentials locally,
scoped to the permission set&apos;s &lt;code&gt;session_duration&lt;/code&gt;. When they expire, you
just re-run &lt;code&gt;aws sso login&lt;/code&gt; — no key rotation, nothing to revoke in the
account itself, because there was never a long-lived credential there to
begin with.&lt;/p&gt;
&lt;h2&gt;Gotchas learned the hard way&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Permission set edits don&apos;t auto-propagate.&lt;/strong&gt; If you change a permission
set&apos;s policy (add a managed policy, edit the inline JSON), existing
account assignments keep the &lt;em&gt;old&lt;/em&gt; IAM role permissions until you
re-provision. In the console this is the &quot;Reprovision&quot; button on the
permission set&apos;s Accounts tab; via API/Terraform it&apos;s
&lt;code&gt;aws sso-admin provision-permission-set&lt;/code&gt; triggered automatically by
&lt;code&gt;terraform apply&lt;/code&gt; for &lt;code&gt;aws_ssoadmin_permission_set&lt;/code&gt; changes — but if
you&apos;re editing an inline policy attached via a separate resource, double
check the apply actually touched the permission set resource itself, not
just the attachment, or the role in the target account can silently stay
stale for hours.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SCIM sync lag.&lt;/strong&gt; A new group or user added in your IdP doesn&apos;t appear
in Identity Center instantly — SCIM sync typically runs every 15-40
minutes depending on the IdP. If you just created a group in Okta and
your &lt;code&gt;aws_identitystore_group&lt;/code&gt; data source in Terraform can&apos;t find it,
that&apos;s not a Terraform bug, it&apos;s sync lag. Check Identity Center →
Settings → Automatic provisioning for the last sync timestamp before you
start debugging your HCL.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Deleting a group in the IdP doesn&apos;t clean up assignments.&lt;/strong&gt; SCIM
deprovisioning removes the user/group from the Identity Store, but I&apos;ve
seen orphaned &lt;code&gt;aws_ssoadmin_account_assignment&lt;/code&gt; state entries hang around
if the group was deleted out-of-band from Terraform. &lt;code&gt;terraform plan&lt;/code&gt;
will complain it can&apos;t find the referenced group ID. Clean up
assignments in code before deleting the group upstream, not after.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Session duration is a hard cap, not a suggestion.&lt;/strong&gt; Unlike an IAM role
you can &lt;code&gt;AssumeRole&lt;/code&gt; into with a custom &lt;code&gt;DurationSeconds&lt;/code&gt; up to the role&apos;s
max, the permission set&apos;s &lt;code&gt;session_duration&lt;/code&gt; is what you get — there&apos;s no
per-login override. If your CI needs longer sessions than your interactive
permission set allows, that&apos;s a sign CI shouldn&apos;t be using an Identity
Center human-login flow at all; use a workload identity (OIDC role
assumption, like I covered in the GitHub Actions post) instead.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The management account itself is a special case.&lt;/strong&gt; You generally don&apos;t
want broad permission sets assigned in the Organizations management
account — keep assignments there to a small break-glass set, and do real
work through member/workload accounts. It&apos;s easy to accidentally grant
&lt;code&gt;AdministratorAccess&lt;/code&gt; in the management account because it&apos;s first in the
account picker.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Trade-offs vs per-account IAM users&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single point of login, single point of failure.&lt;/strong&gt; If Identity Center or
your IdP has an outage, nobody can get &lt;em&gt;interactive&lt;/em&gt; access to any
account through SSO. This is why you keep a small number of emergency IAM
users (with hardware MFA, credentials in a sealed vault process, not
routine use) for break-glass — Identity Center replaces routine human
access, not disaster recovery access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No more per-account password/MFA sprawl&lt;/strong&gt;, but you&apos;ve now got a much
bigger blast radius on the identity source itself — securing your IdP
(and its MFA policy) matters more than it used to, because it&apos;s now the
gate to every account at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Auditing gets dramatically better.&lt;/strong&gt; &lt;code&gt;aws sts get-caller-identity&lt;/code&gt; and
CloudTrail both show the assumed-role session name, which Identity Center
populates with the actual user&apos;s identity — so &lt;code&gt;AssumedRole&lt;/code&gt; events in
CloudTrail are attributable to a person, not a shared IAM user ARN that
three people know the password to.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Programmatic/service access still isn&apos;t Identity Center&apos;s job.&lt;/strong&gt;
Permission sets and &lt;code&gt;aws sso login&lt;/code&gt; are for humans. Machine-to-machine
auth (CI/CD, Lambda, EC2) should stay on IAM roles with OIDC federation
or instance profiles — don&apos;t try to shoehorn a service account into an
Identity Center permission set just to avoid having two auth patterns in
your org.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Rolling it out&lt;/h2&gt;
&lt;p&gt;Don&apos;t flip every account to Identity Center-only in one PR. Stand up
Identity Center, get SCIM sync working, create permission sets, and assign
them in &lt;em&gt;one&lt;/em&gt; low-risk account (I used a scratch &lt;code&gt;sandbox&lt;/code&gt; account) first —
confirm people can actually log in via the portal and get the access you
expect. Then extend account assignments outward account by account, and
only after everyone&apos;s confirmed they can get in through SSO do you go back
and deprovision the old per-account IAM users and their access keys. Keep a
short overlap window rather than a hard cutover date; the failure mode of
&quot;we deleted the IAM user before confirming the SSO group assignment
actually worked&quot; is a locked-out engineer and an emergency root login, not
a fun afternoon.&lt;/p&gt;
</content:encoded></item><item><title>Diagnosing and fixing Lambda cold starts that matter</title><link>https://jstgtech.com/blog/2026-08-10-lambda-cold-starts/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-lambda-cold-starts/</guid><description>A practitioner guide to measuring Lambda Init Duration, trimming package bloat, and deciding when provisioned concurrency is worth its always-on cost.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;Just use provisioned concurrency&quot; is the answer I hear most often when
someone complains about Lambda cold starts, and it&apos;s usually wrong — it fixes
the symptom, costs money every hour whether or not you&apos;re invoked, and skips
the part where you find out &lt;em&gt;why&lt;/em&gt; your function is slow to initialize in the
first place. Most cold start problems are fixable for free: a leaner
deployment package, a runtime that doesn&apos;t need to JIT-warm, or removing a VPC
attachment that was never actually necessary. This is the diagnostic process I
use before reaching for the checkbook.&lt;/p&gt;
&lt;h2&gt;Measure it before you touch anything&lt;/h2&gt;
&lt;p&gt;Every Lambda invocation writes a &lt;code&gt;REPORT&lt;/code&gt; line to CloudWatch Logs. On a cold
start, it includes an &lt;code&gt;Init Duration&lt;/code&gt; field that the warm-start version
doesn&apos;t have:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;REPORT RequestId: 8f3e...  Duration: 412.33 ms  Billed Duration: 413 ms
Memory Size: 512 MB  Max Memory Used: 98 MB  Init Duration: 621.47 ms
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That &lt;code&gt;Init Duration&lt;/code&gt; is the number to chase — it&apos;s time spent on the
execution environment bootstrapping the runtime, running module-level code
outside your handler, and (if configured) resolving your VPC ENI. It is &lt;em&gt;not&lt;/em&gt;
included in billed duration for most runtimes, so it won&apos;t show up as a cost
spike, only as added latency your caller feels. Don&apos;t confuse it with
&lt;code&gt;Duration&lt;/code&gt;, which is your handler&apos;s own execution time and is what you&apos;re
billed for.&lt;/p&gt;
&lt;p&gt;To pull this across many invocations instead of eyeballing one log line, use
CloudWatch Logs Insights:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;fields @timestamp, @initDuration, @duration
| filter ispresent(@initDuration)
| stats count(*) as coldStarts,
        avg(@initDuration) as avgInit,
        pct(@initDuration, 95) as p95Init
  by bin(1h)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;filter ispresent(@initDuration)&lt;/code&gt; line is what isolates cold starts —
warm invocations don&apos;t emit that field at all, so this query effectively
gives you a cold-start rate and latency distribution per hour, for free, with
no X-Ray required.&lt;/p&gt;
&lt;p&gt;X-Ray is worth turning on (&lt;code&gt;Tracing: Active&lt;/code&gt; in the function config) when you
need to see &lt;em&gt;where inside&lt;/em&gt; init time is going — module imports, SDK client
construction, secrets/config fetched at startup — rather than just how long
it took. In the trace timeline, the &lt;code&gt;Initialization&lt;/code&gt; segment sits before your
first subsegment; if it&apos;s dominated by something like a &lt;code&gt;boto3&lt;/code&gt; client build
or a config file fetched from Secrets Manager at import time, that&apos;s your
target, not the runtime itself.&lt;/p&gt;
&lt;h2&gt;Runtime choice moves the floor, not just the average&lt;/h2&gt;
&lt;p&gt;Interpreted runtimes (Python, Node.js, Ruby) have low &lt;em&gt;inherent&lt;/em&gt; init
overhead — a few hundred milliseconds — because there&apos;s no compilation step,
just interpreter startup and module loading. Compiled/JIT runtimes (Java, C#
on .NET, and to a lesser extent Go, which compiles to a static binary ahead of
time) trade that off: Go&apos;s cold start is often the fastest of all because
there&apos;s no runtime to boot at all, while JVM- and CLR-based functions pay for
class loading and JIT warm-up on every cold start, frequently 1-3 seconds for
anything beyond a trivial handler.&lt;/p&gt;
&lt;p&gt;If you&apos;re stuck on Java for ecosystem reasons, &lt;strong&gt;Lambda SnapStart&lt;/strong&gt; is the
single biggest lever available: it takes a pre-initialized, encrypted
snapshot of your execution environment&apos;s memory and disk state after your
&lt;code&gt;static&lt;/code&gt; initializers and any registered &lt;code&gt;beforeCheckpoint&lt;/code&gt; hooks run, then
resumes from that snapshot on cold start instead of re-running init from
scratch. In practice this takes Java functions from multi-second cold starts
down to sub-200ms for many workloads. It&apos;s opt-in per function
(&lt;code&gt;SnapStart: ApplyOn: PublishedVersions&lt;/code&gt;), only applies to published
versions (not &lt;code&gt;$LATEST&lt;/code&gt;), and anything non-deterministic in your static
init — random values, UUIDs, timestamps, opened network connections — needs
to be regenerated in a &lt;code&gt;beforeCheckpoint&lt;/code&gt;/&lt;code&gt;afterRestore&lt;/code&gt; hook, or you&apos;ll ship
the same &quot;random&quot; value to every restored environment. That&apos;s the gotcha that
bites people first: a cached DB connection captured in the snapshot resumes
in a stale, sometimes already-closed state on the other side.&lt;/p&gt;
&lt;h2&gt;Package size is the free win everyone skips&lt;/h2&gt;
&lt;p&gt;Init duration scales with how much code Lambda has to unzip and load before
your handler is reachable, and it&apos;s rarely your own code that&apos;s the problem —
it&apos;s the dependency tree. A Node function that pulls in the entire AWS SDK v2
(&lt;code&gt;aws-sdk&lt;/code&gt;) when it calls one S3 method drags in tens of megabytes it never
touches at runtime.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# See what&apos;s actually contributing to package size
du -sh node_modules/* | sort -rh | head -10

# Node: import only the client you use (SDK v3 is modular by design)
npm uninstall aws-sdk
npm install @aws-sdk/client-s3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For Python, the equivalent is trimming &lt;code&gt;requirements.txt&lt;/code&gt; to what&apos;s imported
at module scope and pushing anything only used inside rarely-hit code paths
to a lazy import inside the function body — module-level imports run during
init, so a heavy library imported &quot;just in case&quot; costs every cold start, not
just the invocations that use it. For any runtime, moving large,
rarely-changing dependencies into a &lt;strong&gt;Lambda Layer&lt;/strong&gt; doesn&apos;t reduce
unzip-and-load time by itself, but it does let you avoid re-uploading (and
Lambda re-validating) a multi-hundred-MB deployment package on every code
change, which matters more for deploy latency than cold starts.&lt;/p&gt;
&lt;p&gt;Container-image Lambdas deserve a specific warning here: they&apos;re pulled from
ECR and have historically had noticeably worse cold starts than zip packages
at larger image sizes, though Lambda&apos;s own image-caching layer has narrowed
that gap significantly since launch. If you&apos;re on container images purely out
of habit and your image is small, a zip package with a layer is very likely
faster to cold-start.&lt;/p&gt;
&lt;h2&gt;VPC attachment: mostly a solved problem, still worth checking&lt;/h2&gt;
&lt;p&gt;Before 2019, attaching a Lambda to a VPC meant provisioning an ENI per
concurrent execution environment, which could add 10 seconds or more to a
cold start. AWS&apos;s Hyperplane-based networking model eliminated most of that
by sharing ENIs across functions in the same VPC/subnet/security-group
combination, and current VPC-attached cold starts are typically within
tens to a couple hundred milliseconds of non-VPC ones. If you&apos;re still
carrying a workaround from that era — a warm-up cron job, an oversized
provisioned-concurrency pool sized for the old ENI cost — it&apos;s worth
re-measuring with the Logs Insights query above before assuming you still
need it. The overhead that remains is small but non-zero, so &lt;strong&gt;don&apos;t attach&lt;/strong&gt;
&lt;strong&gt;a function to a VPC it doesn&apos;t need&lt;/strong&gt; just because a sibling function does;
scope VPC config per function, not per stack.&lt;/p&gt;
&lt;h2&gt;When provisioned concurrency actually earns its cost&lt;/h2&gt;
&lt;p&gt;Provisioned concurrency pre-initializes a pool of execution environments and
keeps them warm, billed hourly whether invoked or not — it doesn&apos;t reduce
init duration, it just makes sure fewer invocations ever hit it. It&apos;s worth
the always-on cost when:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;You have a &lt;strong&gt;synchronous, latency-sensitive&lt;/strong&gt; caller (API Gateway, an
ALB, or a user-facing request path) where p99 latency is a product
requirement, not a nice-to-have.&lt;/li&gt;
&lt;li&gt;Traffic is &lt;strong&gt;spiky rather than steady&lt;/strong&gt; — a steady high-volume function
naturally stays warm from its own invocation rate and rarely cold-starts
regardless.&lt;/li&gt;
&lt;li&gt;You&apos;ve already trimmed package size and picked the leanest viable runtime,
and the remaining init duration is still unacceptable — provisioned
concurrency should be the last lever, not the first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&apos;s overkill for async, batch, or event-driven functions (S3/SQS/EventBridge
triggers) where an extra few hundred milliseconds on an occasional invocation
is invisible to anyone. Application Auto Scaling can scale provisioned
concurrency on a schedule (business-hours-only) or target-tracking policy, so
if you do need it, don&apos;t just set a flat number sized for peak — that&apos;s
paying peak-capacity prices around the clock for a curve that isn&apos;t flat.&lt;/p&gt;
&lt;h2&gt;Rollout: measure, trim, then buy&lt;/h2&gt;
&lt;p&gt;Start with the Logs Insights query above running against production traffic
for a week to get a real cold-start rate and p95 &lt;code&gt;Init Duration&lt;/code&gt; baseline —
don&apos;t optimize against a guess. Fix package bloat and runtime choice first;
both are free and often cut init duration by half or more on their own. Only
reach for provisioned concurrency once you&apos;ve re-measured after those changes
and still have a latency-sensitive path that misses its SLA — and even then,
size it with Application Auto Scaling against your real traffic curve, not a
number that felt safe.&lt;/p&gt;
</content:encoded></item><item><title>Scoping IAM Policies with Tag, IP, and MFA Conditions</title><link>https://jstgtech.com/blog/2026-08-10-least-privilege-iam-conditions/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-least-privilege-iam-conditions/</guid><description>A practical guide to IAM condition keys — aws:ResourceTag, aws:SourceIp, and MFA presence — with JSON examples and the Deny-with-exceptions gotchas that trip people up.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most IAM policies I inherit from a team are scoped by resource ARN and
nothing else: &quot;this role can &lt;code&gt;ec2:*&lt;/code&gt; on these instances.&quot; That&apos;s a start,
but it leaves gaps a determined or careless caller can walk through — an
engineer&apos;s laptop credentials reaching production from a coffee shop, a
role that can touch every EC2 instance in the account regardless of which
team owns it, a sensitive action that only checks &quot;are you authenticated&quot;
rather than &quot;did you actually use MFA to get here.&quot; IAM&apos;s condition keys
close those gaps without adding a second system to manage. They live right
inside the policy document, they&apos;re evaluated by the same engine, and once
you know the handful that matter, they cover almost every real-world
scoping requirement. Here&apos;s how I use them in practice, on real production
policies.&lt;/p&gt;
&lt;h2&gt;Tag-based scoping: aws:ResourceTag and aws:RequestTag&lt;/h2&gt;
&lt;p&gt;If your account has more than one team&apos;s resources in it, tag-based
scoping is the highest-leverage condition you can add. Instead of
enumerating ARNs (which breaks the moment someone launches a new
instance), you scope by tag and let your tagging discipline do the work:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: [&quot;ec2:StartInstances&quot;, &quot;ec2:StopInstances&quot;, &quot;ec2:RebootInstances&quot;],
  &quot;Resource&quot;: &quot;arn:aws:ec2:*:123456789012:instance/*&quot;,
  &quot;Condition&quot;: {
    &quot;StringEquals&quot;: {
      &quot;aws:ResourceTag/team&quot;: &quot;platform&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;aws:ResourceTag&lt;/code&gt; checks the tag already on the resource being acted on —
use it for read/modify/delete actions. For actions that &lt;em&gt;create&lt;/em&gt; a
resource, there&apos;s no tag on it yet at evaluation time, so you need
&lt;code&gt;aws:RequestTag&lt;/code&gt; instead, checking the tag the caller is trying to apply:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Effect&quot;: &quot;Allow&quot;,
  &quot;Action&quot;: &quot;ec2:RunInstances&quot;,
  &quot;Resource&quot;: &quot;arn:aws:ec2:*:123456789012:instance/*&quot;,
  &quot;Condition&quot;: {
    &quot;StringEquals&quot;: {
      &quot;aws:RequestTag/team&quot;: &quot;platform&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; this only stops people who use the console or API correctly —
it does nothing if the tag can simply be omitted or changed later. Pair it
with a &lt;code&gt;Deny&lt;/code&gt; that blocks untagged creation and, separately, blocks
&lt;code&gt;ec2:DeleteTags&lt;/code&gt; / &lt;code&gt;ec2:CreateTags&lt;/code&gt; on the &lt;code&gt;team&lt;/code&gt; key for anyone outside
your platform-admin role. Otherwise &quot;scoped by tag&quot; degrades into
&quot;scoped by tag until someone removes the tag,&quot; which isn&apos;t a security
boundary at all — it&apos;s a labeling convention people are trusting each
other to respect.&lt;/p&gt;
&lt;h2&gt;Network scoping: aws:SourceIp and aws:VpcSourceIp&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;aws:SourceIp&lt;/code&gt; restricts API calls to a CIDR range — your office IP, your
VPN egress, or (less usefully) &lt;code&gt;0.0.0.0/0&lt;/code&gt;. It&apos;s most valuable on IAM users
or roles that still authenticate outside of AWS-managed network paths
(a CI runner with a static egress IP, a break-glass admin user):&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;NotAction&quot;: [&quot;iam:ChangePassword&quot;, &quot;iam:GetUser&quot;],
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;NotIpAddress&quot;: {
      &quot;aws:SourceIp&quot;: [&quot;203.0.113.0/24&quot;, &quot;198.51.100.0/24&quot;]
    },
    &quot;Bool&quot;: {
      &quot;aws:ViaAWSService&quot;: &quot;false&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two things worth calling out. First, &lt;code&gt;aws:SourceIp&lt;/code&gt; looks at the &lt;em&gt;caller&apos;s&lt;/em&gt;
public IP for calls made directly to AWS, but for calls made from inside a
VPC through a VPC endpoint, you want &lt;code&gt;aws:VpcSourceIp&lt;/code&gt; instead — it&apos;s the
private IP inside the VPC, and it only populates when the call actually
transits a VPC endpoint. Mixing the two up is a common reason a
network-scoped policy silently fails for endpoint traffic: the condition
key you checked simply isn&apos;t present on that request, so it evaluates as
&quot;condition not met&quot; and the whole statement is skipped.&lt;/p&gt;
&lt;p&gt;Second, that &lt;code&gt;aws:ViaAWSService: false&lt;/code&gt; condition isn&apos;t decoration —
without it, this Deny also blocks AWS services calling APIs on your
behalf (e.g., CloudFormation invoking IAM during a stack operation),
because those calls don&apos;t carry your IP at all and would otherwise fail
the &lt;code&gt;NotIpAddress&lt;/code&gt; check and get denied. This is the single most common
way I&apos;ve seen an IP-restriction policy break someone&apos;s CI pipeline the
day after it ships.&lt;/p&gt;
&lt;h2&gt;Enforcing MFA: aws:MultiFactorAuthPresent and aws:MultiFactorAuthAge&lt;/h2&gt;
&lt;p&gt;For your most sensitive actions — deleting a CloudTrail trail,
deactivating GuardDuty, changing another user&apos;s credentials — require not
just &quot;authenticated&quot; but &quot;authenticated with MFA, recently&quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;{
  &quot;Effect&quot;: &quot;Deny&quot;,
  &quot;Action&quot;: [
    &quot;iam:DeleteUser&quot;,
    &quot;iam:DeleteRole&quot;,
    &quot;iam:UpdateAccessKey&quot;,
    &quot;cloudtrail:StopLogging&quot;,
    &quot;guardduty:DeleteDetector&quot;
  ],
  &quot;Resource&quot;: &quot;*&quot;,
  &quot;Condition&quot;: {
    &quot;BoolIfExists&quot;: {
      &quot;aws:MultiFactorAuthPresent&quot;: &quot;false&quot;
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Use &lt;code&gt;BoolIfExists&lt;/code&gt; rather than a plain &lt;code&gt;Bool&lt;/code&gt; here. &lt;code&gt;aws:MultiFactorAuthPresent&lt;/code&gt;
is simply &lt;em&gt;absent&lt;/em&gt; from the request context for some call types (notably
calls made with temporary credentials from certain federation flows, or
service-to-service calls), and a plain &lt;code&gt;Bool&lt;/code&gt; condition against a missing
key evaluates as false — which, counterintuitively, means the &lt;code&gt;Deny&lt;/code&gt;
condition (&lt;code&gt;&quot;aws:MultiFactorAuthPresent&quot;: &quot;false&quot;&lt;/code&gt;) matches and the action
gets denied even for legitimate non-interactive callers. &lt;code&gt;BoolIfExists&lt;/code&gt;
only evaluates the condition when the key is present, so it doesn&apos;t
misfire on requests where MFA presence genuinely isn&apos;t reportable.&lt;/p&gt;
&lt;p&gt;Add &lt;code&gt;aws:MultiFactorAuthAge&lt;/code&gt; (in seconds) if you want to force
re-authentication for stale sessions rather than trusting an MFA check
from ten hours ago:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;&quot;Condition&quot;: {
  &quot;NumericGreaterThan&quot;: {
    &quot;aws:MultiFactorAuthAge&quot;: &quot;3600&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Combining conditions safely: the Deny/NotAction trap&lt;/h2&gt;
&lt;p&gt;The pattern above — &lt;code&gt;Effect: Deny&lt;/code&gt; plus &lt;code&gt;NotAction&lt;/code&gt; — is powerful but easy
to get backwards. &lt;code&gt;NotAction&lt;/code&gt; in a &lt;code&gt;Deny&lt;/code&gt; statement means &quot;deny everything
&lt;em&gt;except&lt;/em&gt; these actions,&quot; so the actions you list are the ones exempted
from the deny, not the ones targeted by it. I&apos;ve seen policies where
someone wanted to &lt;em&gt;restrict&lt;/em&gt; a set of dangerous actions and reached for
&lt;code&gt;NotAction&lt;/code&gt;, accidentally exempting exactly the actions they meant to
lock down while denying everything else in the account. If your intent is
&quot;deny these specific actions unless X,&quot; use a plain &lt;code&gt;Action&lt;/code&gt; list. Reserve
&lt;code&gt;NotAction&lt;/code&gt; for &quot;deny everything except these few things&quot; — the safe
subset you&apos;re carving out, like the two IAM self-service calls in the
&lt;code&gt;aws:SourceIp&lt;/code&gt; example above, which a locked-out user still needs to fix
their own password.&lt;/p&gt;
&lt;p&gt;The other non-obvious trap: &lt;strong&gt;explicit Deny always wins, but a missing&lt;/strong&gt;
&lt;strong&gt;condition key doesn&apos;t always mean &quot;deny.&quot;&lt;/strong&gt; Whether an absent key trips
the condition depends entirely on which condition operator you used
(&lt;code&gt;Bool&lt;/code&gt; vs &lt;code&gt;BoolIfExists&lt;/code&gt;, &lt;code&gt;StringEquals&lt;/code&gt; vs &lt;code&gt;StringEqualsIfExists&lt;/code&gt;). IAM
doesn&apos;t warn you when a policy&apos;s condition silently never matches because
the key isn&apos;t in the request context — it just evaluates false and moves
on, and the action proceeds under whatever your next-most-permissive
statement allows. Test conditional Deny policies with the IAM Policy
Simulator against the actual principal and a realistic set of request
parameters before you rely on them, not just against a policy you&apos;re
reading and assuming is correct.&lt;/p&gt;
&lt;h2&gt;Rolling this out&lt;/h2&gt;
&lt;p&gt;Don&apos;t attach a new condition-scoped Deny directly to a broad group and
walk away. Roll it out the way I roll out any access-tightening change:
attach it to a single test principal first, run the actual workflows that
principal needs (including CI and automation, not just interactive
console use), check CloudTrail for &lt;code&gt;errorCode: AccessDenied&lt;/code&gt; events you
didn&apos;t expect, and only then widen the attachment to the group or
account-wide SCP. Tag-based and MFA-based conditions are cheap to write
and expensive to debug in production if you get the &lt;code&gt;IfExists&lt;/code&gt; variant
wrong — the fifteen minutes in the policy simulator is worth it.&lt;/p&gt;
</content:encoded></item><item><title>Terraform S3 native locking: kill your DynamoDB table</title><link>https://jstgtech.com/blog/2026-08-10-terraform-s3-native-locking/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-terraform-s3-native-locking/</guid><description>How to configure Terraform S3 backend state locking without DynamoDB using use_lockfile, plus the safe migration path from an existing lock table.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;For as long as I&apos;ve been writing Terraform, &quot;S3 backend&quot; meant &quot;S3 backend
plus a DynamoDB table for locking,&quot; full stop. You&apos;d provision a tiny
pay-per-request table, grant your CI role &lt;code&gt;dynamodb:GetItem&lt;/code&gt;/&lt;code&gt;PutItem&lt;/code&gt;/
&lt;code&gt;DeleteItem&lt;/code&gt; on it, and never think about it again except when someone asked
&quot;wait, why do we have a DynamoDB table for a static site&apos;s infra?&quot; As of
Terraform 1.10, that table is no longer required — S3 itself can do the
locking, using conditional writes. This site&apos;s own &lt;code&gt;terraform/backend.hcl&lt;/code&gt;
runs on exactly this setup, no DynamoDB table anywhere in the account. Here&apos;s
the history, the config, and the migration path if you&apos;re still running the
old way.&lt;/p&gt;
&lt;h2&gt;Why DynamoDB was ever in the picture&lt;/h2&gt;
&lt;p&gt;State locking exists to stop two &lt;code&gt;terraform apply&lt;/code&gt; runs from racing each
other and corrupting state — classic &quot;last writer wins&quot; data loss. S3 never
had a native compare-and-swap primitive, so Terraform&apos;s S3 backend
piggybacked on DynamoDB&apos;s conditional &lt;code&gt;PutItem&lt;/code&gt; (&lt;code&gt;attribute_not_exists&lt;/code&gt;) to
implement a lock: acquire a lock by writing an item keyed on the state path,
release it by deleting the item. It worked well, but it meant every S3
backend needed a second AWS service, a second IAM policy, and a second thing
to provision correctly before your very first &lt;code&gt;terraform init&lt;/code&gt; — plus a
manual &lt;code&gt;dynamodb:DeleteItem&lt;/code&gt; to clean up a stuck lock after a killed CI job,
which everyone running Terraform in CI has done at least once.&lt;/p&gt;
&lt;p&gt;Amazon added conditional writes to S3 itself in &lt;strong&gt;August 2024&lt;/strong&gt;
(&lt;code&gt;If-None-Match&lt;/code&gt; / &lt;code&gt;If-Match&lt;/code&gt; support on &lt;code&gt;PutObject&lt;/code&gt;), and HashiCorp shipped
support for using that directly as a locking mechanism in &lt;strong&gt;Terraform&lt;/strong&gt;
&lt;strong&gt;1.10&lt;/strong&gt; (November 2024) via a new backend argument: &lt;code&gt;use_lockfile&lt;/code&gt;. No
DynamoDB, no second service, no separate IAM policy — the same S3 permissions
your state already needed now cover locking too.&lt;/p&gt;
&lt;h2&gt;The config&lt;/h2&gt;
&lt;p&gt;This is the actual backend block from this repo, split the way Terraform
backends normally are: the non-secret pieces in a partial config file
(&lt;code&gt;terraform/backend.hcl&lt;/code&gt;), and the empty &lt;code&gt;backend &quot;s3&quot; {}&lt;/code&gt; stanza in
&lt;code&gt;versions.tf&lt;/code&gt; so nothing hardcoded ends up needing per-environment variables.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;# terraform/versions.tf
terraform {
  required_version = &quot;&amp;gt;= 1.10&quot;

  required_providers {
    aws = {
      source  = &quot;hashicorp/aws&quot;
      version = &quot;&amp;gt;= 5.60, &amp;lt; 8.0&quot;
    }
  }

  # Remote state in S3 with native locking (Terraform &amp;gt;= 1.10, no DynamoDB).
  # Non-secret values are supplied at init time via backend.hcl.
  backend &quot;s3&quot; {}
}
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;# terraform/backend.hcl
bucket       = &quot;jstgtech-web-tfstate&quot;
key          = &quot;site/terraform.tfstate&quot;
region       = &quot;us-east-1&quot;
encrypt      = true
use_lockfile = true
&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;terraform init -backend-config=backend.hcl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That&apos;s the whole thing. &lt;code&gt;use_lockfile = true&lt;/code&gt; tells the S3 backend to write a
&lt;code&gt;&amp;lt;key&amp;gt;.tflock&lt;/code&gt; object next to your state object during &lt;code&gt;plan&lt;/code&gt;/&lt;code&gt;apply&lt;/code&gt;, using
a conditional &lt;code&gt;PutObject&lt;/code&gt; so a second concurrent run gets a hard failure
instead of a silent overwrite, and deletes the lock object when the run
finishes (or on &lt;code&gt;terraform force-unlock&lt;/code&gt; if a run got killed before it could
clean up). No &lt;code&gt;dynamodb_table&lt;/code&gt; argument anywhere — the S3 backend has
supported that argument for years for the old locking path, and you can
technically still set both during a transition (more on that below), but a
fresh setup like this one just doesn&apos;t need it.&lt;/p&gt;
&lt;h2&gt;Gotchas that&apos;ll actually bite you&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Versioning on the state bucket is still mandatory, and separate from&lt;/strong&gt;
**  locking.** Locking stops concurrent writes; versioning is what saves you
when someone runs &lt;code&gt;apply&lt;/code&gt; against a bad plan or you need to roll back a
corrupted state file. &lt;code&gt;use_lockfile&lt;/code&gt; doesn&apos;t touch this at all — you still
need &lt;code&gt;aws_s3_bucket_versioning&lt;/code&gt; with &lt;code&gt;status = &quot;Enabled&quot;&lt;/code&gt; on the bucket, the
same as the DynamoDB-locking days. This repo&apos;s bootstrap layer
(&lt;code&gt;terraform/bootstrap/main.tf&lt;/code&gt;) sets it explicitly:&lt;/li&gt;
&lt;/ul&gt;
&lt;pre&gt;&lt;code&gt; resource &quot;aws_s3_bucket_versioning&quot; &quot;tfstate&quot; {
    bucket = aws_s3_bucket.tfstate.id
    versioning_configuration {
      status = &quot;Enabled&quot;
    }
  }
&lt;/code&gt;&lt;/pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Your IAM role needs write permissions it might not already have.&lt;/strong&gt; The
lock file is a real S3 object, so whatever role runs Terraform needs
&lt;code&gt;s3:PutObject&lt;/code&gt; and &lt;code&gt;s3:DeleteObject&lt;/code&gt; on the state bucket — most roles that
could already read/write state already have this, but if you&apos;d scoped a
role down to &lt;code&gt;s3:GetObject&lt;/code&gt; + &lt;code&gt;s3:PutObject&lt;/code&gt; on just the state key, tighten
it to cover the &lt;code&gt;&amp;lt;key&amp;gt;.tflock&lt;/code&gt; path too (or just the whole prefix, which is
simpler and what most setups do — &lt;code&gt;s3:*&lt;/code&gt; scoped to the bucket ARN, not
wildcarded across accounts).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The &lt;code&gt;backend&lt;/code&gt; block cannot reference variables, locals, or anything&lt;/strong&gt;
**  computed — this predates native locking but people relearn it every time.**
You cannot do &lt;code&gt;backend &quot;s3&quot; { bucket = var.state_bucket }&lt;/code&gt;. That&apos;s why
&lt;code&gt;backend &quot;s3&quot; {}&lt;/code&gt; is empty in &lt;code&gt;versions.tf&lt;/code&gt; and everything lives in
&lt;code&gt;backend.hcl&lt;/code&gt;, passed at &lt;code&gt;init&lt;/code&gt; time with &lt;code&gt;-backend-config&lt;/code&gt;. If you have
multiple environments, you&apos;ll have multiple &lt;code&gt;.hcl&lt;/code&gt; files (or a templated
one generated by CI before &lt;code&gt;init&lt;/code&gt;), not variables inside the block.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;use_lockfile&lt;/code&gt; needs Terraform ≥ 1.10, checked hard.&lt;/strong&gt; If someone on your
team is still on 1.9 (or an old pinned CI container), &lt;code&gt;init&lt;/code&gt; will just
error on the unrecognized argument. Bump &lt;code&gt;required_version&lt;/code&gt; in the same PR
that adds &lt;code&gt;use_lockfile&lt;/code&gt; so the mismatch fails loud at &lt;code&gt;init&lt;/code&gt; instead of
someone silently running an older binary against a backend config it
doesn&apos;t understand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stuck locks are unlocked differently now.&lt;/strong&gt; With DynamoDB you&apos;d
&lt;code&gt;aws dynamodb delete-item&lt;/code&gt; the lock row by hand, or use
&lt;code&gt;terraform force-unlock &amp;lt;LOCK_ID&amp;gt;&lt;/code&gt; which did the same thing under the
hood. With native locking, &lt;code&gt;force-unlock&lt;/code&gt; still works — Terraform reads the
lock ID from the &lt;code&gt;.tflock&lt;/code&gt; object&apos;s contents — but if you&apos;re ever debugging
by hand, you&apos;re looking for an S3 object, not a DynamoDB item, and it&apos;s
named &lt;code&gt;&amp;lt;key&amp;gt;.tflock&lt;/code&gt; next to your actual state object in the same bucket.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Migrating an existing DynamoDB-locked backend&lt;/h2&gt;
&lt;p&gt;If you&apos;ve got a working &lt;code&gt;dynamodb_table&lt;/code&gt; setup today, don&apos;t just delete the
table and flip &lt;code&gt;use_lockfile&lt;/code&gt; on in the same change — do it in two steps so
you&apos;re never in a state where in-flight infra changes could hit a locking
config mismatch:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;**Add &lt;code&gt;use_lockfile = true&lt;/code&gt; to &lt;code&gt;backend.hcl&lt;/code&gt; alongside the existing
&lt;code&gt;dynamodb_table&lt;/code&gt; entry**, then run:&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code&gt;   terraform init -reconfigure
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;-reconfigure&lt;/code&gt; (not &lt;code&gt;-migrate-state&lt;/code&gt;) is correct here — you&apos;re not moving
state to a new bucket/key, you&apos;re just changing backend &lt;em&gt;configuration&lt;/em&gt; on
the same backend type. Terraform will pick up native locking going
forward. At this point both mechanisms are technically configured, but
Terraform only uses one lock mechanism per backend version — as of the
1.10–1.13 line, setting both &lt;code&gt;dynamodb_table&lt;/code&gt; and &lt;code&gt;use_lockfile&lt;/code&gt; together
is explicitly supported as a transition state precisely for this
migration.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Run a real &lt;code&gt;plan&lt;/code&gt; and &lt;code&gt;apply&lt;/code&gt; to confirm the new lock path works&lt;/strong&gt; —
watch for the &lt;code&gt;.tflock&lt;/code&gt; object appearing in the state bucket during the
run (&lt;code&gt;aws s3 ls s3://your-tfstate-bucket/ --recursive | grep tflock&lt;/code&gt;
while it&apos;s running, or just check after a &lt;code&gt;plan&lt;/code&gt; that holds the lock long
enough).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Once you&apos;re confident, remove &lt;code&gt;dynamodb_table&lt;/code&gt; from &lt;code&gt;backend.hcl&lt;/code&gt;&lt;/strong&gt; and
run &lt;code&gt;terraform init -reconfigure&lt;/code&gt; again. Nothing about your state file
changes in this step — you&apos;re purely dropping a backend config key.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Delete the DynamoDB table&lt;/strong&gt; — but not immediately. Leave it a week or
two after step 3 in case you need to roll a teammate&apos;s stale local config
back, then &lt;code&gt;terraform state rm&lt;/code&gt;/destroy it via whatever provisioned it
originally (if the table itself was Terraform-managed in a bootstrap
layer, remove the resource block and apply; if it was created by hand,
just delete it from the console or CLI). For a low-traffic project this is
maybe $1–2/month in the pay-per-request billing mode most people use for
lock tables, so the savings are more about **one fewer resource to
provision, IAM-scope, and explain to the next person reading your
Terraform** than meaningful dollars — the real win is operational, not
financial.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;When DynamoDB locking still earns its keep&lt;/h2&gt;
&lt;p&gt;I wouldn&apos;t rip it out reflexively everywhere:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;You&apos;re pinned below Terraform 1.10&lt;/strong&gt; for reasons outside your control
(an older provider requiring an old core version, an internal policy that
hasn&apos;t approved the upgrade yet). &lt;code&gt;use_lockfile&lt;/code&gt; simply isn&apos;t available.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multiple tools lock against the same state outside Terraform&apos;s own CLI&lt;/strong&gt;
— e.g., a custom automation script or a different IaC tool cooperating on
the same DynamoDB lock table by convention. S3 native locking is
Terraform-backend-specific; nothing else in that ecosystem understands
&lt;code&gt;.tflock&lt;/code&gt; objects the way tools built around the DynamoDB API might.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You&apos;re on OpenTofu&lt;/strong&gt; and haven&apos;t checked whether your pinned version has
the equivalent support yet — OpenTofu forked before this landed and added
it on its own timeline, so don&apos;t assume version-number parity with
Terraform means feature parity here.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a single-account, single-tool setup like this site&apos;s — GitHub Actions
running &lt;code&gt;terraform plan&lt;/code&gt;/&lt;code&gt;apply&lt;/code&gt; via OIDC, nothing else touching state — none
of those apply, which is exactly why &lt;code&gt;terraform/backend.hcl&lt;/code&gt; here has no
&lt;code&gt;dynamodb_table&lt;/code&gt; line at all.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: Amazon Bedrock and its guardrails feature</title><link>https://jstgtech.com/blog/2026-08-10-amazon-bedrock/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-amazon-bedrock/</guid><description>What Bedrock buys you over calling a model provider directly, how the Guardrails feature filters content and PII, and where per-token pricing surprises show up.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every team building on top of a foundation model eventually asks the same
question: call the provider&apos;s API directly, or go through a managed layer.
Bedrock&apos;s answer is &quot;stay inside your AWS account boundary and get one API
across multiple model providers&quot; — worth understanding precisely, since
&quot;it&apos;s just an API wrapper&quot; undersells what it actually replaces, and
overselling it as &quot;AWS&apos;s AI service&quot; undersells how much is still on you.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;Bedrock is a &lt;strong&gt;managed access layer&lt;/strong&gt; to foundation models from multiple
providers — Anthropic&apos;s Claude family, Meta&apos;s Llama, Amazon&apos;s own Nova
models, Mistral, and others — through one consistent API and SDK, with no
GPU infrastructure for you to provision or manage. You&apos;re not running a
model; you&apos;re calling one that AWS hosts, billed per request. The value
proposition over calling a provider directly is staying inside your existing
AWS security boundary: IAM for auth instead of a separate API key to
rotate and store, VPC endpoints so traffic never leaves AWS&apos;s network,
CloudTrail logging every invocation, and data that (per AWS&apos;s terms) isn&apos;t
used to train underlying models — which matters if your compliance posture
already assumes AWS&apos;s shared responsibility model and adding a new
third-party vendor relationship is its own review cycle.&lt;/p&gt;
&lt;h2&gt;Guardrails&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Guardrails for Amazon Bedrock&lt;/strong&gt; is a configurable content-filtering layer
you attach to any model call, independent of which underlying model you&apos;re
using. It can block or mask categories of content (hate speech, violence,
prompt-injection attempts), redact detected &lt;strong&gt;PII&lt;/strong&gt; in both the prompt and
the response (SSNs, credit card numbers, emails — configurable per field),
enforce &lt;strong&gt;topic denial&lt;/strong&gt; (block responses about specific configured topics
entirely — &quot;don&apos;t discuss competitor products,&quot; for instance), and run a
&lt;strong&gt;contextual grounding check&lt;/strong&gt; that scores a RAG response against its source
documents to catch hallucination before it reaches the user. It runs as a
policy applied at invocation time, which means you can update filtering
rules without redeploying application code or retraining anything.&lt;/p&gt;
&lt;p&gt;The honest caveat: guardrails are pattern- and classifier-based, not a
guarantee. They meaningfully reduce the surface area for bad outputs and
give you an audit trail of what was blocked, but they&apos;re a mitigation layer,
not a substitute for output validation in anything security- or
compliance-sensitive downstream.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-model flexibility without rewriting integration code.&lt;/strong&gt; Swapping
which model handles a workload — testing Claude against Llama for a given
task, or moving to a newer model version — is a parameter change in the
same API call, not a new SDK integration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Regulated environments&lt;/strong&gt; where &quot;customer data leaves AWS&apos;s network
boundary&quot; is itself the compliance blocker, independent of which model is
actually good enough for the task.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fine-tuning and RAG without standing up your own vector infrastructure.&lt;/strong&gt;
Bedrock Knowledge Bases handles embedding, chunking, and retrieval against
a vector store (OpenSearch Serverless, Aurora, Pinecone) with a managed
ingestion pipeline, if you&apos;d rather not build that yourself.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The pricing gotcha&lt;/h2&gt;
&lt;p&gt;Bedrock bills &lt;strong&gt;per token&lt;/strong&gt;, in and out, and pricing varies by model — a
larger, more capable model can be an order of magnitude more expensive per
token than a smaller one for the same request. The trap is defaulting every
call in an application to the largest/newest model &quot;because it&apos;s the best,&quot;
when a cheaper model handles the bulk of routine requests (simple
classification, short summarization) just as well. &lt;strong&gt;Provisioned&lt;/strong&gt;
&lt;strong&gt;Throughput&lt;/strong&gt; (reserved capacity, billed hourly regardless of usage) is worth
it only at sustained high volume; below that threshold, on-demand per-token
pricing is cheaper even though the per-unit rate looks higher, because
you&apos;re not paying for idle reserved capacity between requests.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Route by task complexity instead of hardcoding one model for an entire
application — a cheap, fast model for classification/extraction/short
responses, a larger model reserved for requests that actually need deeper
reasoning. Bedrock&apos;s consistent API across model families makes that routing
a config decision rather than a rewrite, which is most of the point of using
a multi-model layer instead of committing to one provider&apos;s SDK directly.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: Amazon EventBridge beyond Lambda glue</title><link>https://jstgtech.com/blog/2026-08-10-amazon-eventbridge/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-amazon-eventbridge/</guid><description>What EventBridge actually buys you over SNS/SQS point-to-point wiring, how schema discovery and archive/replay work, and where rule limits bite.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most teams meet EventBridge as &quot;the thing that triggers a Lambda on a cron
schedule&quot; and stop there. That&apos;s a legitimate use, but it undersells the
service — EventBridge is an event bus, and the pattern-matching/routing layer
underneath it is worth understanding on its own, separate from the scheduler
feature that happens to share the console.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;An &lt;strong&gt;event bus&lt;/strong&gt; is a named channel that receives JSON events and evaluates
them against a set of &lt;strong&gt;rules&lt;/strong&gt;. Each rule has an event pattern — a partial
JSON match against the event&apos;s fields — and a list of targets (Lambda, SQS,
Step Functions, Kinesis, another event bus, over 20 AWS services directly).
When an event matches, EventBridge fans it out to every matching rule&apos;s
targets, in parallel, with built-in retry and an optional dead-letter queue
per target. You publish an event once; the bus decides who cares.&lt;/p&gt;
&lt;p&gt;Every account gets a &lt;strong&gt;default bus&lt;/strong&gt; that also receives events natively from
~200 AWS services (an S3 upload, an ECS task state change, a CodePipeline
stage transition) with zero integration code — that&apos;s the part people miss.
You can also create &lt;strong&gt;custom buses&lt;/strong&gt; for your own application events, and
&lt;strong&gt;partner buses&lt;/strong&gt; for SaaS integrations (Datadog, PagerDuty, Zendesk) that
push events directly into your account.&lt;/p&gt;
&lt;h2&gt;Where it beats point-to-point wiring&lt;/h2&gt;
&lt;p&gt;The alternative to EventBridge is usually &quot;Lambda A calls Lambda B directly&quot;
or &quot;publish to an SNS topic with N subscriptions.&quot; Both work, but they
couple the publisher to knowing who&apos;s listening. EventBridge inverts that:
the publisher emits one event describing what happened, and consumers
declare what they care about via pattern matching, without the publisher&apos;s
code ever changing when a fourth consumer shows up. Adding &quot;also send a
Slack notification when an order ships&quot; is a new rule, not a code change to
the order service.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Schema discovery&lt;/strong&gt; is the other underused feature: point EventBridge at a
bus and it infers a schema registry from the events flowing through it,
including versioning as the shape evolves, and can generate strongly-typed
bindings (Java, Python, TypeScript) for consumers. For anything beyond a
handful of hand-maintained event types, that beats a wiki page describing
&quot;here&apos;s what fields this event has, trust me.&quot;&lt;/p&gt;
&lt;h2&gt;Archive and replay&lt;/h2&gt;
&lt;p&gt;Every bus can have an &lt;strong&gt;archive&lt;/strong&gt; attached — EventBridge stores a copy of
matching events (with a retention period you set, including indefinite) and
can &lt;strong&gt;replay&lt;/strong&gt; them into the bus later, re-triggering the same rules. That&apos;s
the feature that turns &quot;we had a bug in the order-confirmation-email Lambda
for six hours&quot; from a data-backfill script into a console button: fix the
Lambda, replay the archived window, done. It&apos;s also genuinely useful for
testing — replay production events into a rule pointed at a dev target
instead of hand-crafting test payloads.&lt;/p&gt;
&lt;h2&gt;The rule-limit gotcha&lt;/h2&gt;
&lt;p&gt;Each event pattern rule can have &lt;strong&gt;at most 5 targets&lt;/strong&gt;, and a bus can have
up to 300 rules by default (a soft limit, raisable). Teams that treat
EventBridge as a single giant bus with dozens of broad-matching rules and
long target lists eventually hit both limits and end up debugging which of
15 similar-looking rules actually matched a given event — the console&apos;s
&quot;test pattern&quot; tool helps, but the real fix is scoping rules narrowly by
&lt;code&gt;detail-type&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt; from the start rather than one catch-all rule per
consumer with a growing target list.&lt;/p&gt;
&lt;h2&gt;Pricing&lt;/h2&gt;
&lt;p&gt;Custom events published to a custom bus are billed per million events;
events matched from AWS service integrations on the default bus are &lt;strong&gt;free&lt;/strong&gt;
— only your own published events cost anything. Archive storage is billed
separately by GB, and replays re-invoke targets, so replaying a large
archived window against a Lambda target bills exactly like the original
traffic did. It&apos;s cheap at normal volumes, but a botched pattern that
accidentally matches everything (an empty or overly broad &lt;code&gt;source&lt;/code&gt; filter)
and fans out to an expensive target is the way people get a surprise line
item, same as any fan-out system.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Give every custom event a &lt;code&gt;detail-type&lt;/code&gt; and &lt;code&gt;source&lt;/code&gt; from day one, even for
a single-consumer event — retrofitting rule scoping after five teams are
publishing to the same bus with inconsistent field names is far more painful
than establishing the convention up front. If you&apos;re publishing from
multiple services, agree on a naming scheme (&lt;code&gt;source: &quot;orders.checkout&quot;&lt;/code&gt;,
&lt;code&gt;detail-type: &quot;OrderPlaced&quot;&lt;/code&gt;) before the first event ships, not after.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: Amazon SQS queue depth, DLQs, and visibility</title><link>https://jstgtech.com/blog/2026-08-10-amazon-sqs/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-amazon-sqs/</guid><description>The SQS mistakes that show up in production — visibility timeout mismatches, DLQ redrive without a plan, and the alarms worth setting on day one.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;SQS is old, boring, and one of the most reliable services AWS runs — which
is exactly why it&apos;s easy to wire up carelessly and not notice until a queue
backs up in production. The service itself rarely fails; the mistakes are
almost always in how visibility timeouts, DLQs, and consumer scaling are
configured around it.&lt;/p&gt;
&lt;h2&gt;The core model, briefly&lt;/h2&gt;
&lt;p&gt;A producer sends a message; a consumer polls, receives it, processes it, and
explicitly &lt;strong&gt;deletes&lt;/strong&gt; it. Between receive and delete, the message is
&lt;strong&gt;invisible&lt;/strong&gt; to other consumers for the &lt;strong&gt;visibility timeout&lt;/strong&gt; duration —
not deleted, just hidden, so a second consumer doesn&apos;t pick up the same
message while the first is still working it. If the consumer crashes,
times out, or never calls delete, the message reappears after the timeout
expires and gets redelivered. That reappear-on-failure behavior is the
entire reliability model, and most SQS production issues trace back to a
mismatch somewhere in that loop.&lt;/p&gt;
&lt;h2&gt;Mistake 1: visibility timeout shorter than processing time&lt;/h2&gt;
&lt;p&gt;If your consumer takes 90 seconds to process a message but the queue&apos;s
visibility timeout is set to the default 30 seconds, the message becomes
visible again &lt;strong&gt;while it&apos;s still being processed&lt;/strong&gt; — a second consumer
picks it up, and now you&apos;re processing the same message twice concurrently.
For anything non-idempotent (charging a card, sending an email, decrementing
inventory), that&apos;s a correctness bug, not just wasted compute. Set the
visibility timeout to comfortably exceed your &lt;strong&gt;maximum&lt;/strong&gt; expected
processing time, not the average — or use &lt;code&gt;ChangeMessageVisibility&lt;/code&gt; to
extend it dynamically from inside a long-running handler if processing time
varies widely.&lt;/p&gt;
&lt;h2&gt;Mistake 2: no DLQ, or a DLQ nobody watches&lt;/h2&gt;
&lt;p&gt;A &lt;strong&gt;dead-letter queue&lt;/strong&gt; catches messages that fail processing repeatedly (by
maxReceiveCount) instead of retrying forever and blocking the queue behind
one poison message. Skipping a DLQ means a single malformed message can
loop indefinitely, burning consumer capacity on retries that will never
succeed. But the more common failure is having a DLQ and never alarming on
it — messages quietly pile up, nobody notices for weeks, and by the time
someone checks, the redrive window (or business relevance of the messages)
has passed. A DLQ without a CloudWatch alarm on &lt;code&gt;ApproximateNumberOfMessages&lt;/code&gt;
&lt;code&gt;Visible&lt;/code&gt; is a DLQ that isn&apos;t doing its job.&lt;/p&gt;
&lt;h2&gt;Mistake 3: treating queue depth as the only signal&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;ApproximateNumberOfMessagesVisible&lt;/code&gt; tells you how many messages are
waiting, but on its own it doesn&apos;t tell you whether that&apos;s a processing
outage or just a traffic spike your consumers will burn down in ten
minutes. Pair it with &lt;code&gt;ApproximateAgeOfOldestMessage&lt;/code&gt; — a queue that&apos;s deep
but where the oldest message is only 90 seconds old is healthy and
scaling; a queue where the oldest message is 40 minutes old means something
downstream is actually stuck, regardless of current depth. Age-of-oldest is
usually the better alarm trigger for &quot;something is broken&quot; versus
&quot;we&apos;re busy.&quot;&lt;/p&gt;
&lt;h2&gt;Standard vs FIFO&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Standard&lt;/strong&gt; queues are at-least-once delivery with best-effort ordering and
effectively unlimited throughput — the right default for most workloads,
and your consumer logic needs to be idempotent regardless (duplicate
delivery is a normal, expected occurrence, not an edge case). &lt;strong&gt;FIFO&lt;/strong&gt;
queues add exactly-once processing and strict ordering **within a message
group**, at the cost of a throughput ceiling (3,000 messages/sec with
batching, per API action) and higher per-request cost. Reach for FIFO only
when ordering is a genuine correctness requirement (e.g., applying account
balance changes in sequence) — defaulting to FIFO &quot;to be safe&quot; trades away
throughput headroom for a guarantee most workloads don&apos;t actually need.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Set a &lt;strong&gt;redrive policy that moves messages back from the DLQ to the source&lt;/strong&gt;
&lt;strong&gt;queue&lt;/strong&gt; (SQS supports this natively now, no custom script needed) as part of
your incident-response runbook, not as something you improvise during an
outage. Deciding in advance how many retries a message deserves before
landing in the DLQ, and what &quot;redrive after the bug is fixed&quot; actually looks
like operationally, is a five-minute conversation before go-live and a much
worse one at 2am with a few thousand stuck messages.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: Aurora Serverless v2 without the v1 cold starts</title><link>https://jstgtech.com/blog/2026-08-10-aurora-serverless-v2/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-aurora-serverless-v2/</guid><description>How Aurora Serverless v2 scales capacity in fine-grained ACUs without pausing, where it still costs more than provisioned Aurora, and its real limits.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Aurora Serverless v1 had a reputation problem, and it was earned: scale-to-
zero meant a cold start that could take tens of seconds on the next
connection, and scaling itself worked in coarse steps that didn&apos;t handle
sudden bursts well. Enough people got burned that &quot;Aurora Serverless&quot; became
shorthand for &quot;don&apos;t.&quot; v2 is a different architecture, not a patched v1, and
it&apos;s worth revisiting that reputation now that the two are easy to conflate.&lt;/p&gt;
&lt;h2&gt;What changed&lt;/h2&gt;
&lt;p&gt;v1 scaled by swapping the entire database to a differently-sized instance
behind the scenes — a real cutover with a brief connection drop, done in
discrete capacity steps. v2 scales &lt;strong&gt;capacity in place&lt;/strong&gt;, in increments of
0.5 &lt;strong&gt;ACUs&lt;/strong&gt; (Aurora Capacity Units, each roughly 2 GiB of memory plus
proportional CPU and networking), typically in under a second, with no
connection interruption. You set a min and max ACU range per cluster (as low
as 0, up to 256 per instance) and Aurora adjusts within it continuously
based on actual load — CPU, memory, and active connections — rather than
stepping through a small number of predefined sizes.&lt;/p&gt;
&lt;p&gt;Crucially, v2 instances can participate in a &lt;strong&gt;Global Database&lt;/strong&gt;, use
&lt;strong&gt;Multi-AZ&lt;/strong&gt; with the same failover mechanics as provisioned Aurora, and
even mix with provisioned instances in the same cluster (serverless reader,
provisioned writer, or vice versa) — none of which v1 supported. That mixed-
instance-class capability is the feature that makes it viable for real
production topologies instead of just dev/test databases.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Variable, hard-to-predict load&lt;/strong&gt; — a multi-tenant SaaS database where
tenant activity spikes unpredictably, or a workload with a strong daily/
weekly cycle (busy on weekday mornings, near-idle overnight) where sizing
for peak means paying for idle capacity most of the time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dev/test/staging environments&lt;/strong&gt; that see bursts of activity during work
hours and near-zero the rest of the time — set a low min ACU and let it
ride down without anyone managing an instance-stop/start schedule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;New workloads where you genuinely don&apos;t know the right instance size&lt;/strong&gt;
**  yet.** Instead of guessing an &lt;code&gt;r6g.xlarge&lt;/code&gt; and resizing later (a
disruptive operation on provisioned Aurora), set a wide ACU range and let
the actual traffic tell you where it settles — then, if it settles at a
steady high number, consider whether provisioned pricing would now be
cheaper for that stable load.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where provisioned Aurora still wins&lt;/h2&gt;
&lt;p&gt;For a &lt;strong&gt;steady, predictable, high-utilization workload&lt;/strong&gt;, provisioned Aurora
with Reserved Instance pricing is cheaper than v2 at the equivalent
capacity — the per-ACU-hour rate carries a premium over the equivalent
provisioned instance-hour, same trade as Fargate versus EC2. If your
database sits at a consistent 8 ACUs of load 24/7 with no meaningful
variance, you&apos;re paying for elasticity you&apos;re not using.&lt;/p&gt;
&lt;p&gt;There&apos;s also a real floor: &lt;strong&gt;0.5 ACU minimum&lt;/strong&gt; for an &quot;always-on&quot; cluster is
roughly comparable to a small &lt;code&gt;t4g&lt;/code&gt; instance&apos;s baseline cost — v2 doesn&apos;t
scale to true zero like v1 could (v2 got a scale-to-zero option later, but
it reintroduces a cold-start pause on the next connection, the exact
trade-off v2 was built to avoid at higher tiers, so it&apos;s really only sane
for genuinely idle dev environments).&lt;/p&gt;
&lt;h2&gt;The scaling-lag gotcha&lt;/h2&gt;
&lt;p&gt;Scaling is fast but not instant, and it reacts to load — it doesn&apos;t
predict it. A workload that goes from idle to a hard spike in under a
second (a flash-sale-style traffic burst) can briefly get throttled or see
elevated latency while Aurora scales up to meet it, because there&apos;s a real
control loop with a reaction time, not a pre-provisioned buffer sitting
ready. Setting a higher &lt;strong&gt;minimum ACU&lt;/strong&gt; than &quot;what average load needs&quot; gives
the scaling logic headroom to absorb bursts before it has to react, at the
cost of paying for that headroom continuously — a direct latency-versus-cost
knob, not a free win.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Watch the &lt;code&gt;ServerlessDatabaseCapacity&lt;/code&gt; CloudWatch metric against your min/
max bounds for a couple of weeks under real traffic before treating the
range as tuned. A cluster that&apos;s pinned at its max ACU most of the day isn&apos;t
&quot;elastic,&quot; it&apos;s a provisioned database paying the serverless premium —
that&apos;s the signal to either raise the max or move to provisioned capacity
for the baseline and reserve v2 for genuinely variable secondary workloads.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: AWS AppSync and its resolver-cost model</title><link>https://jstgtech.com/blog/2026-08-10-aws-appsync/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-aws-appsync/</guid><description>What managed GraphQL on AppSync buys over a hand-rolled Apollo server, how VTL and JS resolvers bill, and where subscriptions change your architecture.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GraphQL servers usually mean running (and scaling, and patching) a Node
process that resolves fields by calling out to your actual data sources.
AppSync&apos;s pitch is skipping that process entirely — you define a schema and
per-field resolvers that AppSync itself executes, with no server of yours in
the request path unless a resolver specifically needs one. That&apos;s a
meaningfully different operational model, and it comes with a cost model
that&apos;s easy to underestimate if you&apos;re used to thinking in request-per-
second server pricing.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;You upload a GraphQL schema and attach a &lt;strong&gt;resolver&lt;/strong&gt; to each field that
needs one — a small piece of logic that maps the incoming GraphQL selection
to a call against a &lt;strong&gt;data source&lt;/strong&gt;: DynamoDB, Aurora (via RDS Data API),
OpenSearch, EventBridge, HTTP endpoints, or a Lambda function for anything
custom. Resolvers used to be written in &lt;strong&gt;VTL&lt;/strong&gt; (Apache Velocity Template
Language, borrowed from API Gateway&apos;s mapping templates) — verbose,
unfamiliar syntax most teams didn&apos;t already know. AppSync now supports
&lt;strong&gt;JavaScript resolvers&lt;/strong&gt; as the default recommended option, which is a
meaningfully better developer experience if you&apos;re starting fresh; VTL still
works for existing resolvers and some advanced batching patterns.&lt;/p&gt;
&lt;p&gt;The other core piece is built-in &lt;strong&gt;real-time subscriptions&lt;/strong&gt; over
WebSockets — a client subscribes to a mutation, and AppSync pushes the
result to every subscribed client when it fires, with the pub/sub
infrastructure entirely managed. That&apos;s the feature that&apos;s genuinely hard to
replicate cheaply with a hand-rolled server; it&apos;s Socket.io-style
infrastructure you don&apos;t have to run.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DynamoDB-backed APIs&lt;/strong&gt;, especially. A direct AppSync-to-DynamoDB
resolver (no Lambda in between) is close to the cheapest, lowest-latency
path from a GraphQL query to a NoSQL read — no cold start, no
intermediate compute to size or scale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apps that need live data&lt;/strong&gt; — collaborative tools, dashboards, chat —
where subscriptions replace a polling loop or a separately-built
WebSocket service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fine-grained, field-level auth.&lt;/strong&gt; AppSync resolvers can enforce
authorization per field (via Cognito groups, IAM, Lambda authorizers, or
OIDC), so a mobile client and an admin client can query the same schema
and see different fields without maintaining two APIs or two sets of REST
endpoints.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where it&apos;s the wrong tool&lt;/h2&gt;
&lt;p&gt;If most of your resolvers end up calling a Lambda anyway (because the logic
is too custom for a direct data-source resolver), you&apos;ve mostly rebuilt
&quot;API Gateway plus Lambda&quot; with GraphQL schema validation on top — worth it
if GraphQL&apos;s client-side benefits (one round trip, client-specified shape)
matter to you, not worth it if you just wanted a managed API layer. And
teams unfamiliar with GraphQL&apos;s N+1 problem will hit it here exactly like
anywhere else: a nested field resolver that fires once per parent item
needs &lt;strong&gt;batching&lt;/strong&gt; (AppSync&apos;s batch invoke for Lambda resolvers, or DynamoDB
BatchGetItem) or it&apos;ll quietly turn one query into hundreds of downstream
calls.&lt;/p&gt;
&lt;h2&gt;The pricing gotcha&lt;/h2&gt;
&lt;p&gt;AppSync bills per &lt;strong&gt;query/mutation request&lt;/strong&gt; and, separately, per
&lt;strong&gt;resolver invocation&lt;/strong&gt; within that request — a query resolving five nested
fields is one request but potentially five (or more, with N+1) billed
resolver executions, plus real-time subscription connection-minutes and
message counts on top if you&apos;re using them. A single GraphQL query that
looks simple from the client can fan out into a surprising number of billed
resolver calls server-side; the AWS docs&apos; pricing page undersells how fast
that adds up on a deeply nested schema, and it&apos;s worth actually tracing a
representative query&apos;s resolver count before assuming the bill will track
request count linearly.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Use &lt;strong&gt;pipeline resolvers&lt;/strong&gt; to chain multiple resolution steps (an auth
check, then a DynamoDB read, then a transform) into a single resolver
attached to one field, rather than pushing that logic into nested field
resolvers on the schema — it collapses what would be several billed
resolver invocations per query into fewer, more predictable ones, and keeps
the N+1 pattern from creeping in as the schema grows.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: AWS Backup replaces per-service snapshot scripts</title><link>https://jstgtech.com/blog/2026-08-10-aws-backup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-aws-backup/</guid><description>How AWS Backup centralizes EBS, RDS, DynamoDB, and EFS backup policy into one plan, and what it still leaves you to configure yourself.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every AWS account I&apos;ve inherited that predates AWS Backup has the same
shape: a Lambda that snapshots EBS volumes on a cron, a separate RDS
automated backup window nobody&apos;s checked in years, and DynamoDB
point-in-time recovery toggled on for some tables and not others, with no
single place to see whether any of it is actually working. AWS Backup
exists to replace that patchwork with one policy.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AWS Backup&lt;/strong&gt; is a managed service that centralizes backup scheduling,
retention, and recovery across most stateful AWS resource types — EBS,
RDS/Aurora, DynamoDB, EFS, FSx, Storage Gateway volumes, and EC2 instances
as a whole (not just their volumes) — under one &lt;strong&gt;backup plan&lt;/strong&gt;. A plan
defines a schedule (cron expression), a retention period, and a
&lt;strong&gt;lifecycle&lt;/strong&gt; that can transition backups to cold storage and eventually
expire them, then you assign resources to the plan by &lt;strong&gt;tag&lt;/strong&gt; or by
explicit resource ID. Everything the plan touches shows up in one &lt;strong&gt;backup&lt;/strong&gt;
&lt;strong&gt;vault&lt;/strong&gt; with a consistent job history, so &quot;did last night&apos;s backup
actually succeed&quot; is one console view instead of checking five different
services&apos; own backup mechanisms.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One policy instead of five.&lt;/strong&gt; Tag every resource that needs a daily
backup with &lt;code&gt;backup: daily&lt;/code&gt;, assign that tag to a plan, and every new
EBS volume or RDS instance with that tag is covered automatically going
forward — no per-resource setup step anyone can forget.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-service consistency.&lt;/strong&gt; Retention and lifecycle rules are defined
once in the plan, not reimplemented per service with each service&apos;s own
quirks (RDS automated backups cap at 35 days retention on their own;
AWS Backup&apos;s retention isn&apos;t bound by that).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Backup vault lock.&lt;/strong&gt; A vault can be locked into &lt;strong&gt;compliance mode&lt;/strong&gt;,
which makes backups genuinely immutable and undeletable — including by
the account root user — for the configured retention period. That&apos;s the
feature that actually matters for ransomware resilience: an attacker
with full account access still can&apos;t delete backups a compliance-locked
vault is holding.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cross-account and cross-region copy.&lt;/strong&gt; A plan can copy backups to a
separate backup account and a separate region in the same job, which is
the AWS-native way to satisfy &quot;backups must survive the loss of the
source account&quot; without building custom replication.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What it still leaves you to configure&lt;/h2&gt;
&lt;p&gt;AWS Backup schedules and retains backups; it doesn&apos;t validate that a
restore actually works. &lt;strong&gt;Restore testing&lt;/strong&gt; is a separate feature (Restore
Testing plans) that you have to opt into explicitly and point at real
resources — untested backups are a common source of &quot;we had backups but
the restore didn&apos;t work&quot; incidents, and AWS Backup existing doesn&apos;t change
that unless you actually run the restore tests.&lt;/p&gt;
&lt;p&gt;It also doesn&apos;t replace &lt;strong&gt;application-consistent&lt;/strong&gt; backup logic for
databases that need it. RDS/Aurora snapshots through AWS Backup use the
same underlying mechanism as native RDS snapshots (so they&apos;re
crash-consistent, and for most engines that&apos;s sufficient), but if you&apos;re
backing up something like a self-managed database on EC2 that needs
quiesce-before-snapshot logic, AWS Backup&apos;s EC2/EBS support won&apos;t give you
that for free — you still need your own pre-snapshot hooks.&lt;/p&gt;
&lt;h2&gt;The pricing model&lt;/h2&gt;
&lt;p&gt;Costs are backup storage (billed per GB-month, warm vs cold tier priced
differently) plus, for some resource types, restore costs. The lifecycle
transition to cold storage (available for EBS, RDS, DynamoDB, and a few
others) meaningfully cuts long-retention costs, but cold-tier backups
typically have a &lt;strong&gt;minimum retention period before transition&lt;/strong&gt; and can
carry early-deletion or restore-time costs, so a plan built for
&quot;retain 7 years, transition to cold at 90 days&quot; needs those constraints
modeled, not just the headline GB-month price.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Turn on &lt;strong&gt;AWS Backup Audit Manager&lt;/strong&gt; (a separate but related feature) once
you have plans running — it continuously evaluates whether resources are
actually covered by a backup plan matching your organization&apos;s policy and
flags drift, which catches the &quot;someone launched a new RDS instance
without the required tag&quot; gap that a plan alone won&apos;t.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: AWS Config for compliance-as-code and drift detection</title><link>https://jstgtech.com/blog/2026-08-10-aws-config/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-aws-config/</guid><description>How AWS Config records resource configuration history and evaluates it against rules continuously, and why it pairs with — not replaces — Terraform.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Terraform tells you what infrastructure &lt;em&gt;should&lt;/em&gt; look like at apply time.
It doesn&apos;t tell you what changed at 3am when someone with console access
manually flipped a security group rule to unblock themselves and forgot to
revert it. That gap — configuration drift between what you declared and
what&apos;s actually running — is what AWS Config is built to close.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;AWS Config&lt;/strong&gt; continuously records the configuration state of supported
resources in your account (over 300 resource types) as a timestamped
history, and evaluates that state against &lt;strong&gt;Config rules&lt;/strong&gt; — either AWS
managed rules (&lt;code&gt;s3-bucket-public-read-prohibited&lt;/code&gt;,
&lt;code&gt;restricted-ssh&lt;/code&gt;, &lt;code&gt;iam-password-policy&lt;/code&gt;) or custom rules backed by your
own Lambda function or Guard/CloudFormation Guard policy. Every recorded
change produces a &lt;strong&gt;configuration item&lt;/strong&gt;, and Config keeps a full
timeline, so you can ask &quot;what did this security group&apos;s rules look like
at 2:14pm last Tuesday&quot; and get an actual answer, not a guess reconstructed
from CloudTrail events.&lt;/p&gt;
&lt;p&gt;Rules run either on a schedule (periodic) or triggered by a configuration
change (change-triggered), and each evaluation produces a
&lt;strong&gt;compliant/non-compliant&lt;/strong&gt; verdict per resource that shows up in one
dashboard across the account — or, aggregated, across an entire
Organization via &lt;strong&gt;Config aggregators&lt;/strong&gt;.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Drift detection that isn&apos;t tied to your IaC tool.&lt;/strong&gt; Config doesn&apos;t
know or care that a resource was created by Terraform — it evaluates
whatever&apos;s actually running against your rules, which catches manual
console changes, break-glass fixes that weren&apos;t reverted, and changes
made by tools outside your Terraform state entirely.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Point-in-time configuration history for incident response.&lt;/strong&gt; &quot;What
changed on this resource in the hour before the incident&quot; is a Config
timeline query, which is a much faster starting point than paging
through raw CloudTrail events trying to reconstruct resource state by
hand.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Automated remediation.&lt;/strong&gt; A non-compliant finding can trigger an SSM
Automation document via &lt;strong&gt;remediation actions&lt;/strong&gt; — auto-revoking a
security group rule that opens 0.0.0.0/0 on port 22, for example — so
drift doesn&apos;t just get flagged, it gets fixed without a human in the
loop for well-understood violations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organization-wide compliance posture in one place.&lt;/strong&gt; Conformance
packs bundle related rules (CIS benchmark, PCI-DSS-aligned checks) and
deploy across every account in an Organization via an aggregator,
giving one compliance score instead of per-account spot checks.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where it doesn&apos;t replace Terraform (or vice versa)&lt;/h2&gt;
&lt;p&gt;Config is &lt;strong&gt;observational and reactive&lt;/strong&gt; — it tells you what&apos;s true now
and evaluates it against policy, but it doesn&apos;t prevent a bad change from
happening in the first place the way a Terraform plan/apply gate or an SCP
does. And Terraform&apos;s state file, while it also describes resource
configuration, only reflects what Terraform itself last applied — it goes
stale the moment something changes outside Terraform, which is exactly
the blind spot Config is designed to catch. The two are complementary:
Terraform (plus policy-as-code gates like Checkov) prevents drift at
apply time; Config catches whatever gets through anyway, including
changes that never went through Terraform at all.&lt;/p&gt;
&lt;h2&gt;The pricing model&lt;/h2&gt;
&lt;p&gt;Config bills per &lt;strong&gt;configuration item recorded&lt;/strong&gt; and per **rule
evaluation**, both of which scale with account activity and resource
count — a large, churny account with many resource types and frequent
changes will rack up config-item costs faster than a small, stable one.
Turning on recording for every supported resource type across every
account in an Organization without checking projected volume first is the
most common way a Config bill surprises someone; scope the recorder to
resource types you actually intend to have rules for if cost is a
concern, rather than defaulting to &quot;record everything.&quot;&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Start with a &lt;strong&gt;conformance pack&lt;/strong&gt; for a benchmark you already care about
(the AWS-provided CIS or Foundational Security Best Practices packs are
good defaults) instead of hand-picking individual managed rules — it gets
you broad, curated coverage immediately, and you can prune or add rules
from there once you see what&apos;s actually flagging in your account.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: AWS Fargate and its per-task pricing surprises</title><link>https://jstgtech.com/blog/2026-08-10-aws-fargate/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-aws-fargate/</guid><description>What you actually give up going serverless with Fargate over EC2-backed ECS, how per-task vCPU/memory billing adds up, and where Fargate Spot fits.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;Just use Fargate&quot; is the default answer to &quot;how do I run this container&quot;
often enough that it&apos;s worth being precise about what it trades away, not
just what it saves. It&apos;s not free serverless magic — it&apos;s EC2 capacity
management moved from your team to AWS, billed per task instead of per
instance, and that shift changes both your ops burden and your cost curve.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;Fargate is a launch type for &lt;strong&gt;ECS&lt;/strong&gt; (and EKS) that removes the EC2 instance
layer entirely. With the EC2 launch type, you manage a cluster of instances,
size them, patch them, and bin-pack tasks onto them yourself (or via Cluster
Autoscaler-style capacity providers). With Fargate, you specify vCPU and
memory per &lt;strong&gt;task definition&lt;/strong&gt;, and AWS runs each task on its own
right-sized, isolated compute — no instances to see, patch, or pack. You
still define everything else about the container (image, environment,
networking, IAM task role) exactly like EC2-backed ECS; only the compute
layer underneath changes.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spiky or unpredictable workloads.&lt;/strong&gt; A batch job that runs for ten
minutes an hour doesn&apos;t need a warm EC2 fleet sized for peak sitting idle
the other fifty minutes. Fargate tasks start in roughly 30-60 seconds and
you pay only for the vCPU/memory-seconds actually consumed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teams without dedicated infra headcount.&lt;/strong&gt; No AMI patching pipeline, no
instance-type selection exercise, no capacity provider tuning. That&apos;s a
real operational cost removed, not just a marketing line.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Per-task network isolation.&lt;/strong&gt; Every Fargate task gets its own elastic
network interface, so security groups apply at the task level naturally —
useful for multi-tenant workloads where you don&apos;t want tasks sharing a
host&apos;s network namespace.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where EC2-backed ECS still wins&lt;/h2&gt;
&lt;p&gt;Fargate&apos;s per-vCPU-hour and per-GB-hour pricing is meaningfully higher than
the equivalent on-demand EC2 price for the same resources — AWS is charging
for not having to manage the instance, and that premium is real. For
&lt;strong&gt;steady-state, high-utilization workloads&lt;/strong&gt; (a fleet of API servers running
24/7 at 60-80% CPU), a well-managed EC2-backed ECS cluster with Reserved
Instances or Savings Plans underneath is usually meaningfully cheaper for
the same compute — you&apos;re just paying someone (your own team) in ops time
instead of paying AWS in margin. The crossover point depends on utilization
and how much that ops time actually costs, but &quot;always Fargate&quot; and &quot;always
EC2&quot; are both wrong defaults; it&apos;s a per-workload call.&lt;/p&gt;
&lt;p&gt;Fargate also has hard ceilings EC2 doesn&apos;t: max 4 vCPU / 30 GB memory per
task on the standard configuration tier (higher limits exist but require
opt-in and aren&apos;t universally available), no GPU support, and no control
over the underlying kernel or host-level tuning (sysctls, huge pages) that
some latency-sensitive workloads need.&lt;/p&gt;
&lt;h2&gt;Fargate Spot&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Fargate Spot&lt;/strong&gt; runs tasks on spare capacity at up to a 70% discount versus
on-demand Fargate pricing, with the same two-minute interruption warning
model as EC2 Spot. It&apos;s a good fit layered into an ECS &lt;strong&gt;capacity&lt;/strong&gt;
&lt;strong&gt;provider strategy&lt;/strong&gt; — e.g., a &lt;code&gt;base&lt;/code&gt; count of on-demand Fargate tasks to
guarantee minimum capacity, with everything above that scaled on Fargate
Spot. That works well for stateless, horizontally-scaled services behind a
load balancer where losing one task briefly just means the ALB stops
routing to it and ECS replaces it; it&apos;s a poor fit for long-running batch
jobs that don&apos;t checkpoint, since an interruption mid-job means starting
over.&lt;/p&gt;
&lt;h2&gt;The pricing gotcha&lt;/h2&gt;
&lt;p&gt;Fargate bills per-task at &lt;strong&gt;1-second granularity with a 1-minute minimum&lt;/strong&gt;,
and — this is the part people miss — vCPU and memory are billed
&lt;strong&gt;independently&lt;/strong&gt; at their configured amounts, not at actual usage. A task
defined with 2 vCPU / 4 GB but that only ever uses 0.5 vCPU still bills for
the full 2 vCPU the whole time it&apos;s running. Over-provisioning task
definitions &quot;to be safe&quot; is the single most common way Fargate bills come in
higher than expected — right-size against actual CloudWatch Container
Insights utilization, not a guess, and revisit it after the workload has run
for a few weeks under real traffic.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;If you&apos;re running dozens of small, short-lived tasks, check whether
&lt;strong&gt;ECS Service Connect&lt;/strong&gt; or batching multiple containers into one task
definition (as sidecars sharing the task&apos;s vCPU/memory allocation) reduces
total billed task-time versus one task per container — the per-task minimum
billing granularity means many tiny tasks can cost more in aggregate than
fewer, right-sized ones doing the same work.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: AWS Step Functions for real workflows</title><link>https://jstgtech.com/blog/2026-08-10-aws-step-functions/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-aws-step-functions/</guid><description>When AWS Step Functions earns its keep over a pile of Lambda glue code, where its per-transition pricing bites, and a Map state trick worth knowing.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every few months I see a team reinvent Step Functions badly: a Lambda that
invokes another Lambda, wrapped in a &lt;code&gt;try/except&lt;/code&gt; that retries three times
and then writes a row to DynamoDB so a cron job can poll for stuck items.
That&apos;s a state machine with extra steps — literally. Step Functions is AWS&apos;s
managed orchestrator for exactly this shape of problem, and it&apos;s worth
knowing precisely when it earns its keep and when it&apos;s overkill.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;A Step Function is a JSON (or YAML, via the newer workflow studio) state
machine defined in &lt;strong&gt;Amazon States Language&lt;/strong&gt;. Each state does one thing —
invoke a Lambda, call another AWS service directly via an &quot;SDK integration,&quot;
branch on a condition, wait, fan out over a list, or hand off to a human
approval step — and the service itself handles the transitions, retries,
timeouts, and error handling between them. Execution history is retained and
visualized automatically, so when something fails at 2am you get a diagram
with a red X on the exact state that broke, not a pile of CloudWatch Logs
you have to stitch together by request ID.&lt;/p&gt;
&lt;p&gt;There are two flavors, and picking the right one matters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Standard workflows&lt;/strong&gt; are built for long-running, auditable processes.
Exactly-once execution, up to a year of runtime, full execution history
retained in the console. Priced per &lt;strong&gt;state transition&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Express workflows&lt;/strong&gt; are built for high-volume, short-duration work
(under 5 minutes) — think per-request orchestration behind an API. At-least-once
execution, no persistent execution history in the console (it goes to
CloudWatch Logs instead, which you pay for separately), priced per
&lt;strong&gt;invocation duration and memory&lt;/strong&gt;, closer to Lambda&apos;s pricing model.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using Standard for a workflow that fires 50,000 times a day doing simple
API-to-API glue work is the single most common way people get an
unpleasantly large Step Functions bill.&lt;/p&gt;
&lt;h2&gt;When to reach for it&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multi-step processes with real failure modes&lt;/strong&gt; — order fulfillment,
video transcoding pipelines, ML training/inference chains, anything with a
&quot;do A, then B, and if B fails, do C instead of D&quot; shape. Encoding that in
nested Lambda try/except blocks gets unreadable fast; a state machine
makes the actual business logic visible as a diagram.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fan-out/fan-in work.&lt;/strong&gt; The &lt;code&gt;Map&lt;/code&gt; state runs a step over every item in an
array — in Distributed Map mode, up to 10,000 concurrent child executions
reading directly from S3 or a JSON array, without you writing a single
line of concurrency-control code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long-running processes that need to survive restarts.&lt;/strong&gt; A Standard
workflow waiting on a human approval, an external webhook, or a batch job
that takes six hours doesn&apos;t cost you anything while it&apos;s waiting — Step
Functions isn&apos;t polling, it&apos;s holding state and will resume the instant a
callback token comes back.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Direct AWS SDK integrations.&lt;/strong&gt; Step Functions can call over 200 AWS
services&apos; APIs directly from a state definition — start a Glue job, put an
item in DynamoDB, publish to SNS — with no Lambda in between. That&apos;s one
less function to deploy, monitor, and patch for what&apos;s really just a
passthrough API call.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;When NOT to reach for it&lt;/h2&gt;
&lt;p&gt;If your &quot;workflow&quot; is two steps with no meaningful failure branching — call
API A, then call API B with A&apos;s result — you don&apos;t need a state machine, you
need a Lambda function or even just synchronous code in your existing
service. The overhead of authoring, deploying, and versioning a state
machine definition isn&apos;t worth it for something a single function handles in
ten lines. And for very high-throughput, sub-second, simple orchestration
(think: per-request routing in a hot path), Express workflows can work, but
you&apos;re often better served by keeping that logic in application code and
reserving Step Functions for where the auditability and visual execution
history actually pay off.&lt;/p&gt;
&lt;h2&gt;The pricing gotcha&lt;/h2&gt;
&lt;p&gt;Standard workflows charge &lt;strong&gt;per state transition&lt;/strong&gt;, not per execution — and
transitions add up faster than people expect. A &lt;code&gt;Map&lt;/code&gt; state iterating over
1,000 items, each running three sequential states, is 3,000 transitions in
one execution, not one. At $0.025 per 1,000 transitions, that&apos;s cheap in
isolation, but a workflow that fans out over large datasets on a frequent
schedule can quietly become one of the more expensive things in an account.
The fix isn&apos;t to avoid &lt;code&gt;Map&lt;/code&gt; — it&apos;s to use &lt;strong&gt;Distributed Map&lt;/strong&gt;, which counts
child workflow executions differently and is built for exactly this
high-fan-out case, plus to actually look at the &quot;state transitions&quot; line
item in Cost Explorer before a workflow goes from a proof of concept to a
production schedule running every five minutes.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Use &lt;code&gt;ResultSelector&lt;/code&gt; and &lt;code&gt;OutputPath&lt;/code&gt; inside state definitions to trim what
gets passed downstream instead of piping entire upstream payloads (including
that giant DynamoDB item or Lambda response) through every subsequent state.
Standard workflow execution history and each state&apos;s input/output are
capped at 256 KB — passing bloated payloads through unfiltered is the most
common way people hit that limit and get a cryptic
&lt;code&gt;States.DataLimitExceeded&lt;/code&gt; error on a workflow that&apos;s otherwise working
fine.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: CloudFront Functions vs Lambda@Edge</title><link>https://jstgtech.com/blog/2026-08-10-cloudfront-functions-vs-lambda-edge/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-cloudfront-functions-vs-lambda-edge/</guid><description>Picking between the two edge-compute options on CloudFront — runtime limits, latency, pricing, and which one actually fits a URL rewrite versus an origin call.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;CloudFront gives you two different ways to run code at the edge, and they&apos;re
not tiers of the same thing — they&apos;re built for genuinely different jobs,
with different runtime models, different latency profiles, and a pricing
gap wide enough that picking the wrong one for a simple task is a real cost
mistake, not just a style preference.&lt;/p&gt;
&lt;h2&gt;The two options&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CloudFront Functions&lt;/strong&gt; runs a restricted subset of JavaScript in a
lightweight, purpose-built runtime embedded directly in CloudFront&apos;s edge
locations — sub-millisecond execution, no cold starts, and it can run on
&lt;strong&gt;viewer request/response&lt;/strong&gt; events only (the events closest to the end
user, before/after CloudFront&apos;s cache). It has hard constraints: no network
access, no filesystem access, a 10 KB code size limit at the &quot;basic&quot; compute
tier (2 MB on the newer &quot;advanced&quot; tier, which raised limits and added
things like key-value store access), and a 1 MB max HTTP request/response
size to operate on.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lambda@Edge&lt;/strong&gt; runs actual Lambda functions (Node.js or Python, your
choice of runtime and package) at CloudFront edge locations, and can run on
all four CloudFront event types — viewer request, &lt;strong&gt;origin request&lt;/strong&gt;,
&lt;strong&gt;origin response&lt;/strong&gt;, and viewer response — with network access, larger code
packages, and access to other AWS services from within the function. It has
real cold starts (worse than standard Lambda, since it&apos;s replicating across
edge regions) and materially higher per-invocation and per-GB-second
pricing than CloudFront Functions.&lt;/p&gt;
&lt;h2&gt;Picking based on the job, not the label&lt;/h2&gt;
&lt;p&gt;The deciding question isn&apos;t &quot;which is more powerful&quot; — Lambda@Edge always
wins that comparison — it&apos;s **does this task need origin access or
first-party AWS service calls**. If the answer is no, CloudFront Functions
is almost always the right choice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;URL rewrites and redirects&lt;/strong&gt; (the trailing-slash/&lt;code&gt;index.html&lt;/code&gt; rewrite
pattern static sites need for pretty URLs) — pure string manipulation on
the request, no origin needed. CloudFront Functions territory, and
meaningfully cheaper and faster than Lambda@Edge for exactly this job.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Header manipulation&lt;/strong&gt; — adding security headers, normalizing a header
for cache-key purposes, stripping a header before it hits the origin.
Same story: no origin call needed, CloudFront Functions handles it at a
fraction of the cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A/B testing via cookie-based routing, viewer-side auth token&lt;/strong&gt;
**  validation** (checking a JWT&apos;s signature without calling out to a
service) — still viewer-request/response only, still fits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reach for &lt;strong&gt;Lambda@Edge&lt;/strong&gt; only when the task genuinely needs something
CloudFront Functions structurally can&apos;t do:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Calling another AWS service or an external API&lt;/strong&gt; from inside the
function (looking up a value in DynamoDB to decide which origin to route
to) — CloudFront Functions has no network access at all, full stop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Modifying the origin request/response&lt;/strong&gt;, not just the viewer-facing
side — e.g., rewriting the request CloudFront sends to a custom origin
based on logic too complex for a viewer-request rewrite alone.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Larger dependencies or non-JS runtimes&lt;/strong&gt; — image manipulation libraries,
anything needing Python or a sizable npm dependency tree that won&apos;t fit
CloudFront Functions&apos; size ceiling.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The pricing and latency gap&lt;/h2&gt;
&lt;p&gt;CloudFront Functions is priced per &lt;strong&gt;invocation&lt;/strong&gt;, at a rate roughly two
orders of magnitude cheaper than Lambda@Edge&apos;s per-invocation-plus-duration
pricing, and it runs with no cold start because it&apos;s not a full Lambda
execution environment being spun up — it&apos;s closer to a purpose-built string-
processing VM than a general compute runtime. For a function firing on
every single request to a high-traffic distribution (which URL-rewrite and
header functions typically do), that pricing gap is not academic — running
a basic redirect rule as Lambda@Edge instead of a CloudFront Function on a
busy site can be the difference between a rounding-error cost and a
noticeable line item.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Default to CloudFront Functions and only reach for Lambda@Edge when you hit
one of its actual constraints (no network access, code size, event type) —
not the other way around. It&apos;s easy to reach for Lambda@Edge out of
familiarity with Lambda generally, but for the viewer-request rewrite and
header-manipulation jobs that make up most CloudFront edge-compute use
cases, it&apos;s strictly worse on both cost and latency for no functional gain.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: querying years of audit history with CloudTrail Lake</title><link>https://jstgtech.com/blog/2026-08-10-cloudtrail-lake/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-cloudtrail-lake/</guid><description>How CloudTrail Lake lets you SQL-query months of API activity without standing up Athena and Glue yourself, and where its pricing model changes the calculus.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;The standard CloudTrail setup — trail to S3, Athena table on top, manual
partition projection — works, but it&apos;s a pipeline you own: someone has to
get the Glue table schema right, keep partitions current, and remember it
exists when an incident finally needs it at 2am. CloudTrail Lake exists to
delete that pipeline.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;CloudTrail Lake&lt;/strong&gt; is a managed, queryable event store for CloudTrail data.
You create an &lt;strong&gt;event data store&lt;/strong&gt;, point it at management events, data
events, or both, across one account or an entire AWS Organization, and AWS
ingests, indexes, and retains that data for up to &lt;strong&gt;seven years&lt;/strong&gt; without
you touching S3 or Glue. Querying is plain &lt;strong&gt;SQL&lt;/strong&gt; against the event data
store through the CloudTrail console or the &lt;code&gt;cloudtrail-data&lt;/code&gt; API — no
Athena table definitions, no partition maintenance, no worrying that a
schema drift in raw JSON broke your queries.&lt;/p&gt;
&lt;p&gt;Under the hood it&apos;s still built on the same event structure as classic
CloudTrail-to-S3, so anything you already know about CloudTrail event
fields (&lt;code&gt;eventName&lt;/code&gt;, &lt;code&gt;sourceIPAddress&lt;/code&gt;, &lt;code&gt;userIdentity&lt;/code&gt;, &lt;code&gt;requestParameters&lt;/code&gt;)
transfers directly — the difference is entirely in ingestion and query
ergonomics, not in what&apos;s being captured.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Incident response and forensics.&lt;/strong&gt; &quot;Who called &lt;code&gt;DeleteBucket&lt;/code&gt; in the
last six months&quot; is a &lt;code&gt;SELECT&lt;/code&gt; with a &lt;code&gt;WHERE&lt;/code&gt; clause instead of an Athena
table setup exercise done under pressure during an active incident. That
difference matters most exactly when you have the least patience for
infrastructure work.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Organization-wide queries.&lt;/strong&gt; An event data store can aggregate events
from every account in an AWS Organization into one queryable store,
which is a real improvement over stitching together per-account S3
buckets and cross-account Athena access.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long retention without a lifecycle policy to babysit.&lt;/strong&gt; Seven years of
retention is configured once at store creation, not maintained via an S3
lifecycle rule someone has to remember not to break.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Federated queries via generative AI tools.&lt;/strong&gt; CloudTrail Lake events can
be queried through Amazon Q and Bedrock Agents&apos; natural-language
interfaces for teams that want &quot;show me anomalous IAM activity this
week&quot; without writing SQL by hand — useful for on-call engineers who
aren&apos;t CloudTrail SQL experts.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where it doesn&apos;t replace the classic setup&lt;/h2&gt;
&lt;p&gt;CloudTrail Lake is not a replacement for a trail delivering to S3 if you
need that data available to &lt;strong&gt;other&lt;/strong&gt; tooling — a SIEM ingesting raw
CloudTrail JSON from S3, a security data lake feeding a different query
engine, or a compliance requirement for immutable S3 object storage with
Object Lock. Lake&apos;s event data store is queryable through its own API and
console, not a general-purpose object store other systems can read from
directly. Most mature setups run both: a trail to S3 for downstream
tooling and archival, and an event data store for ad hoc SQL investigation.&lt;/p&gt;
&lt;h2&gt;The pricing model is the actual decision&lt;/h2&gt;
&lt;p&gt;CloudTrail Lake bills per &lt;strong&gt;GB ingested&lt;/strong&gt;, with a choice between two
pricing options at event-data-store creation: &lt;strong&gt;one-year extendable&lt;/strong&gt;
&lt;strong&gt;retention&lt;/strong&gt; (higher ingestion price, retention extendable up to 7 years,
data can be exported) or &lt;strong&gt;seven-year retention&lt;/strong&gt; (lower ingestion price,
fixed at 7 years, no export). That choice is set per store and isn&apos;t
something you casually change later, so decide upfront whether you&apos;ll ever
need to export the underlying data — if there&apos;s any chance you will, pay
for the extendable tier even though it costs more per GB, because
re-ingesting historical data into a differently-configured store isn&apos;t an
option.&lt;/p&gt;
&lt;p&gt;For accounts with high API call volume (a busy CI/CD pipeline hitting AWS
APIs constantly, or verbose data events on S3/Lambda), ingestion costs can
add up fast — model this against your actual CloudTrail event volume
before turning on data events across every account in an Organization,
rather than discovering the bill after the fact.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Start with &lt;strong&gt;management events only&lt;/strong&gt; in the event data store, and add
&lt;strong&gt;data events&lt;/strong&gt; (S3 object-level, Lambda invocations) selectively for the
handful of buckets or functions where object-level audit trail actually
matters — data events are dramatically higher volume than management
events, and are the single most common reason a CloudTrail Lake bill comes
in far higher than expected.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: Kinesis Data Streams, shards, and when SQS was simpler</title><link>https://jstgtech.com/blog/2026-08-10-kinesis-data-streams/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-kinesis-data-streams/</guid><description>What shards and fan-out actually buy you over SQS, where consumer scaling gets tricky, and the honest case for not reaching for Kinesis by default.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I&apos;ve seen more than one team reach for Kinesis Data Streams because
&quot;streaming&quot; sounded like the right word for their use case, then spend
weeks fighting shard math for a workload that one SQS queue and a Lambda
would have handled with a fraction of the operational overhead. Kinesis is
the right tool for a specific shape of problem — it&apos;s worth being precise
about what that shape is before picking it.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Kinesis Data Streams&lt;/strong&gt; is an append-only, partitioned log: producers
write records to a stream, the stream is divided into &lt;strong&gt;shards&lt;/strong&gt;, and each
shard is an ordered sequence that &lt;strong&gt;multiple independent consumers&lt;/strong&gt; can
read from the same position without deleting records for each other. That
last part is the core difference from SQS: an SQS message is deleted once
a consumer processes it, so only one logical consumer group gets each
message, whereas a Kinesis record stays in the stream (default 24 hours,
extendable to 365 days) and any number of consumer applications can read
the full history independently, at their own pace, from their own
checkpoint.&lt;/p&gt;
&lt;p&gt;Each shard supports up to &lt;strong&gt;1 MB/sec or 1,000 records/sec&lt;/strong&gt; of writes and
&lt;strong&gt;2 MB/sec&lt;/strong&gt; of reads (5 reads/sec) in standard consumer mode, or up to
&lt;strong&gt;2 MB/sec per shard per consumer&lt;/strong&gt; with &lt;strong&gt;enhanced fan-out&lt;/strong&gt;, which gives
each registered consumer a dedicated read throughput instead of sharing
the base 2 MB/sec across all of them.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Multiple independent consumers of the same event stream.&lt;/strong&gt; A
clickstream that needs to feed a real-time dashboard, a fraud-detection
pipeline, and a data lake ingestion job simultaneously — each reading
the full stream independently — is exactly what Kinesis&apos;s replay-without-
deletion model is for. SQS would need a fan-out pattern (SNS to multiple
queues) to approximate this, and even then each queue consumer still
deletes its own copy rather than sharing an ordered log.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Strict ordering within a partition key.&lt;/strong&gt; Records with the same
partition key always land in the same shard and are delivered in the
order written. SQS FIFO queues offer ordering too, but Kinesis&apos;s
partition-key-to-shard model scales that ordering guarantee across much
higher throughput than a single FIFO message group.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replay and reprocessing.&lt;/strong&gt; Because records persist in the stream for
the configured retention window, a consumer that falls behind or a new
consumer version that needs to reprocess history from an earlier point
can do so — you can&apos;t rewind an SQS queue.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where SQS is genuinely simpler&lt;/h2&gt;
&lt;p&gt;If you have &lt;strong&gt;one logical consumer&lt;/strong&gt; (or a consumer group where each
message should be handled exactly once by exactly one worker), SQS is
less to operate: no shard count to manage, no manual scaling decision when
throughput grows, and its default at-least-once, auto-scaling queue model
just works without capacity planning. Kinesis in &lt;strong&gt;provisioned mode&lt;/strong&gt;
requires you to explicitly resize (split or merge) shards as throughput
changes — under-provisioned shards throttle producers with
&lt;code&gt;ProvisionedThroughputExceededException&lt;/code&gt;, and over-provisioned shards are
paying for capacity you don&apos;t use. &lt;strong&gt;On-demand mode&lt;/strong&gt; removes manual shard
management (Kinesis scales automatically based on observed throughput,
within limits), trading that operational burden for a meaningfully higher
per-stream price than provisioned mode at low-to-moderate volume.&lt;/p&gt;
&lt;p&gt;Consumer-side, Kinesis Client Library (KCL) applications carry more
operational surface than an SQS consumer loop — checkpointing, lease
management via a DynamoDB table KCL creates for you, and shard rebalancing
on scale-out are all things that can silently misbehave (a consumer
falling behind and not alarming on &lt;code&gt;IteratorAge&lt;/code&gt; is the classic failure
mode) in ways a plain SQS &lt;code&gt;ReceiveMessage&lt;/code&gt; loop doesn&apos;t have to think
about at all.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Before reaching for Kinesis, ask whether you actually need **multiple
independent consumers replaying the same ordered stream**. If the honest
answer is &quot;no, I have one processing pipeline,&quot; SQS (or SQS FIFO, if you
need ordering within a single consumer group) is very likely the simpler,
cheaper, lower-maintenance choice — save Kinesis for when the fan-out and
replay semantics are the actual requirement, not just the more
impressive-sounding service name.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: cheap DNS-level failover with Route 53 health checks</title><link>https://jstgtech.com/blog/2026-08-10-route53-health-checks-failover/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-route53-health-checks-failover/</guid><description>How Route 53 health checks and failover routing build automatic DNS failover without a load balancer in front, and where DNS TTLs limit how fast it actually is.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Multi-region failover has a reputation for requiring a global load
balancer, cross-region routing infrastructure, and a meaningful
engineering lift. For a lot of workloads — a static site with a backup
origin, an API with a standby region, anything where &quot;route traffic away
from the broken endpoint&quot; is the whole requirement — Route 53 health
checks and failover routing get you most of the way there with nothing
more than DNS records.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;A &lt;strong&gt;Route 53 health check&lt;/strong&gt; polls an endpoint (HTTP, HTTPS, or TCP) on an
interval (default 30 seconds, or 10 seconds for the &quot;fast&quot; option) from a
distributed set of AWS health checker locations worldwide, and marks the
endpoint healthy or unhealthy based on a configurable failure threshold —
requiring multiple consecutive failures across multiple checker locations
before flipping status, which avoids a single flaky network path in one
region triggering an unnecessary failover.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Failover routing policy&lt;/strong&gt; then ties DNS answers to that health check
status: you create a &lt;strong&gt;primary&lt;/strong&gt; record pointing at your main endpoint and
associate it with the health check, and a &lt;strong&gt;secondary&lt;/strong&gt; record pointing at
a backup endpoint. While the primary&apos;s health check passes, Route 53
answers DNS queries with the primary record. The moment the health check
fails, Route 53 stops returning the primary record and starts answering
with the secondary — no load balancer, no application-level failover
logic, just DNS resolving to a different answer once the health check
trips.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Static or largely static sites with a backup origin.&lt;/strong&gt; A CloudFront
distribution normally in front of an S3 bucket, with a health check on
the S3 origin and a failover record pointing at a backup S3 bucket in
another region (or even a &quot;we&apos;re down&quot; static page hosted elsewhere) —
cheap insurance against a regional S3 outage with no compute involved.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API failover across regions without a global load balancer.&lt;/strong&gt; For
workloads where eventual consistency of DNS propagation is acceptable
(see the TTL caveat below), pointing a primary record at a
region-A API Gateway/ALB and a secondary at region B, health-checked on
each, is meaningfully simpler to set up and reason about than Global
Accelerator or a multi-region Application Load Balancer setup, if
anycast-level failover speed isn&apos;t required.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Health checks that inspect more than &quot;is it up.&quot;&lt;/strong&gt; A health check can
match on a specific string in the response body, not just a 200 status
— useful for catching an endpoint that&apos;s returning 200 with a
degraded/error payload, which a naive load balancer health check
sometimes misses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Composable with other routing policies.&lt;/strong&gt; Failover pairs with
weighted, latency-based, and geolocation routing at the record level
(e.g., latency-based routing across regions, each of which has its own
failover pair underneath), so you&apos;re not limited to a single global
primary/secondary if the topology is more complex.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The catch: DNS TTL and caching&lt;/h2&gt;
&lt;p&gt;DNS failover is only as fast as clients actually &lt;strong&gt;re-resolve&lt;/strong&gt; the
record. A record with a 300-second TTL means some fraction of clients —
and, more unpredictably, any resolver or client that doesn&apos;t strictly
honor TTL — keep hitting the now-dead primary for up to that TTL after
failover triggers, sometimes longer with misbehaving caching resolvers.
&lt;strong&gt;Set a low TTL (30-60 seconds) on records used for failover&lt;/strong&gt; well before
you need it — TTL changes themselves take time to propagate, since
resolvers that already cached the old TTL keep using it until their
current cache entry expires, so this isn&apos;t something you can fix in the
moment of an actual outage.&lt;/p&gt;
&lt;p&gt;Health check evaluation itself also isn&apos;t instant: with the default
30-second interval and default failure threshold, detecting an outage and
flipping DNS can take on the order of a minute or two end to end, which
is well short of what many RTO targets actually need for a fully
automated failover — model that latency into your DR plan rather than
assuming &quot;we have failover configured&quot; means &quot;we have sub-minute RTO.&quot;&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Create a &lt;strong&gt;calculated health check&lt;/strong&gt; (one that aggregates the status of
several child health checks with AND/OR/NOT logic) when &quot;healthy&quot; means
more than one endpoint being reachable — e.g., requiring both the API and
its database dependency&apos;s health check to pass before Route 53 considers
the primary healthy, instead of failing over on a symptom while the
actual root cause goes undetected.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: when S3 Intelligent-Tiering beats hand-rolled lifecycle rules</title><link>https://jstgtech.com/blog/2026-08-10-s3-intelligent-tiering/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-s3-intelligent-tiering/</guid><description>How Intelligent-Tiering automates storage-class transitions by access pattern, its monitoring fee, and when a plain lifecycle policy is still cheaper.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;S3 lifecycle rules that move objects to Infrequent Access after 30 days
and Glacier after 90 are a reasonable default when access patterns are
predictable. They&apos;re a bad fit when they&apos;re not — and &quot;predictable access
pattern&quot; is an assumption worth questioning for a lot of buckets that
actually hold a mix of hot and cold data with no clean age-based line
between them.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;S3 Intelligent-Tiering&lt;/strong&gt; is a storage class that automatically moves
objects between access tiers based on &lt;strong&gt;observed access patterns&lt;/strong&gt;
instead of a fixed age threshold you define upfront. Objects start in the
&lt;strong&gt;Frequent Access&lt;/strong&gt; tier; after 30 consecutive days with no access, they
move to &lt;strong&gt;Infrequent Access&lt;/strong&gt; automatically; after 90 days with no access,
optionally to &lt;strong&gt;Archive Instant Access&lt;/strong&gt;; and, if you opt in to the
deeper tiers, after 180 days to &lt;strong&gt;Archive Access&lt;/strong&gt; and
&lt;strong&gt;Deep Archive Access&lt;/strong&gt; — with the key property that if an object in a
colder tier is accessed again, it moves back to Frequent Access
automatically, and there&apos;s &lt;strong&gt;no retrieval fee&lt;/strong&gt; for the Frequent,
Infrequent, or Archive Instant Access tiers (retrieval from the two
deepest archive tiers works like Glacier and does require a restore
request with retrieval time).&lt;/p&gt;
&lt;p&gt;This is the core difference from a manual lifecycle policy: a lifecycle
rule moves objects on a **fixed schedule regardless of whether they&apos;re
still being accessed**, so an object accessed on day 89 still transitions
to Glacier on day 90 if that&apos;s what the rule says, and pulling it back out
means eating a retrieval cost and (for Glacier-class tiers) a wait.
Intelligent-Tiering only moves objects that have genuinely gone cold, and
un-cools them automatically the moment access resumes.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Unpredictable or mixed access patterns.&lt;/strong&gt; A bucket holding user
uploads where some files get accessed constantly and others never
again, with no way to know in advance which is which per object, is
exactly the case a fixed-age lifecycle rule handles badly and
Intelligent-Tiering handles by design.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No operational tuning required.&lt;/strong&gt; There&apos;s no age threshold to pick,
monitor, and revisit as access patterns change over the life of the
bucket — Intelligent-Tiering adapts continuously, so a bucket&apos;s usage
pattern shifting six months from now doesn&apos;t require anyone to notice
and adjust a lifecycle rule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No retrieval fees on tier transitions within the standard tiers.&lt;/strong&gt;
Pulling an infrequently-accessed object back out doesn&apos;t carry the
same per-GB retrieval charge a Standard-IA or Glacier object would,
which matters for buckets where &quot;infrequent&quot; doesn&apos;t mean &quot;never.&quot;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where a plain lifecycle rule is still cheaper&lt;/h2&gt;
&lt;p&gt;Intelligent-Tiering charges a small &lt;strong&gt;per-object monthly monitoring and&lt;/strong&gt;
&lt;strong&gt;automation fee&lt;/strong&gt; on top of storage costs, for every object over 128 KB
(objects smaller than that are charged at Frequent Access rates and
never monitored or transitioned, since the monitoring fee would exceed
any storage savings). For a bucket with &lt;strong&gt;millions of small objects&lt;/strong&gt;, that
per-object fee adds up fast and can exceed what you&apos;d pay just leaving
everything in Standard, let alone what a manual lifecycle-to-IA rule would
cost. And if access patterns genuinely &lt;strong&gt;are&lt;/strong&gt; predictable — logs that
are always hot for 30 days and never touched again, backups that are
written once and only ever read during a disaster recovery event — a
fixed lifecycle rule with no monitoring fee at all is strictly cheaper,
because you already know the answer Intelligent-Tiering would spend money
figuring out.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Run S3 Storage Lens or check the bucket&apos;s access patterns via S3 Inventory
before choosing between the two — if you can already describe the access
pattern in one sentence (&quot;logs go cold after 30 days, always&quot;), write a
lifecycle rule. If the honest answer is &quot;it varies by object and I don&apos;t
know the split,&quot; Intelligent-Tiering is worth the monitoring fee to stop
guessing.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: wiring automatic rotation into Secrets Manager</title><link>https://jstgtech.com/blog/2026-08-10-secrets-manager-rotation/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-secrets-manager-rotation/</guid><description>How Secrets Manager rotation actually works end to end with the Lambda rotator pattern for RDS, and the failure modes that leave a secret half-rotated.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Storing a database password in Secrets Manager instead of a &lt;code&gt;.env&lt;/code&gt; file is
the easy part. The part that actually earns the &quot;we rotate credentials&quot;
line in a security questionnaire is automatic rotation — and that&apos;s where
most setups I&apos;ve reviewed stop halfway, with a secret stored but never
actually rotated because nobody wired up the rotation Lambda.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Secrets Manager&lt;/strong&gt; stores secrets encrypted with KMS and, for the
services that matter most, can &lt;strong&gt;rotate them on a schedule automatically&lt;/strong&gt;
via a &lt;strong&gt;rotation Lambda function&lt;/strong&gt; that Secrets Manager invokes. For
RDS, Aurora, DocumentDB, and Redshift, AWS provides &lt;strong&gt;pre-built rotation&lt;/strong&gt;
&lt;strong&gt;Lambda templates&lt;/strong&gt; (deployed via a SAM app from the Secrets Manager
console or CLI) that implement the standard four-step rotation process
without you writing the logic yourself:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;createSecret&lt;/strong&gt; — generate a new password and stage it as &lt;code&gt;AWSPENDING&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;setSecret&lt;/strong&gt; — set that new password on the actual database&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;testSecret&lt;/strong&gt; — verify the new credential actually authenticates&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;finishSecret&lt;/strong&gt; — promote &lt;code&gt;AWSPENDING&lt;/code&gt; to &lt;code&gt;AWSCURRENT&lt;/code&gt;, completing the
rotation&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That staged, four-step model exists specifically so a failure partway
through doesn&apos;t lock you out: &lt;code&gt;AWSCURRENT&lt;/code&gt; only moves to the new password
after &lt;code&gt;testSecret&lt;/code&gt; confirms it works, so a broken &lt;code&gt;setSecret&lt;/code&gt; step leaves
the database still accepting the old, still-&lt;code&gt;AWSCURRENT&lt;/code&gt; password rather
than stranding the app with a password nothing accepts.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;No credential ever needs to be manually rotated by a human again&lt;/strong&gt;
once it&apos;s wired up — the whole point of rotation is removing &quot;someone
remembers to change the password quarterly&quot; from the list of things
that depend on human follow-through, which is also the thing that
reliably doesn&apos;t happen.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application code stays credential-agnostic.&lt;/strong&gt; Apps fetch the current
secret value via &lt;code&gt;GetSecretValue&lt;/code&gt; at connection time (ideally through
the Secrets Manager RDS/JDBC connector libraries, which handle the
&lt;code&gt;AWSCURRENT&lt;/code&gt;/&lt;code&gt;AWSPENDING&lt;/code&gt; transition transparently) instead of having a
password baked into config, so a rotation doesn&apos;t require an app
deploy — as long as the app re-fetches rather than caching the
credential for its entire process lifetime.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Multi-user rotation strategy for zero-downtime cutover.&lt;/strong&gt; For
workloads that can&apos;t tolerate any connection using a stale credential
during rotation, Secrets Manager supports an &lt;strong&gt;alternating-user&lt;/strong&gt;
rotation strategy — two database users, rotation alternates which one is
&lt;code&gt;AWSCURRENT&lt;/code&gt;, so old connections using the previous user keep working
until they naturally cycle rather than being cut off mid-rotation.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where it goes wrong in practice&lt;/h2&gt;
&lt;p&gt;The most common failure isn&apos;t rotation itself — it&apos;s **rotation never
running successfully because the Lambda can&apos;t reach the database**. The
rotation Lambda needs network access to the database (correct VPC
subnets, security group rules allowing it in) and the Secrets Manager VPC
endpoint if the Lambda runs without internet egress; get either wrong and
rotation fails silently on schedule, over and over, until someone notices
the secret&apos;s &lt;code&gt;LastRotatedDate&lt;/code&gt; hasn&apos;t moved in months. &lt;strong&gt;Alarm on rotation&lt;/strong&gt;
&lt;strong&gt;failures explicitly&lt;/strong&gt; (CloudWatch metric filter on the rotation Lambda&apos;s
error logs, or EventBridge on &lt;code&gt;RotationFailed&lt;/code&gt;) rather than assuming &quot;I
set a rotation schedule&quot; means it&apos;s actually rotating.&lt;/p&gt;
&lt;p&gt;The other gap: rotation changes the secret in Secrets Manager and on the
database, but doesn&apos;t retroactively fix every place a credential might be
cached — a long-lived connection pool that doesn&apos;t recycle connections, or
a sidecar that read the secret once at container start and never again,
keeps using the old credential until it happens to reconnect. Rotation
strategy has to account for how long-lived your actual connections are,
not just how often the schedule fires.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Set the rotation schedule&apos;s testing in a &lt;strong&gt;non-production secret first&lt;/strong&gt;,
and deliberately break &lt;code&gt;setSecret&lt;/code&gt; (point it at a nonexistent user, say)
to confirm the four-step staging really does leave &lt;code&gt;AWSCURRENT&lt;/code&gt; untouched
on failure before you trust it against a production database credential —
verifying the failure mode is safe is worth the ten minutes it takes.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: SSH-less, bastion-less EC2 access with SSM</title><link>https://jstgtech.com/blog/2026-08-10-ssm-session-manager/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-ssm-session-manager/</guid><description>How Systems Manager Session Manager replaces bastion hosts and open SSH ports with IAM-authenticated, logged shell access, and where the agent still trips people up.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I still see security groups with port 22 open to a bastion host&apos;s IP range,
a bastion host someone has to patch, and an SSH key rotation process nobody
actually follows. Session Manager has been GA for years and solves this
completely, and yet the bastion pattern persists mostly out of habit — worth
a clear-eyed look at what actually changes when you switch.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Session Manager&lt;/strong&gt; is a feature of AWS Systems Manager that opens an
interactive shell session to an EC2 instance (or on-prem/other-cloud server
running the SSM agent) entirely over the agent&apos;s outbound HTTPS connection
to the SSM service — no inbound port needs to be open on the instance at
all, not even 22. Authentication and authorization happen through &lt;strong&gt;IAM&lt;/strong&gt;:
if your IAM identity has &lt;code&gt;ssm:StartSession&lt;/code&gt; permission (typically scoped by
resource tag) and the instance has SSM&apos;s managed policy on its instance
role, you get a shell. No SSH key to distribute, no bastion host to
maintain, no security group rule to remember to remove later.&lt;/p&gt;
&lt;p&gt;Every session is logged: session start/end, the identity that connected,
and — if you enable it — the &lt;strong&gt;full session transcript&lt;/strong&gt; to CloudWatch Logs
or S3, plus &lt;strong&gt;KMS encryption&lt;/strong&gt; of session data in transit. That&apos;s a
meaningfully better audit story than SSH access, where &quot;who ran what
command on this box&quot; usually means reconstructing it from shell history
files that a user can edit or delete.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Removing SSH entirely from your security posture.&lt;/strong&gt; No open port 22
means one less thing for a port scanner to find and one less credential
(an SSH key) to leak, rotate, or worry about ending up in a public repo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Instances with no public IP.&lt;/strong&gt; A fully private-subnet EC2 instance with
no NAT and no bastion is normally unreachable for interactive access;
Session Manager works over a &lt;strong&gt;VPC endpoint&lt;/strong&gt; (&lt;code&gt;com.amazonaws.region.ssm&lt;/code&gt;
plus the two related endpoints), so instances with zero internet egress
are still reachable for administration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Temporary, auditable access grants.&lt;/strong&gt; Because access is IAM policy, not
a distributed key, revoking someone&apos;s shell access to production is an
IAM policy change that takes effect immediately — no key rotation across
a fleet, no bastion account cleanup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Port forwarding without a bastion.&lt;/strong&gt; &lt;code&gt;aws ssm start-session&lt;/code&gt;
&lt;code&gt;  --document-name AWS-StartPortForwardingSession&lt;/code&gt; tunnels a local port to a
remote one (handy for reaching an RDS instance in a private subnet from a
laptop) without opening the database to the internet or standing up a
jump host at all.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What still trips people up&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;SSM agent&lt;/strong&gt; has to be installed, running, and able to reach the SSM
service endpoints — most current AMIs (Amazon Linux 2023, recent Ubuntu/
Windows AMIs) ship with it preinstalled, but older custom AMIs and
minimal/hardened base images often don&apos;t, and &quot;Session Manager isn&apos;t
working&quot; on those turns out to be a missing or outdated agent, not an IAM
problem. The instance also needs outbound HTTPS reachability to the SSM
endpoints — either a NAT gateway/instance, or the three SSM-related VPC
interface endpoints for fully private subnets — and forgetting the VPC
endpoints on a private-subnet instance is the most common &quot;it works in one
VPC and not another&quot; support ticket.&lt;/p&gt;
&lt;p&gt;The other gap: Session Manager solves &lt;strong&gt;interactive shell access&lt;/strong&gt;, but it&apos;s
not a substitute for scoped, least-privilege &lt;strong&gt;instance role&lt;/strong&gt; permissions.
Someone with &lt;code&gt;ssm:StartSession&lt;/code&gt; on an instance whose instance role has
broad S3 or IAM permissions still inherits that instance&apos;s blast radius once
they&apos;re in the shell — Session Manager changes how you get access, it
doesn&apos;t change what that access can do once you&apos;re there.&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Turn on &lt;strong&gt;session logging to CloudWatch Logs&lt;/strong&gt; and set a retention/alerting
policy on it from day one — the audit trail is the single biggest advantage
over SSH, and it&apos;s only useful if someone&apos;s actually watching or alarming on
it, not just accumulating in a log group nobody queries until an incident
forces the question.&lt;/p&gt;
</content:encoded></item><item><title>Service spotlight: running an AWS Well-Architected Tool review</title><link>https://jstgtech.com/blog/2026-08-10-well-architected-tool/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-well-architected-tool/</guid><description>What the Well-Architected Tool actually surfaces when you run a workload through it, where its generic questions fall short, and how to act on the output.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;&quot;We should do a Well-Architected review&quot; tends to get said in the same
tone as &quot;we should write more tests&quot; — a good idea everyone agrees with
and nobody schedules. Having actually run several of these against real
production workloads, the tool is more useful than its reputation as a
compliance checkbox exercise suggests, provided you go in knowing what it
will and won&apos;t tell you.&lt;/p&gt;
&lt;h2&gt;What it actually is&lt;/h2&gt;
&lt;p&gt;The &lt;strong&gt;AWS Well-Architected Tool&lt;/strong&gt; is a free console tool that walks a
defined &lt;strong&gt;workload&lt;/strong&gt; (you register one per application or system you want
reviewed) through a structured questionnaire organized by the **six
pillars**: Operational Excellence, Security, Reliability, Performance
Efficiency, Cost Optimization, and Sustainability. Each pillar has a set
of questions (&quot;How do you manage identity and permissions for people and
machines?&quot;), each question has a list of &lt;strong&gt;best-practice choices&lt;/strong&gt; you
select as implemented or not, and unchosen best practices become flagged
&lt;strong&gt;risk items&lt;/strong&gt; — categorized as high or medium risk — in a summary report
per pillar.&lt;/p&gt;
&lt;p&gt;The output is a &lt;strong&gt;milestone&lt;/strong&gt;: a snapshot of your answers and risk items
at a point in time, which you can re-run later and diff against to show
whether risk actually went down after remediation work, not just that a
review happened once.&lt;/p&gt;
&lt;h2&gt;Where it earns its keep&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Structured coverage across dimensions people forget under deadline&lt;/strong&gt;
**  pressure.** Reliability and cost questions in particular tend to get
skipped in day-to-day feature work; walking through all six pillars on
a schedule forces a periodic look at &quot;do we have alarms on this,&quot; &quot;is
this single-AZ,&quot; and &quot;why is this instance still on-demand&quot; even when
no one&apos;s actively fighting a fire in those areas.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A shared vocabulary for architecture discussions.&lt;/strong&gt; Once a team has
gone through a review together, &quot;that&apos;s a Reliability risk&quot; or &quot;that&apos;s
not really Well-Architected on the Security pillar&quot; becomes shorthand
everyone in the room understands the same way, which speeds up later
design reviews.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lenses for workload-specific guidance.&lt;/strong&gt; Beyond the six generic
pillars, AWS publishes &lt;strong&gt;lenses&lt;/strong&gt; (Serverless, SaaS, Machine Learning,
Container Build, and others) with more specific best practices for that
architecture style — the Serverless lens asks meaningfully different
questions than the generic Operational Excellence pillar does, and is
worth applying if your workload fits one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AWS credits for remediation, sometimes.&lt;/strong&gt; Depending on your account&apos;s
relationship with AWS (particularly through a Partner or an active
Enterprise Support engagement), completing a review and acting on
findings can be tied to funding programs — worth checking with your
account team if cost is a factor in prioritizing the work.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Where it falls short&lt;/h2&gt;
&lt;p&gt;The questionnaire is generic by design, which means its questions are
necessarily abstracted away from your actual architecture — it will ask
whether you have a documented incident response runbook, but it can&apos;t
tell you whether your &lt;strong&gt;specific&lt;/strong&gt; runbook is any good, or whether your
DR RTO target is realistic for your actual failure modes. It&apos;s a
structured prompt for a conversation your team needs to have, not a
substitute for that conversation, and treating it as a checkbox exercise
where you tick &quot;implemented&quot; without actually verifying the practice is
in place defeats the entire point — self-reported answers are only as
honest as the person answering them.&lt;/p&gt;
&lt;p&gt;It also doesn&apos;t automatically inspect your account — answering the
questionnaire accurately still requires someone to actually go look at
what&apos;s configured, which is real work. (AWS&apos;s separate &lt;strong&gt;Trusted&lt;/strong&gt;
&lt;strong&gt;Advisor&lt;/strong&gt; and &lt;strong&gt;Well-Architected Tool&apos;s own automated checks&lt;/strong&gt;
integration can pull some findings in automatically for a subset of best
practices, narrowing but not eliminating that manual-verification gap.)&lt;/p&gt;
&lt;h2&gt;A practical tip&lt;/h2&gt;
&lt;p&gt;Don&apos;t try to review an entire application&apos;s full six-pillar surface in
one sitting — split the review across a couple of working sessions per
pillar with the people who actually own that area (security engineer for
the Security pillar, whoever owns the on-call rotation for Reliability),
and treat the output high-risk items as backlog tickets with owners, not
a report that goes in a drawer once the review is done.&lt;/p&gt;
</content:encoded></item><item><title>CVE-2026-16812: a CVSS 10 hole in your SD-WAN control plane</title><link>https://jstgtech.com/blog/2026-08-10-arista-velocloud-cve-2026-16812/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-arista-velocloud-cve-2026-16812/</guid><description>An unauthenticated command injection in Arista VeloCloud Orchestrator On-Prem lets attackers pivot from one exposed console to every edge device it manages.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most weeks the scariest CVE is scary because of what it&apos;s attached to — a
database, a CI/CD server, a pile of customer data. This week it&apos;s scarier
because of what it&apos;s &lt;em&gt;in front of&lt;/em&gt;: the single management plane for an
entire SD-WAN fleet. &lt;strong&gt;CVE-2026-16812&lt;/strong&gt;, an unauthenticated OS command
injection in Arista&apos;s VeloCloud Orchestrator (VCO) On-Prem, scores a
perfect CVSS 10.0, is under active exploitation, and CISA added it to the
Known Exploited Vulnerabilities catalog with a federal remediation deadline
of July 30 (&lt;a href=&quot;https://www.cisa.gov/news-events/alerts/2026/07/27/cisa-adds-two-known-exploited-vulnerabilities-catalog&quot;&gt;CISA&lt;/a&gt;). If you run any branch offices, retail sites, or hybrid-cloud links over
VeloCloud, this is the one to stop and read carefully.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;VCO is the web console that operators use to configure and monitor every
VeloCloud Edge device in a deployment — think of it as the &quot;cloud console&quot;
for your own private SD-WAN. According to Arista&apos;s advisory, the flaw lets
&quot;remote attackers access privileged functionality that was intended only
for internal use and should not be remotely accessible&quot; (&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/arista-patches-velocloud-orchestrator-zero-day-exploited-in-attacks/&quot;&gt;BleepingComputer&lt;/a&gt;). In plain terms: some internal-only code path that shells out to the
underlying OS is reachable from the external web interface without
authentication, and attacker-controlled input reaches that shell call
unsanitized. No credentials, no prior access, no user interaction — just
network reachability to the VCO web UI is enough to get arbitrary command
execution as whatever user the orchestrator process runs as.&lt;/p&gt;
&lt;p&gt;The uncomfortable detail from The Register&apos;s coverage is that the exposure
is structural: the orchestrator is &quot;exposed by default, with no
configuration capable of removing that exposure entirely&quot; (&lt;a href=&quot;https://www.theregister.com/security/2026/07/28/arista-patches-actively-exploited-velocloud-bug-as-cisa-puts-admins-on-the-clock/5279414&quot;&gt;The Register&lt;/a&gt;). This isn&apos;t a case of someone forgetting to put the admin panel behind a
VPN — On-Prem VCO&apos;s own design assumes some surface has to be internet-
reachable for edge devices to phone home to it, and that&apos;s the same surface
the vulnerable code path sits on. Arista&apos;s Hosted and Dedicated (SaaS)
deployments were already patched before the advisory went public, which
narrows this specifically to organizations running their &lt;em&gt;own&lt;/em&gt; VCO
instance rather than using Arista&apos;s managed service.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;This is where CVE-2026-16812 earns the CVSS 10. Compromising VCO doesn&apos;t
just hand over one server — it hands over the control plane for every
VeloCloud Edge device the orchestrator manages: branch routers, site
configurations, routing policy, and the tunnels those edges use to reach
each other and the cloud. Arista&apos;s own guidance says exploitation &quot;may
compromise the confidentiality, integrity, and availability of the
orchestrator and data managed by the orchestrator,&quot; and researchers have
flagged the realistic follow-on as attackers using orchestrator access to
push malicious configuration to managed edges, effectively turning a single
web app bug into a foothold across every site in the WAN. If your VCO
manages edges that terminate into a VPC or a colo where your workloads
live, that&apos;s the pivot path from &quot;someone popped our SD-WAN console&quot; to
&quot;someone is on our network.&quot;&lt;/p&gt;
&lt;p&gt;CISA has already observed active exploitation, and researchers have
published three IP addresses seen scanning and delivering payloads:
&lt;code&gt;8.19.75.217&lt;/code&gt;, &lt;code&gt;206.72.242.124&lt;/code&gt;, and &lt;code&gt;206.72.242.162&lt;/code&gt;. Worth feeding those
into your firewall/IDS blocklists today regardless of your patch status —
they&apos;re a known-bad signal, not a mitigation on their own.&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Arista has shipped fixed builds for every affected On-Prem branch:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;5.2.x → &lt;strong&gt;5.2.3.14&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;6.1.x → &lt;strong&gt;6.1.3.4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;6.4.x → &lt;strong&gt;6.4.2.4&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;7.0.x → &lt;strong&gt;7.0.0.1&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Patch first, but don&apos;t stop there. Because the flaw grants unauthenticated
RCE and has apparently been exploited since before public disclosure,
Arista&apos;s own advisory goes further than &quot;update and move on&quot; — it
recommends rotating credentials, validating that managed edge devices
haven&apos;t had unauthorized configuration changes pushed to them, and treating
a confirmed-compromised instance as a candidate for restore-from-clean
rather than trusting an in-place patch. That&apos;s a strong signal from the
vendor that patching alone may not be sufficient remediation if you can&apos;t
rule out prior compromise — check orchestrator and edge audit logs for
activity around the known-bad IPs above before you consider this closed.&lt;/p&gt;
&lt;p&gt;If patching isn&apos;t immediate, restrict the VCO web interface to trusted
management networks now. It won&apos;t fully close the hole given how the
product&apos;s exposure is architected, but cutting off casual internet
scanning buys time, and it&apos;s the same &quot;assume it&apos;s reachable, so fence it&quot;
posture you&apos;d want for any management plane with this much downstream
reach.&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;The recurring theme this year is that orchestration and management planes
— CI/CD servers, RMM consoles, and now SD-WAN orchestrators — are
increasingly the preferred target, precisely because compromising one
system fans out into control over everything it manages. If you run
on-prem management software for infrastructure with real blast radius, the
question worth asking isn&apos;t just &quot;is it patched&quot; but &quot;what does an
attacker get if this one box falls&quot; — and whether that answer is something
you&apos;re comfortable with by design, not just by patch level.&lt;/p&gt;
</content:encoded></item><item><title>CVE-2025-30066: The GitHub Action Tag You Trusted Lied</title><link>https://jstgtech.com/blog/2026-08-10-github-actions-supply-chain/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-github-actions-supply-chain/</guid><description>A compromised maintainer token let attackers rewrite tj-actions/changed-files version tags, dumping CI/CD secrets from thousands of repos into public build logs.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every workflow file with a line like &lt;code&gt;uses: tj-actions/changed-files@v45&lt;/code&gt;
is making a promise to itself that a tag won&apos;t change out from under it.
In March 2025 that promise broke for real: attackers rewrote the tags on
&lt;code&gt;tj-actions/changed-files&lt;/code&gt;, a GitHub Action used in over 23,000 repositories,
to point at a malicious commit that dumped CI/CD secrets straight into
public workflow logs — tracked as &lt;strong&gt;CVE-2025-30066&lt;/strong&gt; (&lt;a href=&quot;https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction&quot;&gt;CISA&lt;/a&gt;). It&apos;s one of the cleanest real-world illustrations of why &quot;pin a version&quot;
and &quot;pin a commit&quot; are not the same security control, and it&apos;s directly
relevant if your CI — like this site&apos;s — runs on GitHub Actions.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;tj-actions/changed-files&lt;/code&gt; is a popular action that reports which files
changed in a PR or push, and plenty of pipelines gate steps on its output.
On March 14, 2025, its maintainers discovered that a large number of the
project&apos;s version tags had been silently repointed to a commit that never
went through review: it injected a Node.js payload that scanned GitHub
Runner memory for credentials — cloud access keys, PATs, npm tokens, private
RSA keys — and printed them, base64-encoded, into the workflow run log
(&lt;a href=&quot;https://www.wiz.io/blog/github-action-tj-actions-changed-files-supply-chain-attack-cve-2025-30066&quot;&gt;Wiz&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The entry point traces back further, and it&apos;s the more interesting part of
the story. Wiz&apos;s investigation found the compromise was likely cascading:
three days earlier, on March 11, attackers had already hijacked the &lt;code&gt;v1&lt;/code&gt;
tag of a &lt;em&gt;different&lt;/em&gt; action, &lt;code&gt;reviewdog/action-setup&lt;/code&gt;, and pointed it at
malicious code of their own (&lt;strong&gt;CVE-2025-30154&lt;/strong&gt;) (&lt;a href=&quot;https://www.stepsecurity.io/blog/reviewdog-github-actions-are-compromised&quot;&gt;StepSecurity&lt;/a&gt;). &lt;code&gt;tj-actions/eslint-changed-files&lt;/code&gt; happened to depend on
&lt;code&gt;reviewdog/action-setup&lt;/code&gt;, and the maintainers&apos; own bot ran with a GitHub
personal access token — a token that Wiz believes was harvested through
that same compromised dependency and then used to push the malicious commit
and rewrite the &lt;code&gt;changed-files&lt;/code&gt; tags (&lt;a href=&quot;https://www.wiz.io/blog/new-github-action-supply-chain-attack-reviewdog-action-setup&quot;&gt;Wiz&lt;/a&gt;). One stolen credential in one small action rippled into a second,
much more widely used one within days.&lt;/p&gt;
&lt;p&gt;The structural flaw both incidents share is that a tag like &lt;code&gt;@v1&lt;/code&gt; or &lt;code&gt;@v45&lt;/code&gt;
is just a mutable pointer, not a content hash. GitHub lets any tag be force-
pushed to a new commit at any time by anyone with write access — there&apos;s no
cryptographic binding between the ref your workflow trusts and the code
that actually runs when the runner checks it out. Reviewing an action once
and pinning &lt;code&gt;@v4&lt;/code&gt; doesn&apos;t protect you from a maintainer account (or its
automation token) getting popped six months later.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;The initial estimate — every one of the 23,000+ repos using
&lt;code&gt;tj-actions/changed-files&lt;/code&gt; — made headlines fast, but the real exposure
window was narrower and still meaningfully bad. The malicious code was live
between roughly March 12, 00:00 UTC and March 15, 12:00 UTC; anyone who ran
the action against a public repo during that window had whatever secrets
were in scope for the job printed straight into a log anyone could read,
before GitHub could pull them down (&lt;a href=&quot;https://github.com/advisories/ghsa-mrrh-fwg8-r2c3&quot;&gt;GitHub Advisory Database&lt;/a&gt;). On the upstream &lt;code&gt;reviewdog&lt;/code&gt; side, deeper analysis found the confirmed
secret-leak count was smaller than the initial panic suggested — around 218
repositories actually had secrets exposed in logs, not tens of thousands
(&lt;a href=&quot;https://www.bleepingcomputer.com/news/security/github-action-hack-likely-led-to-another-in-cascading-supply-chain-attack/&quot;&gt;BleepingComputer&lt;/a&gt;) — but &quot;smaller than feared&quot; still means real cloud credentials and
tokens sitting in public logs for anyone who scraped them in time. GitHub
temporarily pulled the &lt;code&gt;tj-actions/changed-files&lt;/code&gt; repository entirely while
the malicious commit was reverted and tags restored, which is its own
signal of how seriously the platform treated it.&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;The fix that actually closes this hole is boring and mechanical: pin every
third-party Action to a full 40-character commit SHA, not a tag —
&lt;code&gt;uses: tj-actions/changed-files@a1b2c3...&lt;/code&gt; instead of &lt;code&gt;@v45&lt;/code&gt;. A SHA can&apos;t
be silently repointed the way a tag can, so a compromised maintainer token
can rewrite tags all day and your pinned workflow still runs the commit you
reviewed. Pinning by SHA does mean you lose automatic updates, which is why
you pair it with &lt;strong&gt;Dependabot&lt;/strong&gt; or &lt;strong&gt;Renovate&lt;/strong&gt; — both can open PRs that
bump the pinned SHA (with the new tag as a comment for readability) so
upgrades stay a reviewed diff instead of an invisible tag move. On the org
side, GitHub lets you restrict which Actions are allowed to run at all
(allow-list specific actions or require them to be from verified creators),
which is worth turning on for anything beyond a personal repo. If you ran
an affected action during the exposure window, don&apos;t stop at patching: pull
the workflow run logs, search them for anything that looks like a leaked
credential, and rotate every secret that was in scope for those jobs —
assume exposure rather than hoping the log got pulled in time.&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;A third-party GitHub Action is a dependency with write access to your CI
environment and, by extension, whatever your CI can reach — cloud
credentials, deploy keys, package registry tokens. Marketplace popularity
and a green checkmark on the README aren&apos;t a security review; they&apos;re a
popularity contest. This site&apos;s own CI runs entirely on GitHub Actions, and
the practical takeaway is the same one that applies to any &lt;code&gt;npm install&lt;/code&gt;:
pin to something immutable, let a bot handle the update diffs, and treat
every action in your workflow file as code you&apos;re choosing to run with your
secrets — because that&apos;s exactly what it is.&lt;/p&gt;
</content:encoded></item><item><title>CVE-2025-0282: the Ivanti VPN zero-day, and its sequel</title><link>https://jstgtech.com/blog/2026-08-10-ivanti-vpn-exploitation/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-ivanti-vpn-exploitation/</guid><description>A stack-based buffer overflow in Ivanti Connect Secure let Chinese state hackers run a malware ecosystem on VPN gateways for weeks before anyone noticed.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you manage remote access for a living, Ivanti Connect Secure has probably
cost you a weekend at some point in the last three years. The latest entry is
&lt;strong&gt;CVE-2025-0282&lt;/strong&gt;, an unauthenticated stack-based buffer overflow in Connect
Secure, Policy Secure, and Neurons for ZTA gateways that Mandiant caught being
exploited as a zero-day starting in mid-December 2024, weeks before Ivanti
shipped a fix (&lt;a href=&quot;https://cloud.google.com/blog/topics/threat-intelligence/ivanti-connect-secure-vpn-zero-day&quot;&gt;Google Cloud / Mandiant&lt;/a&gt;). CISA added it to the Known Exploited Vulnerabilities
catalog and gave federal agencies until January 15 to patch (&lt;a href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa25-022a&quot;&gt;CISA KEV&lt;/a&gt;). It&apos;s the third
major Ivanti edge-device RCE chain in two years, and that pattern is the
actual story here.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;CVE-2025-0282 is a classic memory-safety bug: a stack-based buffer overflow
(CWE-121) in a web-facing component of Connect Secure that a remote,
unauthenticated attacker can trigger with a crafted request, landing arbitrary
code execution on the appliance with no credentials and no user interaction
(&lt;a href=&quot;https://www.rapid7.com/blog/post/2025/01/08/etr-cve-2025-0282-ivanti-connect-secure-zero-day-exploited-in-the-wild/&quot;&gt;Rapid7&lt;/a&gt;). It carries a CVSS base score of 9.0 — full compromise, network
attack vector, no privileges required.&lt;/p&gt;
&lt;p&gt;This isn&apos;t Ivanti&apos;s first appliance-RCE rodeo. A year earlier it was
CVE-2023-46805 (an auth bypass) chained with CVE-2024-21887 (a command
injection) to get the same outcome on the same product line (&lt;a href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa24-060b&quot;&gt;CISA&lt;/a&gt;). The
underlying story repeats: Connect Secure is a Linux-based appliance running a
web application stack that&apos;s directly exposed to the internet by design —
that&apos;s the whole point of a VPN gateway — and a memory-corruption or
auth-logic bug in that stack turns &quot;reachable on 443&quot; straight into root on
the box. There&apos;s no perimeter behind the perimeter device.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;Mandiant&apos;s incident response found a genuinely elaborate malware ecosystem
dropped on compromised appliances, tracked as the SPAWN family: &lt;strong&gt;SPAWNANT&lt;/strong&gt;
(the installer/persistence mechanism), &lt;strong&gt;SPAWNMOLE&lt;/strong&gt; (a tunneler for pivoting
into the internal network), &lt;strong&gt;SPAWNSNAIL&lt;/strong&gt; (an SSH backdoor), and
&lt;strong&gt;SPAWNSLOTH&lt;/strong&gt; (a log-tampering tool to blind forensic analysis) — plus two
newly observed tools, the &lt;strong&gt;PHASEJAM&lt;/strong&gt; dropper and &lt;strong&gt;DRYHOOK&lt;/strong&gt; credential
harvester (&lt;a href=&quot;https://cloud.google.com/blog/topics/threat-intelligence/ivanti-connect-secure-vpn-zero-day&quot;&gt;Google Cloud / Mandiant&lt;/a&gt;). Most notably, SPAWNANT was built specifically
to tamper with Ivanti&apos;s own Integrity Checker Tool manifest, so the appliance
would report clean to the exact tool defenders were told to trust.&lt;/p&gt;
&lt;p&gt;Mandiant attributes the activity, with medium confidence, to UNC5337 —
believed to be part of UNC5221, the same China-nexus espionage cluster behind
the 2023/2024 Ivanti chain — going after VPN gateways specifically because
they sit at the network edge with credentials and session state flowing
through them and, historically, no EDR agent watching what runs on them
(&lt;a href=&quot;https://therecord.media/china-espionage-ivanti-vulnerabilities-mandiant&quot;&gt;The Record&lt;/a&gt;). At the time of disclosure, tens of thousands of Connect
Secure instances were sitting exposed to the internet, and exploitation had
already been underway for roughly a month before the public advisory
(&lt;a href=&quot;https://cybersecuritynews.com/33542-ivanti-connect-secure-instances-exposed/&quot;&gt;CyberSecurityNews&lt;/a&gt;). And the pattern didn&apos;t stop there: three months later, Ivanti
disclosed &lt;strong&gt;CVE-2025-22457&lt;/strong&gt;, another unauthenticated stack-based buffer
overflow in the same product line — initially misjudged as a low-severity
denial-of-service bug until Mandiant showed it was remotely exploitable —
and again observed exploited in the wild by a suspected China-nexus actor
before agencies could patch (&lt;a href=&quot;https://cloud.google.com/blog/topics/threat-intelligence/china-nexus-exploiting-critical-ivanti-vulnerability&quot;&gt;Google Cloud / Mandiant&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Patched builds for CVE-2025-0282 have been available since January 8, 2025:
Connect Secure &lt;strong&gt;22.7R2.5&lt;/strong&gt;, Policy Secure &lt;strong&gt;22.7R1.2&lt;/strong&gt;, and Neurons for ZTA
gateways &lt;strong&gt;22.7R2.3&lt;/strong&gt; (&lt;a href=&quot;https://hub.ivanti.com/s/article/Security-Advisory-Ivanti-Connect-Secure-Policy-Secure-ZTA-Gateways-CVE-2025-0282-CVE-2025-0283?language=en_US&quot;&gt;Ivanti advisory&lt;/a&gt;). If you&apos;re still behind those
versions, that&apos;s the first move, no exceptions.&lt;/p&gt;
&lt;p&gt;But given that SPAWNANT was purpose-built to falsify the Integrity Checker
Tool&apos;s output, a clean ICT scan on an unpatched or recently patched device
isn&apos;t proof of a clean appliance. Run both the internal and external ICT, but
treat it as one signal among several, not a verdict — cross-reference against
Ivanti&apos;s published indicators of compromise, check for unexpected outbound
tunnels or SSH listeners, and if you have any reason to believe a device was
exposed during the exploitation window, the safer path is a factory reset and
clean rebuild from a patched image rather than trusting an in-place upgrade
to have removed a persistence mechanism designed to survive exactly that.
Rotate every credential and certificate that ever transited the appliance —
VPN gateways see user passwords, session tokens, and often service-account
secrets, all of which should be considered burned if compromise can&apos;t be
ruled out.&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;Three major RCE chains on the same Ivanti product line in under two years,
each exploited as a zero-day before a patch existed, each attributed to
state-nexus actors going straight for the edge — this isn&apos;t bad luck, it&apos;s a
target selection. Edge appliances are internet-facing by requirement, run
vendor firmware most security teams can&apos;t instrument the way they&apos;d
instrument a server, and sit exactly where credentials and network access
converge. If you operate any VPN gateway, firewall, or SSL-VPN appliance —
Ivanti or otherwise — the operational question isn&apos;t &quot;are we patched&quot; as a
one-time checkbox, it&apos;s whether you have a standing plan for zero-day
exploitation on a box you can&apos;t put an agent on: network segmentation around
the appliance, egress monitoring for exactly the kind of tunneling SPAWNMOLE
does, and a rebuild-from-clean playbook you&apos;ve actually rehearsed before you
need it at 2am.&lt;/p&gt;
</content:encoded></item><item><title>Java deserialization after Log4Shell: CVE-2023-46604</title><link>https://jstgtech.com/blog/2026-08-10-java-deserialization-post-log4shell/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-java-deserialization-post-log4shell/</guid><description>Apache ActiveMQ&apos;s unauthenticated RCE shows Java deserialization bugs never went away after Log4Shell — only the exploitation playbook got faster.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Log4Shell (CVE-2021-44228) made &quot;Java deserialization&quot; a household phrase for
a few terrifying weeks in December 2021, but it wasn&apos;t a one-off. Less than
two years later, &lt;strong&gt;CVE-2023-46604&lt;/strong&gt;, an unauthenticated remote code execution
bug in Apache ActiveMQ&apos;s OpenWire protocol, gave attackers the same
outcome — arbitrary command execution with zero credentials — and it wasn&apos;t
a lookup-and-log JNDI trick this time, it was textbook insecure
deserialization in a message broker that sits on the network path of a huge
number of Java shops. CISA added it to the Known Exploited Vulnerabilities
catalog within days, and ransomware crews had it weaponized before most
defenders had patched (&lt;a href=&quot;https://www.rapid7.com/blog/post/2023/11/01/etr-suspected-exploitation-of-apache-activemq-cve-2023-46604/&quot;&gt;Rapid7&lt;/a&gt;). If you assumed Log4Shell was the last time a Java
serialization bug would take down a fleet of production systems overnight,
this one is worth sitting with.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;ActiveMQ&apos;s OpenWire is the binary wire protocol brokers and clients use to
talk to each other, and by default it&apos;s listening on port 61616 with no
authentication required to establish a connection. Inside that protocol,
one command type — &lt;code&gt;ExceptionResponse&lt;/code&gt; — carries a class name and a message
string so a broker can tell a client &quot;here&apos;s the exception that happened.&quot;
The marshalling code that unpacks it, &lt;code&gt;BaseDataStreamMarshaller.createThrowable&lt;/code&gt;,
takes that attacker-supplied class name off the wire and instantiates it
directly, passing the attacker-supplied message string into the constructor,
without first checking that the class is actually a &lt;code&gt;Throwable&lt;/code&gt; (&lt;a href=&quot;https://www.rapid7.com/blog/post/2023/11/01/etr-suspected-exploitation-of-apache-activemq-cve-2023-46604/&quot;&gt;Rapid7&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;That&apos;s the whole bug. Anyone who can open a TCP connection to the OpenWire
port can send a crafted &lt;code&gt;EXCEPTION_RESPONSE&lt;/code&gt; packet naming any class on the
server&apos;s classpath — including Spring&apos;s &lt;code&gt;ClassPathXmlApplicationContext&lt;/code&gt;,
which happily fetches and executes an XML bean definition from an
attacker-controlled URL. Point that at a remote XML file that defines a
&lt;code&gt;ProcessBuilder&lt;/code&gt;-backed bean and the broker runs your shell command. It&apos;s
the same family as the Commons Collections gadget chains that made Java
deserialization famous a decade ago — take a class that legitimately does
something dangerous when instantiated or invoked, and abuse a deserializer
that doesn&apos;t discriminate about what it&apos;s allowed to construct. The fix
Apache shipped adds exactly the type check that should have been there from
the start: reject any &quot;exception&quot; class name that isn&apos;t actually a
&lt;code&gt;Throwable&lt;/code&gt; before instantiating it (&lt;a href=&quot;https://www.rapid7.com/blog/post/2023/11/01/etr-suspected-exploitation-of-apache-activemq-cve-2023-46604/&quot;&gt;Rapid7&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;CVE-2023-46604 scores CVSS 9.8, and researchers rated exploitation
complexity as trivial — no auth, no user interaction, one crafted packet
(&lt;a href=&quot;https://www.huntress.com/threat-library/vulnerabilities/cve-2023-46604&quot;&gt;Huntress&lt;/a&gt;). The result is arbitrary command execution as whatever OS user
runs the broker process, and in the wild that turned into exactly what you&apos;d
expect once a reliable pre-auth RCE with a public PoC exists: a race between
opportunistic and targeted actors. Rapid7 tracked exploitation attributed to
HelloKitty ransomware starting within days of disclosure (&lt;a href=&quot;https://www.rapid7.com/blog/post/2023/11/01/etr-suspected-exploitation-of-apache-activemq-cve-2023-46604/&quot;&gt;Rapid7&lt;/a&gt;), Trend Micro and Sekoia both documented the Kinsing
cryptomining botnet using it to drop miners and rootkits on Linux hosts
(&lt;a href=&quot;https://www.trendmicro.com/en_us/research/23/k/cve-2023-46604-exploited-by-kinsing.html&quot;&gt;Trend Micro&lt;/a&gt;, &lt;a href=&quot;https://blog.sekoia.io/activemq-cve-2023-46604-exploited-by-kinsing-and-overview-of-this-threat/&quot;&gt;Sekoia&lt;/a&gt;), and SOC Prime and others tracked TellYouThePass
ransomware riding the same bug (&lt;a href=&quot;https://socradar.io/blog/critical-rce-vulnerability-in-apache-activemq-is-targeted-by-hellokitty-ransomware-cve-2023-46604/&quot;&gt;SOCRadar&lt;/a&gt;). Message brokers tend to sit deep in the
architecture — application servers, integration layers, IoT backends all
talk to them — so a broker compromise isn&apos;t an edge-of-network incident,
it&apos;s a foothold with a direct line to whatever internal services trust that
broker&apos;s traffic.&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Apache patched the marshalling logic and shipped fixed releases across every
supported branch: &lt;strong&gt;5.15.16, 5.16.7, 5.17.6, and 5.18.3&lt;/strong&gt;, with 6.0.0 also
carrying the fix (&lt;a href=&quot;https://activemq.apache.org/news/cve-2023-46604&quot;&gt;Apache ActiveMQ&lt;/a&gt;). If you&apos;re still running an
unpatched broker, upgrading is non-negotiable — this isn&apos;t a &quot;add
authentication in front of it&quot; situation, because the flaw is in how the
broker parses its own wire protocol before any application-level auth
applies. Restricting network access to port 61616 to only trusted broker
and client hosts is a reasonable stopgap while you schedule the upgrade, but
given how quickly this was weaponized, treat any internet-facing or
broadly-reachable OpenWire port as already assumed-compromised and hunt for
follow-on cryptominer or webshell activity, not just confirm the patch
applied.&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;What&apos;s actually changed since Log4Shell is the plumbing around detection:
WAF and IDS vendors now ship signatures for OpenWire-style deserialization
payloads within hours of a PoC landing, CISA&apos;s KEV catalog gives defenders a
authoritative &quot;this is being exploited right now&quot; signal instead of relying
on vendor advisories alone, and software composition analysis tools flag
vulnerable broker and library versions in CI before they ever ship. On the
JDK side, JEP 415&apos;s context-specific deserialization filters (Java 17+) let
you scope an allowlist to a specific &lt;code&gt;ObjectInputStream&lt;/code&gt; instead of one
JVM-wide filter that&apos;s either too loose or breaks half your app (&lt;a href=&quot;https://www.baeldung.com/java-context-specific-deserialization-filters&quot;&gt;Baeldung&lt;/a&gt;) — a real improvement over JEP 290&apos;s blunt instrument.&lt;/p&gt;
&lt;p&gt;What hasn&apos;t changed is the underlying pattern: a component trusts a class
name or a byte stream from the network more than it should, and something
reachable on the classpath turns that trust into code execution. A 2024
NDSS study found over 3,600 GitHub Java projects still carrying known
deserialization vulnerabilities (&lt;a href=&quot;https://www.javacodegeeks.com/2026/05/serialization-is-still-javas-biggest-attack-surface-what-jep-290-actually-did-and-what-it-didnt.html&quot;&gt;Java Code Geeks&lt;/a&gt;) — the detection tooling
is faster, but it&apos;s still catching the same class of bug, not preventing
it from being written. If your Java services still do implicit
&lt;code&gt;ObjectInputStream&lt;/code&gt; deserialization or unauthenticated binary protocol
parsing anywhere on the network path, Log4Shell and ActiveMQ are both
telling you the same thing: the fix belongs in the code that trusts the
bytes, not in the WAF rule that flags them after the fact.&lt;/p&gt;
</content:encoded></item><item><title>Exposed Kubernetes API Servers Are an RBAC Time Bomb</title><link>https://jstgtech.com/blog/2026-08-10-kubernetes-rbac-exposed-api-server/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-kubernetes-rbac-exposed-api-server/</guid><description>Attackers are actively hunting internet-facing Kubernetes API servers with anonymous-auth on and default service accounts holding broad RBAC access.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;You don&apos;t need a zero-day to take over a Kubernetes cluster. You need an
API server that answers on the public internet and an &lt;code&gt;anonymous-auth&lt;/code&gt;
flag nobody turned off. Aqua Nautilus researchers spent three months
scanning for exactly that combination and found it everywhere: hundreds of
organizations with clusters already under active attack, most of it
opportunistic cryptomining riding in through anonymous access that had
somehow been granted real privileges (&lt;a href=&quot;https://www.csoonline.com/article/648756/kubernetes-clusters-under-attack-in-hundreds-of-organizations.html&quot;&gt;CSO Online&lt;/a&gt;, &lt;a href=&quot;https://www.globenewswire.com/news-release/2023/08/08/2720569/0/en/Aqua-Nautilus-Researchers-Find-Kubernetes-Clusters-Under-Attack-in-Hundreds-of-Organizations.html&quot;&gt;Aqua Nautilus&lt;/a&gt;). This isn&apos;t a rare edge case — Shadowserver&apos;s internet-wide scans have
repeatedly found north of 380,000 Kubernetes API servers responding
publicly, the large majority in the US and Western Europe (&lt;a href=&quot;https://www.shadowserver.org/news/over-380-000-open-kubernetes-api-servers/&quot;&gt;Shadowserver&lt;/a&gt;, &lt;a href=&quot;https://www.theregister.com/2022/05/23/kubernetes-vulnerable-shadowserver/&quot;&gt;The Register&lt;/a&gt;). Exposure alone doesn&apos;t mean compromise, but it&apos;s the precondition every
campaign in this space depends on, and the misconfigurations that turn
&quot;reachable&quot; into &quot;owned&quot; are boringly common.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;Kubernetes has two places where &quot;no credentials&quot; quietly becomes &quot;some
credentials.&quot; The first is the API server and kubelet&apos;s &lt;code&gt;anonymous-auth&lt;/code&gt;
setting, which historically defaulted to enabled and maps unauthenticated
requests to the &lt;code&gt;system:anonymous&lt;/code&gt; user (or the &lt;code&gt;system:unauthenticated&lt;/code&gt;
group). By itself that&apos;s supposed to be harmless — anonymous requests
should have no RBAC permissions bound to them. In practice, Aqua&apos;s kubelet
research found operators binding that anonymous identity to real roles,
sometimes admin-level ones, usually as a shortcut to get some internal
tool or health check working without wiring up proper auth (&lt;a href=&quot;https://www.aquasec.com/blog/kubernetes-exposed-exploiting-the-kubelet-api/&quot;&gt;Aqua Security&lt;/a&gt;). Once that binding exists, anyone who can reach the API server or the
kubelet&apos;s HTTPS port (10250) has exactly the access that role grants — no
login required. Aqua&apos;s kube-hunter tool has a dedicated check for this
pattern precisely because it recurs so often across audited clusters
(&lt;a href=&quot;https://aquasecurity.github.io/kube-hunter/kb/KHV036.html&quot;&gt;Aqua Security / kube-hunter&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The second gap is the default service account. Every pod gets one mounted
automatically unless a manifest explicitly sets
&lt;code&gt;automountServiceAccountToken: false&lt;/code&gt;, and that token grants whatever RBAC
role is bound to the account — which in a lot of real clusters is broader
than anyone intended, because &lt;code&gt;default&lt;/code&gt; service accounts accumulate
permissions over time as people bind roles the fast way instead of the
correct way. Layer that onto an internet-reachable control plane —
sometimes because of a misconfigured cloud load balancer, sometimes
because someone ran &lt;code&gt;kubectl proxy --address=0.0.0.0 --accept-hosts=&apos;.*&apos;&lt;/code&gt;
on a bastion and forgot about it, a specific misconfiguration Aqua called
out by name in its research — and a single unauthenticated HTTP request is
enough to start probing.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;The API server isn&apos;t just another service — it&apos;s the thing that can
schedule code onto every node in the cluster. Anonymous or default-account
access that includes pod-create permission lets an attacker launch a pod
with a &lt;code&gt;hostPath&lt;/code&gt; mount or &lt;code&gt;privileged: true&lt;/code&gt;, which is a direct path off
the container and onto the underlying node. From the node, the attacker
can read the kubelet&apos;s credentials and every service account token
scheduled there, pivoting sideways into other namespaces the original
access point never touched. In cloud-hosted clusters, pods often carry
workload-identity credentials (IRSA on EKS, Workload Identity on GKE) or
can reach the instance metadata service directly, so cluster compromise
routinely becomes cloud-account compromise. This is also the exact class
of risk that made CVE-2018-1002105 so severe a few years back — a bug that
let unauthenticated requests reach the API server&apos;s proxy layer and come
out the other side with cluster-admin, a reminder that &quot;you&apos;re talking to
the API server&quot; and &quot;you have privileged access&quot; are one and the same
threat model even without a misconfiguration involved (&lt;a href=&quot;https://www.tenable.com/blog/kubernetes-privilege-escalation-vulnerability-publicly-disclosed-cve-2018-1002105&quot;&gt;Tenable&lt;/a&gt;). In practice, most of what Aqua and others observe on these exposed
clusters is quieter than that: TeamTNT- and Kinsing-style campaigns that
drop XMRig miners across every node they can reach, plus at least one
documented &quot;RBAC buster&quot; campaign that used the access specifically to
plant a persistent backdoor role rather than just mine coins (&lt;a href=&quot;https://www.tigera.io/blog/teamtnt-latest-ttps-targeting-kubernetes/&quot;&gt;Tigera&lt;/a&gt;, &lt;a href=&quot;https://www.csoonline.com/article/648756/kubernetes-clusters-under-attack-in-hundreds-of-organizations.html&quot;&gt;CSO Online&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Disable anonymous auth.&lt;/strong&gt; Run kubelets with &lt;code&gt;--anonymous-auth=false&lt;/code&gt;
and route real authentication through &lt;code&gt;--client-ca-file&lt;/code&gt; or a token
webhook; never bind &lt;code&gt;system:anonymous&lt;/code&gt; or &lt;code&gt;system:unauthenticated&lt;/code&gt; to
any ClusterRole, including &quot;harmless-looking&quot; ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop exposing the control plane.&lt;/strong&gt; Put the API server behind a private
endpoint and security-group/authorized-network restrictions (EKS
private endpoint access, GKE authorized networks, or equivalent) instead
of a public load balancer with no source restriction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Audit RBAC for least privilege&lt;/strong&gt;, especially bindings on &lt;code&gt;default&lt;/code&gt;
service accounts — Kubernetes&apos; own docs have a good-practices guide for
this and it&apos;s worth running against every namespace, not just the
obviously sensitive ones (&lt;a href=&quot;https://kubernetes.io/docs/concepts/security/rbac-good-practices/&quot;&gt;Kubernetes docs&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set &lt;code&gt;automountServiceAccountToken: false&lt;/code&gt;&lt;/strong&gt; on any pod that doesn&apos;t
actually call the Kubernetes API, so a compromised pod doesn&apos;t
automatically hand over a credential.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lock down the kubelet port (10250)&lt;/strong&gt; with network policies and
firewall rules — it shouldn&apos;t be reachable from outside the cluster
network, full stop.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn on audit logging&lt;/strong&gt; and alert on anonymous or unexpected-identity
requests hitting the API server; that signal is cheap to collect and
catches this exact pattern early.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;None of this requires a novel exploit — every campaign cited here rode in
on defaults nobody explicitly locked down. Kubernetes was built to be
flexible for cluster operators first and secure-by-default second, and
that tradeoff means the burden of hardening anonymous access, RBAC
bindings, and network exposure sits entirely on whoever stood the cluster
up. Attackers aren&apos;t finding your cluster through cleverness; they&apos;re
running the same internet-wide scans Shadowserver publishes for free and
checking whether you did the hardening step or skipped it. Assume they&apos;ve
already scanned you, and go verify which answer they got.&lt;/p&gt;
</content:encoded></item><item><title>Leaked credentials in public repos get used in minutes</title><link>https://jstgtech.com/blog/2026-08-10-leaked-credentials-public-repos/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-leaked-credentials-public-repos/</guid><description>GitGuardian logged 28.65M secrets on public GitHub in 2025, and researchers have watched leaked AWS keys get abused in under five minutes.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;GitGuardian&apos;s 2026 State of Secrets Sprawl report counted 28.65 million new
hardcoded secrets pushed to public GitHub commits in 2025 alone — a 34%
jump year over year and the largest single-year increase they&apos;ve recorded
(&lt;a href=&quot;https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/&quot;&gt;GitGuardian&lt;/a&gt;).
That&apos;s not a backlog of old mistakes; it&apos;s this year&apos;s output. And the
same report found that 64% of secrets that leaked back in 2022 are *still
valid* today (&lt;a href=&quot;https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/&quot;&gt;GitGuardian&lt;/a&gt;).
Meanwhile Unit 42&apos;s tracking of the EleKtra-Leak campaign found attackers
detecting and using exposed AWS IAM credentials within roughly five minutes
of them landing on GitHub (&lt;a href=&quot;https://unit42.paloaltonetworks.com/malicious-operations-of-exposed-iam-keys-cryptojacking/&quot;&gt;Unit 42&lt;/a&gt;).
Committing a secret to a public repo isn&apos;t a &quot;someone might find this
eventually&quot; risk. It&apos;s closer to broadcasting it live.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;Almost none of this is malicious. It&apos;s &lt;code&gt;.env&lt;/code&gt; files added to a repo before
anyone wrote a &lt;code&gt;.gitignore&lt;/code&gt; entry for them, database URLs hardcoded into a
config &quot;just for local testing,&quot; service-account JSON dropped into a test
fixture, or a CI job that echoes an environment variable into build logs
that are themselves public. GitGuardian&apos;s report also flags AI-assisted
coding as a growing contributor — commits with AI-generated code leak
secrets at roughly double the baseline rate, and secrets tied to AI
services specifically were up 81% year over year (&lt;a href=&quot;https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026-pr/&quot;&gt;GitGuardian&lt;/a&gt;),
which tracks: an assistant that&apos;s never seen your &lt;code&gt;.gitignore&lt;/code&gt; will happily
paste a working API key straight into a code sample.&lt;/p&gt;
&lt;p&gt;The part engineers consistently underestimate is that deleting the secret
in a follow-up commit doesn&apos;t delete it from the repo. Git preserves
history — the credential is still sitting in an earlier commit object,
reachable by anyone who clones the repo or even just browses the commit
log on GitHub. Force-pushing a rewritten history to your own branch
doesn&apos;t help either if the repo was ever public even briefly: forks, local
clones, and GitHub&apos;s own caches can retain the blob indefinitely. The only
thing that actually neutralizes a leaked secret is invalidating the
credential itself — history rewrites are cleanup, not remediation.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;This is the part that should change how you triage. Automated scanners
watch GitHub&apos;s public event stream continuously, and multiple independent
research efforts have clocked exploitation in the single-digit minutes:
Unit 42 measured attackers weaponizing exposed IAM keys from the
EleKtra-Leak campaign in about five minutes (&lt;a href=&quot;https://unit42.paloaltonetworks.com/malicious-operations-of-exposed-iam-keys-cryptojacking/&quot;&gt;Unit 42&lt;/a&gt;),
and independent write-ups of live incidents describe leaked AWS keys being
grabbed and turned into GPU cryptomining fleets within 4 to 11 minutes of
the push, run across every region the credential could reach
(&lt;a href=&quot;https://www.techradar.com/pro/security/exposed-aws-credentials-stolen-within-minutes-by-github-hackers&quot;&gt;TechRadar&lt;/a&gt;).
The economics are simple: spinning up GPU instances for Monero mining
costs the attacker nothing (it&apos;s your bill) and the scanning infrastructure
is fully automated, so there&apos;s no human in the loop slowing things down.
There is no &quot;we&apos;ll rotate it during the next sprint&quot; window here — by the
time a human notices the commit, the credential has often already been
used.&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;The controls that matter, roughly in order of leverage:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Push protection.&lt;/strong&gt; GitHub&apos;s secret scanning can block a push before a
known-format secret ever lands in the repo, and its partner program
automatically notifies the issuing provider (AWS, Stripe, and ~150
others) when a valid secret is detected in a public repo, sometimes
triggering revocation faster than the committer even sees the alert
(&lt;a href=&quot;https://docs.github.com/en/code-security/concepts/secret-security/push-protection&quot;&gt;GitHub Docs&lt;/a&gt;).
Turn this on org-wide, not per-repo.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-commit scanning.&lt;/strong&gt; Tools like &lt;code&gt;gitleaks&lt;/code&gt; or &lt;code&gt;trufflehog&lt;/code&gt; run
locally before a commit is even made, catching secrets in formats GitHub
doesn&apos;t recognize (internal API keys, custom tokens) and catching them
before they touch history at all, which is strictly better than catching
them after.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat any committed secret as compromised, full stop.&lt;/strong&gt; Don&apos;t reach
for &lt;code&gt;git filter-branch&lt;/code&gt; or a force-push as the first response — rotate
the credential at the source first. A scrubbed history with a still-valid
key behind it protects nobody; an invalidated key with a messy history is
a Tuesday.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shrink the blast radius with short-lived credentials.&lt;/strong&gt; The strongest
structural fix is having fewer long-lived secrets to leak in the first
place. This site&apos;s own CI/CD (see the &lt;a href=&quot;https://jstgtech.com/blog/2026-08-10-github-actions-oidc-aws&quot;&gt;GitHub Actions OIDC to AWS
tutorial&lt;/a&gt;) uses OpenID Connect federation instead of storing AWS
access keys as repo secrets — GitHub Actions requests a short-lived,
auto-expiring token scoped to a specific IAM role for each run, so
there&apos;s no long-lived AWS key sitting in secrets storage to leak in the
first place. Where OIDC isn&apos;t an option, aggressive credential expiry and
scoped-down IAM policies get you most of the same benefit.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;Prevention controls — push protection, pre-commit hooks, &lt;code&gt;.gitignore&lt;/code&gt;
discipline — are worth having, but GitGuardian&apos;s own numbers show they
aren&apos;t closing the gap: secrets leaked have grown 152% since 2021 against
a 98% growth in GitHub&apos;s developer base (&lt;a href=&quot;https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/&quot;&gt;GitGuardian&lt;/a&gt;),
meaning the leak rate is outpacing the population producing the leaks.
Some secret will eventually get past whatever scanner you&apos;ve configured,
because prevention is a filter and filters have gaps by definition. What
actually determines whether that leak becomes an incident report or a
non-event is how fast you can rotate the credential and how little damage
it can do in the window before you notice. That&apos;s why the real investment
isn&apos;t just &quot;stop secrets from leaking&quot; — it&apos;s &quot;assume one will leak, and
make sure it&apos;s short-lived, narrowly scoped, and rotated before a bot ever
gets a chance to use it.&quot;&lt;/p&gt;
</content:encoded></item><item><title>MOVEit and the MFT zero-day exploitation playbook</title><link>https://jstgtech.com/blog/2026-08-10-mft-zero-day-exploitation/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-mft-zero-day-exploitation/</guid><description>CVE-2023-34362 turned one SQL injection in MOVEit Transfer into 2,700+ breached organizations — and the same pattern keeps repeating against MFT software.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;In May 2023 the Clop ransomware group quietly started exploiting an
unpatched SQL injection in Progress Software&apos;s MOVEit Transfer — days
before anyone outside the attackers knew it existed. By the time the dust
settled, more than 2,700 organizations and upward of 90 million individuals
had data stolen through &lt;strong&gt;CVE-2023-34362&lt;/strong&gt;, making it one of the largest
single-vulnerability breaches on record (&lt;a href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a&quot;&gt;CISA&lt;/a&gt;, &lt;a href=&quot;https://techcrunch.com/2023/08/25/moveit-mass-hack-by-the-numbers/&quot;&gt;TechCrunch&lt;/a&gt;). It wasn&apos;t an isolated incident — it was the clearest example yet of a
pattern that keeps repeating against managed file transfer (MFT) software:
find one flaw in an internet-facing appliance built to move sensitive data,
and turn it into hundreds of breaches in a matter of days.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;MOVEit Transfer&apos;s vulnerability was a textbook SQL injection: an
unauthenticated attacker could send crafted input to the web application
and manipulate backend SQL queries against MySQL, Microsoft SQL Server, or
Azure SQL, escalating from unauthorized database access to remote code
execution (&lt;a href=&quot;https://www.rapid7.com/blog/post/2023/06/01/rapid7-observed-exploitation-of-critical-moveit-transfer-vulnerability/&quot;&gt;Rapid7&lt;/a&gt;). Clop used it to drop a custom ASP.NET web shell — dubbed LEMURLOOT,
typically written to disk as &lt;code&gt;human2.aspx&lt;/code&gt; to blend in with MOVEit&apos;s
legitimate &lt;code&gt;human.aspx&lt;/code&gt; — giving them a durable, authenticated-looking
foothold for pulling files straight out of the transfer database (&lt;a href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a&quot;&gt;Mandiant&lt;/a&gt;). Forensic teams found evidence attackers had mapped the database schema
in advance, meaning this wasn&apos;t smash-and-grab improvisation — it was a
prepared exploit chain, sat on for weeks, then fired against every
internet-reachable MOVEit instance Clop could find.&lt;/p&gt;
&lt;p&gt;The deeper problem is what MFT software &lt;em&gt;is&lt;/em&gt;. Products like MOVEit,
GoAnywhere, and Cleo exist specifically to sit at the network edge,
accept authenticated (and sometimes unauthenticated) file uploads from
external partners, and hold the resulting files — often full of PII,
financial records, or healthcare data — until someone downstream picks
them up. That&apos;s three attractive properties stacked on one box: it has to
be internet-facing by design, it processes untrusted external input as its
core function, and the data sitting on it is exactly what a data-extortion
crew wants. A vulnerability class that would be a moderate finding on an
internal app becomes catastrophic on an MFT server because the &quot;why would
an attacker target this&quot; question answers itself.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;Clop&apos;s MOVEit campaign wasn&apos;t a slow-burn intrusion — CISA and the FBI
described mass, opportunistic exploitation across a few days before
switching to the extortion phase (&lt;a href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a&quot;&gt;CISA&lt;/a&gt;). Confirmed victims spanned government agencies (multiple U.S. federal
agencies among them), airlines (British Airways, Aer Lingus), payroll
provider Zellis and its downstream customers, the BBC, and the government
of Nova Scotia — the breadth reflecting how MOVEit sits inside countless
unrelated organizations&apos; back-office data flows (&lt;a href=&quot;https://www.bankinfosecurity.com/latest-moveit-data-breach-victim-tally-455-organizations-a-22650&quot;&gt;BankInfoSecurity&lt;/a&gt;). By late October 2023, tracking firm Emsisoft put the confirmed count at
2,559 organizations and over 66 million individuals, with later tallies
climbing past 2,700 organizations and roughly 90+ million people as more
downstream disclosures rolled in through 2024 (&lt;a href=&quot;https://www.cloudskope.com/breaches/moveit-breach-2023&quot;&gt;Cloudskope&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Notably, Clop didn&apos;t encrypt anything. This was pure data-theft-and-extort:
steal the files, then list victims on a leak site with a payment deadline.
That&apos;s become the default MFT playbook — encryption is optional, exposure
of the stolen files is the leverage, and it&apos;s fast to scale because the
same exploit chain works against every unpatched instance simultaneously.
It repeated almost exactly with the earlier GoAnywhere MFT SQLi/RCE
(CVE-2023-0669) and again in December 2024 against Cleo&apos;s Harmony, VLTrader,
and LexiCom products, where Clop chained CVE-2024-50623 and
CVE-2024-55956 — the second flaw shipped because the first patch was
incomplete — to deploy a Java backdoor and hit organizations that thought
they&apos;d already remediated (&lt;a href=&quot;https://www.rapid7.com/blog/post/2024/12/10/etr-widespread-exploitation-of-cleo-file-transfer-software-cve-2024-50623/&quot;&gt;Rapid7&lt;/a&gt;, &lt;a href=&quot;https://www.bleepingcomputer.com/news/security/clop-ransomware-claims-responsibility-for-cleo-data-theft-attacks/&quot;&gt;BleepingComputer&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Patch immediately and don&apos;t stop there — Progress shipped fixes for
CVE-2023-34362 within days, but two related SQLi flaws
(CVE-2023-35036, CVE-2023-35708) surfaced shortly after in the same code
paths, the same &quot;one patch wasn&apos;t the whole story&quot; pattern seen later with
Cleo. Treat a single advisory as the start of a remediation window, not
the end.&lt;/p&gt;
&lt;p&gt;For detection, CISA&apos;s advisory (AA23-158A) published concrete IOCs worth
hunting for regardless of which MFT product you run: unexpected &lt;code&gt;.aspx&lt;/code&gt;
files in the web root (&lt;code&gt;human2.aspx&lt;/code&gt; for MOVEit specifically), new/unknown
admin or service accounts created around the exploitation window, SQL
injection patterns and anomalous query volume in application logs, and
outbound connections to unfamiliar IPs shortly after suspicious file
activity (&lt;a href=&quot;https://www.cisa.gov/news-events/cybersecurity-advisories/aa23-158a&quot;&gt;CISA&lt;/a&gt;). CISA also published YARA and Sigma rules for LEMURLOOT specifically —
run them even after patching, since a compromise that predates your patch
won&apos;t be undone by it.&lt;/p&gt;
&lt;p&gt;Architecturally, stop treating MFT servers like ordinary web apps. Segment
them into their own network zone with tightly scoped egress — a file
transfer server has no legitimate reason to be initiating arbitrary
outbound connections, so alerting on unexpected outbound data flows is one
of the highest-signal detections available. Put a WAF in front of the web
interface and use it to block known exploit patterns while you patch.
Minimize what&apos;s actually exposed to the internet: if partners can reach
you over a VPN or IP allowlist instead of the open web, do that. And treat
the underlying files as sensitive at rest — encrypt them, and don&apos;t let
the transfer server itself be the only thing standing between &quot;in transit&quot;
and &quot;exfiltrated.&quot;&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;MOVEit, GoAnywhere, and Cleo aren&apos;t unrelated incidents — they&apos;re the same
shape of failure recurring because MFT software occupies a structural sweet
spot for attackers: internet-facing by requirement, processing untrusted
input by design, and holding exactly the data a data-extortion crew wants
to steal. That combination doesn&apos;t exist for MFT alone — it applies to any
edge appliance that ingests external data and holds something valuable
afterward. If you run one, the operative question isn&apos;t &quot;has this been
patched recently,&quot; it&apos;s &quot;what happens to every file that&apos;s touched this
box if it&apos;s compromised tomorrow&quot; — and whether your network segmentation,
egress monitoring, and patch cadence would actually catch it before it
becomes the next mass-breach headline.&lt;/p&gt;
</content:encoded></item><item><title>The 198M-voter S3 leak that still explains most breaches</title><link>https://jstgtech.com/blog/2026-08-10-public-s3-bucket-exposure/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-public-s3-bucket-exposure/</guid><description>A 2017 misconfigured S3 bucket exposed 198 million voter records, and the same public-bucket misconfiguration still causes new breaches every year.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Most S3 exposure incidents don&apos;t require a zero-day, a phishing email, or
even much skill — just someone who knows how to type an S3 URL into a
browser. In June 2017, UpGuard security researcher Chris Vickery found
exactly that: a bucket named &lt;code&gt;dra-dw&lt;/code&gt; — &quot;Deep Root Analytics Data
Warehouse&quot; — sitting fully public on the open internet, no password and no
authentication anywhere in front of it. Inside was 1.1 terabytes of data on
nearly every registered voter in the United States: 198 million records
with names, dates of birth, home addresses, phone numbers, and modeled
ethnicity and religion scores, compiled by contractors working for the
Republican National Committee (&lt;a href=&quot;https://gizmodo.com/gop-data-firm-accidentally-leaks-personal-details-of-ne-1796211612&quot;&gt;Gizmodo&lt;/a&gt;, &lt;a href=&quot;https://www.upguard.com/breaches/the-rnc-files&quot;&gt;UpGuard&lt;/a&gt;). Deep Root Analytics had, in
UpGuard&apos;s account of the incident, simply set the bucket&apos;s permissions to
public instead of private (&lt;a href=&quot;https://cyberscoop.com/chris-vickery-upguard-aws-s3-data-leakage-deep-root-analytics/&quot;&gt;CyberScoop&lt;/a&gt;). That&apos;s a 2017 incident — old
enough that AWS has since shipped several generations of guardrails
specifically built to prevent it — and yet the identical failure mode, a
bucket flipped to public and found by a researcher scanning the internet
rather than by the company that owns it, keeps showing up in breach
reports nearly every year since. That&apos;s the real story here: not one
incident, but a misconfiguration class that refuses to die.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;S3 buckets are private by default. Getting one exposed takes a deliberate
(if often thoughtless) action, and over a decade of these incidents the
paths to that action have stayed remarkably consistent. Legacy ACLs are
the classic one: S3&apos;s original access-control-list model lets you grant
read or write access to predefined groups, and two of those groups are
routinely confused for &quot;my organization&quot; when they actually mean &quot;the
entire internet&quot; or &quot;any authenticated AWS account on the planet&quot; — not
your customers, not your team, literally anyone with an AWS account.
Bucket policies cause the same damage a different way: a policy meant to
scope access to a specific role or CloudFront distribution gets written
with a wildcard principal (&lt;code&gt;&quot;Principal&quot;: &quot;*&quot;&lt;/code&gt;) or an overly broad condition
that accidentally satisfies for anonymous requests too. And a large share
of real-world exposures trace back to third-party tooling — backup
utilities, data pipelines, static site generators, BI exports — that
defaults to a public bucket because it&apos;s the path of least resistance for
the vendor&apos;s quickstart guide, and nobody revisits the setting once it
works. None of these require malice. They require a permission model with
more than one way to say &quot;public&quot; and no default nudge toward noticing
you&apos;ve done it.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;The Deep Root leak wasn&apos;t found by Deep Root. It was found by a
third-party researcher running broad scans for open cloud storage, who
stumbled onto a predictable Amazon subdomain,
&lt;code&gt;dra-dw.s3.amazonaws.com&lt;/code&gt; (&lt;a href=&quot;https://www.upguard.com/breaches/the-rnc-files&quot;&gt;UpGuard&lt;/a&gt;). That&apos;s the pattern across
nearly every S3 exposure story of the last decade — Verizon&apos;s exposed
customer call records via a third-party vendor, Dow Jones&apos;s 2.2 million
subscriber records, Accenture&apos;s exposed API keys and credentials — the
company almost never discovers its own leak. It&apos;s found by a security
researcher, a journalist, or occasionally a criminal, all of whom have the
same tooling: internet-wide bucket-name scanners that turn &quot;is this bucket
public&quot; into a solved, automatable question. Once a bucket is enumerable,
the blast radius is everything in it, immediately, with no exploit chain
required — the exposure is the vulnerability. For Deep Root that meant PII
on 198 million Americans sitting downloadable for at least several days
before Vickery&apos;s report reached the company and the bucket was locked down
(&lt;a href=&quot;https://cyberscoop.com/chris-vickery-upguard-aws-s3-data-leakage-deep-root-analytics/&quot;&gt;CyberScoop&lt;/a&gt;).&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;AWS&apos;s answer to this exact failure mode is &lt;strong&gt;S3 Block Public Access&lt;/strong&gt;,
launched in November 2018 specifically in response to years of incidents
like this one — it&apos;s a set of four switches, settable at the bucket or the
whole-account level, that override any ACL or bucket policy trying to
grant public access, so a misconfigured policy simply can&apos;t take effect
(&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2018/11/introducing-amazon-s3-block-public-access&quot;&gt;AWS&lt;/a&gt;). As of April 2023, AWS made this the default for every new bucket and
disabled ACLs by default account-wide, which closes off the legacy-ACL
path that caused Deep Root&apos;s leak unless someone deliberately opts back
in. Layer on top of that: &lt;strong&gt;IAM Access Analyzer for S3&lt;/strong&gt;, which
continuously flags buckets reachable from outside your account or
organization, including via cross-account bucket policies that Block
Public Access alone won&apos;t catch; an &lt;strong&gt;AWS Config rule&lt;/strong&gt;
(&lt;code&gt;s3-bucket-public-read-prohibited&lt;/code&gt; / &lt;code&gt;s3-bucket-public-write-prohibited&lt;/code&gt;)
that continuously evaluates every bucket against the policy you&apos;ve set,
rather than only at creation time; and an &lt;strong&gt;SCP at the AWS Organizations&lt;/strong&gt;
&lt;strong&gt;level&lt;/strong&gt; denying any principal from disabling Block Public Access or
attaching a public bucket policy in the first place, so an individual
account or a rushed engineer can&apos;t quietly opt back into exposure.
Finally, turn on &lt;strong&gt;CloudTrail S3 data events&lt;/strong&gt; (off by default, since
they&apos;re high-volume) for buckets holding anything sensitive — object-level
&lt;code&gt;GetObject&lt;/code&gt; logging is what tells you whether an exposure window was ever
actually accessed, versus merely theoretically reachable, which matters
enormously for incident scoping and breach-notification decisions.&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;This misconfiguration has been headline news since at least 2017 —
Verizon, Dow Jones, Accenture, WWE, Pentagon contractor INSCOM, and Deep
Root Analytics all made the same mistake within about a twelve-month
stretch — and it&apos;s still a recurring line item in breach reports today,
years after AWS built free, one-click guardrails specifically to stop it.
The reason isn&apos;t that the fix is hard; it&apos;s that S3&apos;s original design let
&quot;public&quot; be an opt-in property of individual objects and policies, which
means it only takes one wrong click, one stale ACL, or one default-public
third-party tool to undo. Block Public Access flips that assumption: it
makes private the property you have to fight to escape, at the account
level, instead of the property you have to remember to defend at the
object level. Every org still running without Block Public Access
enforced by SCP, without Access Analyzer alerting, and without Config
rules gating drift is one misclick away from being next month&apos;s version of
this story.&lt;/p&gt;
</content:encoded></item><item><title>CVE-2024-21626 and runc&apos;s shared-kernel blast radius</title><link>https://jstgtech.com/blog/2026-08-10-runc-containerd-escape/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-10-runc-containerd-escape/</guid><description>A leaked file descriptor in runc, and a fresh 2025 trio of procfs races, show what &quot;container isolation&quot; really guarantees — and what it doesn&apos;t.</description><pubDate>Mon, 10 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Every container on a multi-tenant host is a guest sharing one kernel with
every other tenant. Namespaces and cgroups make that kernel look partitioned,
but they don&apos;t partition the kernel&apos;s &lt;em&gt;code&lt;/em&gt; — a bug reachable from inside a
container is reachable from the same privilege level the kernel runtime
itself trusts. &lt;strong&gt;CVE-2024-21626&lt;/strong&gt;, the &quot;Leaky Vessels&quot; flaw in runc, is the
clearest recent proof: a leaked file descriptor let a malicious image escape
its container and land as root on the host, no exploit chain required beyond
a crafted &lt;code&gt;Dockerfile&lt;/code&gt; (&lt;a href=&quot;https://labs.snyk.io/resources/cve-2024-21626-runc-process-cwd-container-breakout/&quot;&gt;Snyk&lt;/a&gt;, &lt;a href=&quot;https://thehackernews.com/2024/02/runc-flaws-enable-container-escapes.html&quot;&gt;The Hacker News&lt;/a&gt;). And it wasn&apos;t a one-off — a fresh batch of runc container-escape CVEs
landed in late 2025, hitting the exact same trust boundary from a different
angle.&lt;/p&gt;
&lt;h2&gt;Root cause&lt;/h2&gt;
&lt;p&gt;runc is the low-level OCI runtime underneath Docker, containerd, and most of
Kubernetes&apos; container execution path — it&apos;s the thing that actually calls
&lt;code&gt;pivot_root&lt;/code&gt;, sets up namespaces, and execs the container process. CVE-2024-21626
came from an order-of-operations bug: runc could leak an internal file
descriptor referencing the host&apos;s working directory &lt;em&gt;before&lt;/em&gt; it finished
walling the process off with &lt;code&gt;pivot_root&lt;/code&gt;. A malicious image that set its
&lt;code&gt;WORKDIR&lt;/code&gt; to something like &lt;code&gt;/proc/self/fd/7&lt;/code&gt; could ride that leaked fd
straight into a directory on the host filesystem, escaping the container
rootfs entirely (&lt;a href=&quot;https://github.com/strikoder/cve-2024-21626-runc-1.1.11-escape&quot;&gt;GitHub PoC&lt;/a&gt;, &lt;a href=&quot;https://access.redhat.com/security/vulnerabilities/RHSB-2024-001&quot;&gt;Red Hat&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The 2025 trio — &lt;strong&gt;CVE-2025-31133&lt;/strong&gt;, &lt;strong&gt;CVE-2025-52565&lt;/strong&gt;, and &lt;strong&gt;CVE-2025-52881&lt;/strong&gt;,
disclosed by a SUSE researcher in November — is the same story with a
different mechanism: race conditions in how runc mounts &lt;code&gt;/dev&lt;/code&gt; and enforces
&quot;masked paths&quot; under &lt;code&gt;/proc&lt;/code&gt;. An attacker who controls container startup
config can swap &lt;code&gt;/dev/null&lt;/code&gt; or &lt;code&gt;/dev/pts/$n&lt;/code&gt; for a symlink pointing at a
sensitive procfs file milliseconds before runc bind-mounts it, tricking the
runtime into mounting host &lt;code&gt;/proc&lt;/code&gt; paths read-write inside the container, or
redirecting writes meant for a scoped procfs entry to arbitrary host paths
like &lt;code&gt;/proc/sysrq-trigger&lt;/code&gt; (&lt;a href=&quot;https://www.sysdig.com/blog/runc-container-escape-vulnerabilities&quot;&gt;Sysdig&lt;/a&gt;, &lt;a href=&quot;https://github.com/opencontainers/runc/security/advisories/GHSA-9493-h29p-rfm2&quot;&gt;OCI advisory&lt;/a&gt;). Different bug class, same underlying truth: the isolation boundary is
enforced entirely by kernel code that every container process can call into,
and a single logic slip anywhere in that code path breaks the boundary for
everyone sharing the kernel.&lt;/p&gt;
&lt;h2&gt;Blast radius&lt;/h2&gt;
&lt;p&gt;An attacker who lands one of these isn&apos;t stealing data from a container —
they&apos;re getting arbitrary code execution on the &lt;em&gt;host&lt;/em&gt;, at whatever privilege
the container runtime holds, which is typically root. On a shared build farm,
CI runner, or multi-tenant Kubernetes node, that one escape gives an
attacker every other container&apos;s filesystem, secrets mounted into other
pods, the kubelet&apos;s credentials, and a jumping-off point to the rest of the
cluster. Leaky Vessels was especially nasty for CI/CD because it triggers
just by &lt;em&gt;building&lt;/em&gt; a malicious image — you don&apos;t need to run untrusted code
in production, you just need your pipeline to pull an attacker-controlled
base image or Dockerfile and build it (&lt;a href=&quot;https://www.paloaltonetworks.com/blog/cloud-security/leaky-vessels-vulnerabilities-container-escape/&quot;&gt;Palo Alto Networks&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Working proof-of-concept exploit code for CVE-2024-21626 has been public on
GitHub since shortly after disclosure, which is exactly the scenario that
turns a CVSS score into an operational problem — no 0-day skill required,
just an unpatched runtime and a way to get a container built or started. The
2025 trio requires a bit more precision (winning a mount race), but the
outcome is the same: root on the node, and from there, lateral movement
across whatever else that node was trusted to isolate.&lt;/p&gt;
&lt;h2&gt;Remediation&lt;/h2&gt;
&lt;p&gt;Patch the runtime, not just the orchestrator sitting on top of it — Docker
and Kubernetes ship runc bundled, so &quot;update Kubernetes&quot; doesn&apos;t automatically
mean &quot;update runc&quot; if you&apos;re on a vendored or older build:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;CVE-2024-21626&lt;/strong&gt;: fixed in &lt;strong&gt;runc 1.1.12&lt;/strong&gt;, &lt;strong&gt;containerd 1.6.28 / 1.7.13&lt;/strong&gt;,
and &lt;strong&gt;Docker Engine 25.0.2&lt;/strong&gt; (&lt;a href=&quot;https://scout.docker.com/vulnerabilities/id/CVE-2024-21626&quot;&gt;Docker Scout&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CVE-2025-31133 / CVE-2025-52565 / CVE-2025-52881&lt;/strong&gt;: fixed in &lt;strong&gt;runc&lt;/strong&gt;
**  1.2.8, 1.3.3, and 1.4.0-rc.3** (&lt;a href=&quot;https://www.securityweek.com/runc-vulnerabilities-can-be-exploited-to-escape-containers/&quot;&gt;SecurityWeek&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Beyond patching, this bug class is exactly why &quot;container&quot; and &quot;security
boundary&quot; shouldn&apos;t be treated as synonyms for anything you don&apos;t trust.
Defense in depth that actually helps here: run &lt;strong&gt;rootless containers&lt;/strong&gt; so a
kernel-level escape lands as an unprivileged host user instead of root;
apply &lt;strong&gt;seccomp&lt;/strong&gt; and drop capabilities aggressively so even a successful
escape has less to work with; and for genuinely untrusted workloads — public
CI runners, multi-tenant SaaS sandboxes, anything running code you didn&apos;t
write — put a real boundary under the shared kernel with &lt;strong&gt;gVisor&lt;/strong&gt; (a
userspace syscall shim the exploit has to get through first) or &lt;strong&gt;Kata&lt;/strong&gt;
&lt;strong&gt;Containers&lt;/strong&gt; (a real VM boundary per workload, so a kernel exploit only pops
that one micro-VM).&lt;/p&gt;
&lt;h2&gt;The bigger lesson&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;runc&lt;/code&gt;, &lt;code&gt;containerd&lt;/code&gt;, and every OCI-compliant runtime built on the shared-kernel
model give you process isolation, not a security boundary against a
sufficiently novel kernel bug — and this class of vulnerability proves that
gap isn&apos;t hypothetical, it&apos;s recurred across at least two unrelated bug
classes in two years. That&apos;s fine for internal services running your own
trusted images. It&apos;s a real risk for anything that runs code you don&apos;t
control: public CI, contributor-submitted builds, multi-tenant platforms.
For those, the honest question isn&apos;t &quot;did we patch runc&quot; — it&apos;s &quot;why are we
relying on a shared kernel to isolate an adversary in the first place,&quot; and
whether a VM-backed runtime like Kata or a syscall-filtering sandbox like
gVisor should be the default instead of the exception.&lt;/p&gt;
</content:encoded></item><item><title>Cloud roundup: unauthenticated TeamCity RCE now in KEV</title><link>https://jstgtech.com/blog/2026-08-09-cloud-roundup/</link><guid isPermaLink="true">https://jstgtech.com/blog/2026-08-09-cloud-roundup/</guid><description>A critical unauthenticated TeamCity RCE hits the CISA KEV list, plus Tomcat and Langflow exploitation and a nice AWS Lambda bandwidth bump.</description><pubDate>Sun, 09 Aug 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you run a self-hosted CI/CD server, stop reading and go patch it. That&apos;s the headline today — the rest is a mix of AWS platform news and more actively-exploited flaws in tools a lot of us have sitting in our stacks.&lt;/p&gt;
&lt;h2&gt;TeamCity RCE is now under active exploitation&lt;/h2&gt;
&lt;p&gt;CISA added &lt;strong&gt;CVE-2026-63077&lt;/strong&gt;, a critical (CVSS 9.8) unauthenticated remote code execution flaw in JetBrains TeamCity On-Premises, to the Known Exploited Vulnerabilities catalog on August 5 (&lt;a href=&quot;https://www.cisa.gov/news-events/alerts/2026/08/05/cisa-adds-one-known-exploited-vulnerability-catalog&quot;&gt;CISA&lt;/a&gt;, &lt;a href=&quot;https://thehackernews.com/2026/08/cisa-flags-teamcity-cve-2026-63077-rce.html&quot;&gt;The Hacker News&lt;/a&gt;). An attacker can abuse the agent polling protocol to skip authentication entirely and run arbitrary OS commands as the TeamCity server process — which means your build agents, artifacts, and any credentials TeamCity holds for deploying into AWS are all in play. JetBrains has patches out (upgrade to 2025.11.7 or 2026.1.3, per &lt;a href=&quot;https://blog.jetbrains.com/teamcity/2026/07/cve-2026-63077/&quot;&gt;JetBrains&apos; advisory&lt;/a&gt;); federal agencies were given until August 8 to remediate under BOD 26-04. If your TeamCity server has any path to the public internet, treat this like the SolarWinds-style CI/CD compromise scenario it is and patch today, then audit what credentials that server has been handing out.&lt;/p&gt;
&lt;h2&gt;Two more KEV additions worth knowing about&lt;/h2&gt;
&lt;p&gt;The same CISA update cycle also added &lt;strong&gt;CVE-2026-9198&lt;/strong&gt;, a code injection bug in Langflow (the visual builder a lot of teams use to prototype LLM pipelines) that gives unauthenticated RCE on default installs — telemetry shows 650+ exploitation attempts from 244 unique IPs since early July (&lt;a href=&quot;https://thehackernews.com/2026/08/cisa-flags-langflow-rce-tomcat-and-n.html&quot;&gt;The Hacker News&lt;/a&gt;). And &lt;strong&gt;CVE-2026-34486&lt;/strong&gt;, a missing-encryption flaw in Apache Tomcat&apos;s &lt;code&gt;EncryptInterceptor&lt;/code&gt;, is reportedly being hit by an AI-orchestrated, China-nexus automated attack campaign. If you&apos;ve stood up Langflow for an internal AI experiment and forgotten about it, that&apos;s exactly the kind of exposed dev tool this campaign is scanning for — take: audit anything you spun up &quot;just to try AI stuff&quot; six months ago and never locked down.&lt;/p&gt;
&lt;h2&gt;AWS Lambda gets a real bandwidth bump&lt;/h2&gt;
&lt;p&gt;Less urgent, more useful: AWS Lambda functions outside a VPC with 2 GB+ of memory now scale network bandwidth proportionally, from 625 Mbps at 2 GB up to 3,000 Mbps at 10 GB, at no extra charge (&lt;a href=&quot;https://aws.amazon.com/about-aws/whats-new/2026/08/aws-lambda-network-bandwidth/&quot;&gt;AWS&lt;/a&gt;). That&apos;s a meaningful jump for anything doing large payload transfers, S3 streaming, or bursty data-heavy work in Lambda — previously you were capped at 625 Mbps regardless of memory. Catch: it&apos;s not automatic. You have to request the &quot;Network bandwidth per execution environment&quot; quota bump via Service Quotas before your functions see it, so if you have latency-sensitive Lambdas moving a lot of data, it&apos;s worth requesting now even if you don&apos;t need it yet — quota changes aren&apos;t instant.&lt;/p&gt;
&lt;h2&gt;Also worth a look&lt;/h2&gt;
&lt;p&gt;CloudWatch now offers managed Prometheus collectors that auto-provision and scale to pull OpenTelemetry/Prometheus metrics from EKS, EC2, ECS, MSK, and OpenSearch without you running your own collector fleet — worth a look if you&apos;re maintaining a hand-rolled Prometheus scraping setup on EKS. And separately, N-able confirmed active exploitation of an N-central authentication bypass (&lt;strong&gt;CVE-2026-18577&lt;/strong&gt;) that let an attacker pivot from a compromised N-central server into managed endpoints — a reminder that RMM/MSP tooling is a high-value target precisely because of the blast radius one compromised instance gives an attacker.&lt;/p&gt;
&lt;h2&gt;Bottom line&lt;/h2&gt;
&lt;p&gt;Today&apos;s theme is &quot;your build and automation tooling is the target, not just your app.&quot; Patch TeamCity if you run it, sweep for forgotten Langflow instances, and grab the free Lambda bandwidth quota bump while it&apos;s on your mind.&lt;/p&gt;
</content:encoded></item></channel></rss>