<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Peter Brau</title>
    <description>The latest articles on DEV Community by Peter Brau (@psbrau-tech).</description>
    <link>https://dev.to/psbrau-tech</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4140175%2F07276bdd-28a7-411a-a62b-e06f72fb1e21.png</url>
      <title>DEV Community: Peter Brau</title>
      <link>https://dev.to/psbrau-tech</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/psbrau-tech"/>
    <language>en</language>
    <item>
      <title>A Successful Docker Build Does Not Mean You Built a Runnable Release</title>
      <dc:creator>Peter Brau</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:16:14 +0000</pubDate>
      <link>https://dev.to/psbrau-tech/a-successful-docker-build-does-not-mean-you-built-a-runnable-release-213e</link>
      <guid>https://dev.to/psbrau-tech/a-successful-docker-build-does-not-mean-you-built-a-runnable-release-213e</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;A container image can build successfully and still be unusable as a release artifact.&lt;/p&gt;

&lt;p&gt;A successful image build proves that Docker could assemble the image filesystem and metadata. It does not prove that the image's configured process can start, import the application correctly, remain alive, or answer the application's minimum health contract.&lt;/p&gt;

&lt;p&gt;The observed failure class was exactly that gap: the image existed and could be retrieved, but the application process failed because Uvicorn was pointed at the wrong application target.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Signature
&lt;/h2&gt;

&lt;p&gt;The build and registry stages succeed, but the deployment never becomes healthy because the container's primary process exits or cannot initialize the application.&lt;/p&gt;

&lt;p&gt;At that point the defect is surrounded by infrastructure noise:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the registry already contains the candidate image;&lt;/li&gt;
&lt;li&gt;an orchestrator pulls it;&lt;/li&gt;
&lt;li&gt;health checks begin failing;&lt;/li&gt;
&lt;li&gt;deployment stabilization waits consume time;&lt;/li&gt;
&lt;li&gt;rollback or replacement may start;&lt;/li&gt;
&lt;li&gt;operators have to distinguish an application-startup defect from networking, load balancing, IAM, or orchestration problems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the same image would have failed immediately on a local runner, discovering it after publication is unnecessary delay.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Expected
&lt;/h2&gt;

&lt;p&gt;The release pipeline implicitly treated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source -&amp;gt; docker build -&amp;gt; registry push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;as evidence that the artifact was deployable.&lt;/p&gt;

&lt;p&gt;The stronger release contract should have been:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source
  -&amp;gt; build image
  -&amp;gt; start that exact image
  -&amp;gt; wait for application readiness
  -&amp;gt; call a minimal health endpoint
  -&amp;gt; stop the container
  -&amp;gt; publish that exact image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The CI contract validated &lt;strong&gt;packaging&lt;/strong&gt;, not &lt;strong&gt;runtime startup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In the observed case, the Uvicorn command did not match the application's factory-style startup contract. Uvicorn's current documentation still distinguishes the factory form: &lt;code&gt;--factory&lt;/code&gt; tells Uvicorn to treat the referenced application target as a callable that returns an ASGI application.&lt;/p&gt;

&lt;p&gt;A generic factory-style startup command looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uvicorn &amp;lt;APPLICATION_MODULE&amp;gt;:create_app --factory --host 0.0.0.0 --port 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module and callable are application-specific. The reusable lesson is to make that startup contract executable in CI before the artifact becomes a release candidate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Obvious Fix Was Wrong
&lt;/h2&gt;

&lt;p&gt;Fixing the command in the deployment environment alone repairs one incident but leaves the detection gap intact.&lt;/p&gt;

&lt;p&gt;The next malformed entry point, missing runtime dependency, import failure, or startup configuration error can still travel through image publication and reach the orchestrator before anyone discovers it.&lt;/p&gt;

&lt;p&gt;A second weak fix is to build one image for the smoke test and then rebuild another image for publication. That proves one artifact and ships another.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Correction
&lt;/h2&gt;

&lt;p&gt;Run the exact built image as a container before publication and require a minimal health check to pass.&lt;/p&gt;

&lt;p&gt;Docker's current CLI documentation defines &lt;code&gt;docker run&lt;/code&gt; as creating and running a container from an image. A generic CI preflight can therefore use the same image reference that will later be pushed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;container_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"app-under-test"&lt;/span&gt;

cleanup&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1 &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;trap &lt;/span&gt;cleanup EXIT

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 127.0.0.1:8080:8000 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$IMAGE_ID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;for &lt;/span&gt;attempt &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;1 30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  if &lt;/span&gt;curl &lt;span class="nt"&gt;--fail&lt;/span&gt; &lt;span class="nt"&gt;--silent&lt;/span&gt; http://127.0.0.1:8080/health &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;sleep &lt;/span&gt;1
&lt;span class="k"&gt;done

&lt;/span&gt;docker logs &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$container_name&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details should match the application:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use the real startup command baked into the image unless the test is explicitly validating an override;&lt;/li&gt;
&lt;li&gt;use a health endpoint that proves the application initialized far enough to serve its minimum contract;&lt;/li&gt;
&lt;li&gt;keep host port exposure loopback-only for the CI check;&lt;/li&gt;
&lt;li&gt;capture container logs when startup fails;&lt;/li&gt;
&lt;li&gt;guarantee cleanup on success and failure.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After the preflight passes, publish the &lt;strong&gt;same image identity&lt;/strong&gt;. Do not rebuild between validation and push.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The correction proves the relevant failure class when CI fails before publication if the configured process cannot start or the health endpoint never becomes ready.&lt;/p&gt;

&lt;p&gt;A regression test should deliberately exercise the startup contract so that an invalid application target or equivalent startup defect is caught without a registry push, orchestrator rollout, or rollback.&lt;/p&gt;

&lt;p&gt;The positive path should demonstrate that the exact image which passed the startup check is the image subsequently published.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Have Caught This Earlier
&lt;/h2&gt;

&lt;p&gt;Container publication should have a release preflight with three explicit assertions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Process assertion:&lt;/strong&gt; the configured primary process remains running long enough to initialize.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Health assertion:&lt;/strong&gt; the application responds successfully on its minimum local health contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Artifact-identity assertion:&lt;/strong&gt; the image published is the same immutable artifact that passed the first two checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is a good example of moving failure left without inventing a large test system. A short local container run can eliminate an entire class of slow deployment-time diagnosis.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable Rule
&lt;/h2&gt;

&lt;p&gt;A Docker build proves that an image can be assembled. A release check must prove that the intended process starts and the application can answer a minimal health contract.&lt;/p&gt;

&lt;p&gt;Build it, run it, health-check it, then publish that exact artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/reference/cli/docker/container/run/" rel="noopener noreferrer"&gt;Docker: docker container run&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.uvicorn.org/settings/" rel="noopener noreferrer"&gt;Uvicorn: Settings&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>deployment</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>CloudFormation Generated Role Names Can Break Least-Privilege IAM Twice</title>
      <dc:creator>Peter Brau</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:15:01 +0000</pubDate>
      <link>https://dev.to/psbrau-tech/cloudformation-generated-role-names-can-break-least-privilege-iam-twice-2jg</link>
      <guid>https://dev.to/psbrau-tech/cloudformation-generated-role-names-can-break-least-privilege-iam-twice-2jg</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Least-privilege CloudFormation execution roles are often scoped to the IAM resources a stack is expected to create. That is a sound approach until the policy assumes a physical role-name pattern that CloudFormation does not actually use.&lt;/p&gt;

&lt;p&gt;AWS currently documents that when &lt;code&gt;RoleName&lt;/code&gt; is omitted from an &lt;code&gt;AWS::IAM::Role&lt;/code&gt;, CloudFormation generates a unique physical ID and uses that value as the role name. The logical resource name is therefore not a reliable policy boundary by itself.&lt;/p&gt;

&lt;p&gt;A mismatch can break the forward deployment and then break rollback against the same generated role family.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Signature
&lt;/h2&gt;

&lt;p&gt;The stack begins creating an IAM role, then an IAM operation fails because the execution policy's resource ARN pattern does not match the physical role name CloudFormation generated.&lt;/p&gt;

&lt;p&gt;In the observed failure class, one generated role retained the expected naming shape while another generated role used a shorter physical name than the policy anticipated. The first visible denial occurred during tagging. When the stack rolled back, cleanup exposed the same resource-boundary mistake again.&lt;/p&gt;

&lt;p&gt;That sequence is a useful signal: if both forward work and rollback are failing against the same IAM resource family, inspect the physical identity before adding more actions.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Expected
&lt;/h2&gt;

&lt;p&gt;The execution policy was intended to authorize a bounded family of stack-created IAM roles, not every IAM role in the account.&lt;/p&gt;

&lt;p&gt;The expectation was that the generated physical role names would continue to match a longer prefix inferred from the stack and logical-resource naming convention.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The policy was written from the &lt;strong&gt;intended naming convention&lt;/strong&gt; rather than the &lt;strong&gt;observed physical resource identity&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A resource scope such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:role/&amp;lt;EXPECTED_BOUNDED_PREFIX&amp;gt;*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is only useful if &lt;code&gt;&amp;lt;EXPECTED_BOUNDED_PREFIX&amp;gt;&lt;/code&gt; actually matches the role names CloudFormation creates.&lt;/p&gt;

&lt;p&gt;The lesson is not that generated names are unsafe. The lesson is that generated physical IDs are an implementation boundary that least-privilege policy must account for. CloudFormation's current &lt;code&gt;AWS::IAM::Role&lt;/code&gt; documentation explicitly says that it generates a unique physical ID for the role name when &lt;code&gt;RoleName&lt;/code&gt; is not supplied.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Obvious Fix Was Wrong
&lt;/h2&gt;

&lt;p&gt;The tempting correction is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:role/*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes a naming mismatch disappear by allowing the execution role to operate against every role in the account. It also destroys the resource boundary the policy was meant to preserve.&lt;/p&gt;

&lt;p&gt;A second tempting mistake is to add only the action that failed during the forward path. That can get the deployment farther while leaving rollback unable to detach, untag, or delete the role if a later resource fails.&lt;/p&gt;

&lt;p&gt;Least privilege must cover the intended &lt;strong&gt;lifecycle&lt;/strong&gt;, not just the happy-path create call.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Correction
&lt;/h2&gt;

&lt;p&gt;Use CloudFormation evidence to derive a bounded physical-name contract:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the stack event containing the denied IAM action and resource ARN.&lt;/li&gt;
&lt;li&gt;Inspect the actual physical role name CloudFormation created or attempted to use.&lt;/li&gt;
&lt;li&gt;Compare that name with the resource ARN pattern in the execution policy.&lt;/li&gt;
&lt;li&gt;Replace the incorrect assumed prefix with the smallest verified prefix that covers only the intended generated role family.&lt;/li&gt;
&lt;li&gt;Review the IAM actions needed for both forward operations and rollback/cleanup.&lt;/li&gt;
&lt;li&gt;Keep &lt;code&gt;iam:PassRole&lt;/code&gt; separately constrained to the intended role resources and destination service.&lt;/li&gt;
&lt;li&gt;Add a policy-contract test for the bounded physical-name pattern.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The exact lifecycle actions depend on the template. Depending on what the stack manages, the forward path may need operations such as role creation, tagging, policy attachment, and role passing; rollback may need the corresponding detach, untag, and delete operations.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;iam:PassRole&lt;/code&gt;, AWS documents the &lt;code&gt;iam:PassedToService&lt;/code&gt; condition key as a way to restrict which service may receive the role. For an ECS task role, a generic condition shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"iam:PassedToService"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ecs-tasks.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That condition should complement, not replace, a bounded &lt;code&gt;Resource&lt;/code&gt; ARN for the roles that may be passed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The forward-path correction is proven when CloudFormation can perform the intended IAM operations against the generated role without broadening the policy to all roles.&lt;/p&gt;

&lt;p&gt;The rollback contract should be verified separately. A deployment permission model is incomplete if a later stack failure can leave CloudFormation unable to detach policies, remove tags, or delete resources that the same execution role was allowed to create.&lt;/p&gt;

&lt;p&gt;For protected environments, the safest proof is a synthetic or otherwise controlled lifecycle exercise that demonstrates both creation/update and complete rollback under the same bounded policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Have Caught This Earlier
&lt;/h2&gt;

&lt;p&gt;Three controls can catch this class before it consumes a long protected deployment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Physical-name preflight:&lt;/strong&gt; compare expected IAM ARN patterns with generated physical names observed in a safe change or prior synthetic stack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forward/rollback policy matrix:&lt;/strong&gt; for each stack-managed IAM resource family, map create/update operations to their cleanup counterparts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static policy contract:&lt;/strong&gt; test that the execution role permits only the verified bounded prefixes and that &lt;code&gt;iam:PassRole&lt;/code&gt; retains both resource and service-principal constraints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a naming assumption is not backed by an observed physical resource or current CloudFormation behavior, treat it as an unverified dependency.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable Rule
&lt;/h2&gt;

&lt;p&gt;Least-privilege IAM for CloudFormation must be written against what AWS actually creates, not what a logical resource name makes you expect.&lt;/p&gt;

&lt;p&gt;And deployment permission is not complete until rollback can undo what the forward path was allowed to create.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/TemplateReference/aws-resource-iam-role.html" rel="noopener noreferrer"&gt;AWS CloudFormation: AWS::IAM::Role&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html" rel="noopener noreferrer"&gt;AWS CloudFormation: Set a service role&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_passrole.html" rel="noopener noreferrer"&gt;AWS IAM: Grant permissions to pass a role&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html" rel="noopener noreferrer"&gt;AWS IAM: IAM and STS condition context keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>infrastructure</category>
      <category>security</category>
    </item>
    <item>
      <title>GitHub Actions OIDC AccessDenied May Be a Trust-Policy Problem, Not a Permission-Policy Problem</title>
      <dc:creator>Peter Brau</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:01:14 +0000</pubDate>
      <link>https://dev.to/psbrau-tech/github-actions-oidc-accessdenied-may-be-a-trust-policy-problem-not-a-permission-policy-problem-55fc</link>
      <guid>https://dev.to/psbrau-tech/github-actions-oidc-accessdenied-may-be-a-trust-policy-problem-not-a-permission-policy-problem-55fc</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;When a GitHub Actions job cannot assume an AWS role through OpenID Connect, the first instinct is often to inspect or widen the role's attached AWS permissions. That can target the wrong layer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sts:AssumeRoleWithWebIdentity&lt;/code&gt; is an identity-and-trust decision. AWS evaluates whether the incoming OIDC identity is allowed to obtain the role before the resulting session receives the role's service permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Signature
&lt;/h2&gt;

&lt;p&gt;The workflow reaches AWS role assumption and fails with an authorization error before it can call the AWS service the job was intended to use.&lt;/p&gt;

&lt;p&gt;That is materially different from a workflow that assumes the role successfully and then receives &lt;code&gt;AccessDenied&lt;/code&gt; from an AWS service API.&lt;/p&gt;

&lt;p&gt;The first failure class points to OIDC identity and trust. The second points to the permissions granted after assumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Expected
&lt;/h2&gt;

&lt;p&gt;A GitHub Actions workload with &lt;code&gt;id-token: write&lt;/code&gt; should request an OIDC token whose issuer, audience, and subject satisfy the AWS role's trust policy. AWS STS should then issue a short-lived role session, after which the role's normal permission policy governs service access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The failure class we diagnosed came from a mismatch between the workload identity GitHub presented and the identity the AWS trust policy permitted.&lt;/p&gt;

&lt;p&gt;The important trust inputs are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the AWS OIDC provider for &lt;code&gt;token.actions.githubusercontent.com&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the audience expected by AWS STS, commonly &lt;code&gt;sts.amazonaws.com&lt;/code&gt; when using the standard AWS credentials flow;&lt;/li&gt;
&lt;li&gt;the exact GitHub &lt;code&gt;sub&lt;/code&gt; claim authorized by the trust policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The subject deserves special attention because it depends on workflow context. A job that references a GitHub Environment has an environment-oriented subject rather than the branch or pull-request subject a copied example may use.&lt;/p&gt;

&lt;p&gt;There is also a newer compatibility boundary. GitHub documents that repositories created after &lt;strong&gt;July 15, 2026&lt;/strong&gt; use an immutable default subject format that includes owner and repository IDs. Repositories created earlier keep the previous name-based format unless they opt in, while later renames or transfers also move to the immutable format. A trust policy should therefore be derived from the subject format the repository actually uses, not from a hard-coded historical example.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Obvious Fix Was Wrong
&lt;/h2&gt;

&lt;p&gt;Adding &lt;code&gt;s3:*&lt;/code&gt;, &lt;code&gt;cloudformation:*&lt;/code&gt;, or another AWS service permission to the role cannot make a failed trust decision succeed. Those permissions are available only after AWS has issued the role session.&lt;/p&gt;

&lt;p&gt;Widening them during an OIDC failure increases privilege without fixing the failing control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Correction
&lt;/h2&gt;

&lt;p&gt;Keep the trust policy narrow and make its claims match the actual workflow identity.&lt;/p&gt;

&lt;p&gt;A generic shape is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Federated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;AWS_ACCOUNT_ID&amp;gt;:oidc-provider/token.actions.githubusercontent.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"token.actions.githubusercontent.com:sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;GITHUB_SUBJECT&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;lt;GITHUB_SUBJECT&amp;gt;&lt;/code&gt; is deliberately a placeholder. Determine it from the repository's current OIDC subject mode and the job context. If the job uses a protected GitHub Environment, keep the trust relationship aligned with that environment and use the environment's protection rules as another control.&lt;/p&gt;

&lt;p&gt;A useful diagnostic sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Confirm the job has &lt;code&gt;permissions: id-token: write&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Confirm the AWS account has the intended GitHub OIDC provider.&lt;/li&gt;
&lt;li&gt;Confirm the role trust policy names that provider as the federated principal.&lt;/li&gt;
&lt;li&gt;Confirm the &lt;code&gt;aud&lt;/code&gt; condition matches the audience requested by the credentials flow.&lt;/li&gt;
&lt;li&gt;Confirm the &lt;code&gt;sub&lt;/code&gt; condition matches the repository's current GitHub subject format and the job context.&lt;/li&gt;
&lt;li&gt;Only after role assumption succeeds, diagnose service-level permissions.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The correction is validated when the workflow can obtain the intended AWS role session under the expected GitHub context while a context outside the trust boundary remains unauthorized.&lt;/p&gt;

&lt;p&gt;An identity-only preflight can stop after role assumption and an STS identity check. That cleanly proves the trust layer before any deployment or mutation is attempted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Have Caught This Earlier
&lt;/h2&gt;

&lt;p&gt;Treat OIDC trust as a testable contract.&lt;/p&gt;

&lt;p&gt;Before a deployment workflow is allowed to execute protected operations, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the repository's current OIDC subject format;&lt;/li&gt;
&lt;li&gt;the expected audience;&lt;/li&gt;
&lt;li&gt;the environment, branch, or event context encoded in the subject;&lt;/li&gt;
&lt;li&gt;the exact federated provider ARN;&lt;/li&gt;
&lt;li&gt;a zero-change role-assumption preflight.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For long-lived repositories, record whether they use the previous or immutable GitHub subject format so a rename, transfer, or opt-in does not become a surprise production failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable Rule
&lt;/h2&gt;

&lt;p&gt;If GitHub Actions fails at &lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;, debug identity and trust first. Do not widen the role's AWS service permissions until the role can actually be assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/reference/security/oidc" rel="noopener noreferrer"&gt;GitHub: OpenID Connect reference&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/actions/how-tos/secure-your-work/security-harden-deployments/oidc-in-aws" rel="noopener noreferrer"&gt;GitHub: Configuring OpenID Connect in Amazon Web Services&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_create_for-idp_oidc.html" rel="noopener noreferrer"&gt;AWS IAM: Create a role for OpenID Connect federation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>github</category>
      <category>security</category>
    </item>
    <item>
      <title>CloudFormation Least Privilege Can Fail Outside the Service You Think You Are Deploying</title>
      <dc:creator>Peter Brau</dc:creator>
      <pubDate>Thu, 24 Sep 2026 00:01:13 +0000</pubDate>
      <link>https://dev.to/psbrau-tech/cloudformation-least-privilege-can-fail-outside-the-service-you-think-you-are-deploying-2kgl</link>
      <guid>https://dev.to/psbrau-tech/cloudformation-least-privilege-can-fail-outside-the-service-you-think-you-are-deploying-2kgl</guid>
      <description>&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;A least-privilege CloudFormation execution role can look correct when compared with the resources in a template and still fail during deployment.&lt;/p&gt;

&lt;p&gt;The reason is that CloudFormation does not merely translate each resource type into one same-service API call. The resource implementation can perform supporting lookups, associations, tagging, or cleanup through APIs in adjacent AWS services.&lt;/p&gt;

&lt;p&gt;If the execution role was designed only from the visible service names in the template, a legitimate provider-side dependency can surface as an unexpected &lt;code&gt;AccessDenied&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure Signature
&lt;/h2&gt;

&lt;p&gt;The stack reaches a valid resource operation and fails on an AWS API action that appears to belong to a different service than the resource being created or associated.&lt;/p&gt;

&lt;p&gt;Two observed examples illustrate the class:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an Application Load Balancer creation path required &lt;code&gt;ec2:GetSecurityGroupsForVpc&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;a Web ACL association path required a load-balancer-side permission in addition to WAF permissions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first denial looked surprising because the resource was an Elastic Load Balancing resource and the missing EC2 action was not a &lt;code&gt;Describe*&lt;/code&gt; call.&lt;/p&gt;

&lt;p&gt;The second looked surprising because the operation was conceptually a WAF association, yet the integration crossed the WAF and load-balancer permission boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  What We Expected
&lt;/h2&gt;

&lt;p&gt;The execution role was intended to permit only the API surface required to create, update, and remove the stack's resources.&lt;/p&gt;

&lt;p&gt;The mistaken assumption was that the required API surface could be inferred directly from the service namespace of each CloudFormation resource type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The policy model was based on &lt;strong&gt;template service ownership&lt;/strong&gt;, while CloudFormation's actual resource operation depended on &lt;strong&gt;provider-side API behavior&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For the load-balancer case, the provider called the current EC2 API &lt;code&gt;GetSecurityGroupsForVpc&lt;/code&gt;. A narrowly scoped correction can grant that action without granting broad EC2 access. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ec2:GetSecurityGroupsForVpc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"aws:RequestedRegion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;AWS_REGION&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Resource: "*"&lt;/code&gt; does not mean the entire role should be broad. Some AWS APIs do not offer useful resource-level ARN scoping for the request. Least privilege then comes from granting the smallest action set and applying supported condition boundaries.&lt;/p&gt;

&lt;p&gt;The Web ACL example also demonstrates why historical incident evidence must be rechecked before reuse. The observed denial involved &lt;code&gt;elasticloadbalancing:SetWebACL&lt;/code&gt;. Current AWS WAF documentation now distinguishes that older Application Load Balancer setting from a newer association model that uses load-balancer-side &lt;code&gt;CreateWebACLAssociation&lt;/code&gt; and &lt;code&gt;DeleteWebACLAssociation&lt;/code&gt; permissions along with WAF permissions.&lt;/p&gt;

&lt;p&gt;That change does not weaken the lesson. It strengthens it: provider and integration API paths can evolve, so the exact current denied action and current service documentation are better inputs than a permanent hard-coded permission list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Obvious Fix Was Wrong
&lt;/h2&gt;

&lt;p&gt;A common response to an unexpected provider-side denial is to attach a broad AWS managed policy for the adjacent service until the deployment succeeds.&lt;/p&gt;

&lt;p&gt;That solves uncertainty by expanding privilege. It also discards the most useful evidence in the failure: AWS already told you the exact action that was denied.&lt;/p&gt;

&lt;p&gt;For the observed EC2 lookup, broad EC2 read or write access was unnecessary. For the WAF/load-balancer association, broad load-balancer access was unnecessary. The correction could remain focused on the required integration action.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimal Correction
&lt;/h2&gt;

&lt;p&gt;Use the denial as a dependency-discovery signal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capture the exact denied action and resource, if one is reported.&lt;/li&gt;
&lt;li&gt;Identify the CloudFormation resource or association operation active at the time.&lt;/li&gt;
&lt;li&gt;Check the &lt;strong&gt;current&lt;/strong&gt; AWS documentation for that provider/integration path.&lt;/li&gt;
&lt;li&gt;Determine whether the missing API supports resource-level scoping.&lt;/li&gt;
&lt;li&gt;Add only the required action and the strongest supported resource or condition boundary.&lt;/li&gt;
&lt;li&gt;Review a fresh change set before protected execution.&lt;/li&gt;
&lt;li&gt;Add the newly discovered dependency to a static policy-contract test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Do not preserve an action forever merely because one historical provider path used it. Revalidate integration permissions when AWS changes the underlying association model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification
&lt;/h2&gt;

&lt;p&gt;The correction is validated when the same resource operation succeeds under the narrowed execution role without requiring a broader managed policy.&lt;/p&gt;

&lt;p&gt;For a provider-side dependency, verification should cover both the forward operation and any corresponding update or removal path that the stack can exercise. The policy-contract test should then assert the narrow dependency so a later refactor does not silently remove it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Should Have Caught This Earlier
&lt;/h2&gt;

&lt;p&gt;A useful preflight is a &lt;strong&gt;resource-provider dependency ledger&lt;/strong&gt; for protected infrastructure changes.&lt;/p&gt;

&lt;p&gt;For each resource family, record adjacent-service actions discovered through current documentation or validated deployment evidence. Static IAM tests can then check that the execution role includes the required narrow actions before a long-running CloudFormation operation begins.&lt;/p&gt;

&lt;p&gt;The ledger should be treated as versioned knowledge rather than a permanent truth table. When an AWS integration changes, update the contract instead of accumulating obsolete permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reusable Rule
&lt;/h2&gt;

&lt;p&gt;When a least-privilege CloudFormation deployment fails, do not ask only, "What service is this resource from?" Ask, "What APIs does the current resource operation call to create, associate, inspect, update, and remove it?"&lt;/p&gt;

&lt;p&gt;That question usually leads to a smaller and safer correction than attaching another broad policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html" rel="noopener noreferrer"&gt;AWS CloudFormation: Set a service role&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_GetSecurityGroupsForVpc.html" rel="noopener noreferrer"&gt;Amazon EC2 API: GetSecurityGroupsForVpc&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/waf/latest/developerguide/security_iam_service-with-iam.html" rel="noopener noreferrer"&gt;AWS WAF: How AWS WAF works with IAM&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>aws</category>
      <category>infrastructure</category>
      <category>security</category>
    </item>
  </channel>
</rss>
