<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[João Penteado]]></title><description><![CDATA[Security engineer by trade, curious about most everything else. Based in Japan. I write about digital security, tech, and where it intersects with society. Technical deep dives, industry commentary, and the occasional strong take. Views are my own.]]></description><link>https://www.joaopenteado.com</link><image><url>https://substackcdn.com/image/fetch/$s_!oJEN!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F85cfbb9f-8e0a-491f-a503-032a33eeb654_336x336.png</url><title>João Penteado</title><link>https://www.joaopenteado.com</link></image><generator>Substack</generator><lastBuildDate>Thu, 17 Sep 2026 16:44:19 GMT</lastBuildDate><atom:link href="https://www.joaopenteado.com/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[João Penteado]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[joaopenteado@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[joaopenteado@substack.com]]></itunes:email><itunes:name><![CDATA[João Penteado]]></itunes:name></itunes:owner><itunes:author><![CDATA[João Penteado]]></itunes:author><googleplay:owner><![CDATA[joaopenteado@substack.com]]></googleplay:owner><googleplay:email><![CDATA[joaopenteado@substack.com]]></googleplay:email><googleplay:author><![CDATA[João Penteado]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[Paved Roads Without Walled Gardens]]></title><description><![CDATA[Making the repository the lifecycle root for a service and its infrastructure.]]></description><link>https://www.joaopenteado.com/p/paved-roads-without-walled-gardens</link><guid isPermaLink="false">https://www.joaopenteado.com/p/paved-roads-without-walled-gardens</guid><dc:creator><![CDATA[João Penteado]]></dc:creator><pubDate>Wed, 12 Aug 2026 09:01:18 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/040d13ee-2851-4536-aaf4-93aac9241706_1672x941.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!63XO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!63XO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!63XO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!63XO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!63XO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!63XO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png" width="1456" height="819" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ba909e28-6987-4695-8816-da248713f0e0_1672x941.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:819,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2588890,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.joaopenteado.com/i/210176938?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!63XO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 424w, https://substackcdn.com/image/fetch/$s_!63XO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 848w, https://substackcdn.com/image/fetch/$s_!63XO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 1272w, https://substackcdn.com/image/fetch/$s_!63XO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fba909e28-6987-4695-8816-da248713f0e0_1672x941.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A pattern I have found useful for small services, internal tools, and projects that benefit from fast iteration is to make the <strong>repository itself the lifecycle boundary for the service and its infrastructure</strong>.</p><p>The basic idea is simple:</p><blockquote><p>Create the Git repository from a specific template, and the necessary infrastructure is provisioned along with it automatically.</p></blockquote><p>I think of the resources associated with a repository as an infrastructure <em>arena</em>: an isolated set of environments, cloud resources, identities, Terraform state, deployment automation, and other service-specific machinery whose lifecycle is <strong>tied</strong> to that repository.</p><p>This is not a pattern I would claim is universally applicable. There are plenty of systems whose infrastructure have a longer lifecycles than any individual application repository, and plenty of organizations where shared resources, cross-service dependencies or more centralized control make sense.</p><p>But for the right class of workload, I have found this model makes experimentation and service development substantially cheaper.</p><h2>The problem</h2><p>Provisioning a new service usually involves significantly more work than developing the service itself.</p><p>Before the first useful line of application code is deployed, someone may need to create a cloud project or account, configure Terraform state, create identities and permissions, establish CI/CD, configure workload identity, create registries, set up deployment infrastructure, and wire all of those things together.</p><p>Platform teams often try to solve this by introducing increasingly high-level abstractions, which in many cases result in big modules, multiple infrastructure monorepos or automated workflows.</p><p>That can work, but those abstractions often come with a different cost: <strong>the platform begins to dictate what an application is supposed to look like.</strong> They are usually designed around what infrastructure teams think is an &#8220;average&#8221; service, yet very few services are actually <em>completely</em> average in practice. The result is often infrastructure that only approximately fits the workload: overprovisioned resources, unnecessary components, and design choices preserved primarily for the sake of uniformity. Opting out may be technically possible, but doing so often means leaving the intended golden path, taking on additional work, and having to justify decisions that would otherwise be perfectly reasonable for the service at hand.</p><p>I believe that automating the repetitive parts aggressively while keeping the underlying infrastructure accessible is more scalable, faster to iterate with and often leads to an overall better development experience.</p><div class="pullquote"><p>The paved road should be easy to enter, lightweight by default, and <em>deliberately</em> unopinionated about what a service should look like.</p></div><p>Instead of starting from a large abstraction designed around an imagined average service, the base layer should provide only the essentials that are guaranteed to be common across all services: sensible defaults, security guardrails, lifecycle management, and a working deployment path. From there, teams can opt into additional capabilities as their service requires them.</p><p>Those capabilities should themselves be reusable and composable: databases, queues, deployment patterns, observability, workload-specific templates, and other infrastructure pieces that can be stacked together as needed rather than imposed upfront.</p><p>The result is closer to a set of interchangeable and reusable components than to a fixed application framework. Start with the smallest universally useful unit as a foundation, add the pieces that actually fit the workload, and retain the ability to build something different when the provided pieces do not.</p><h2>The repository as the lifecycle root</h2><p>In this model, engineers create services from repository templates.</p><p>A minimal template might be called something like <code>google-cloud-project</code>. Creating a repository from that template is enough to bootstrap the fundamental infrastructure needed by the project.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!X6rx!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!X6rx!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 424w, https://substackcdn.com/image/fetch/$s_!X6rx!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 848w, https://substackcdn.com/image/fetch/$s_!X6rx!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 1272w, https://substackcdn.com/image/fetch/$s_!X6rx!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!X6rx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png" width="1456" height="555" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/ce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:555,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:104118,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.joaopenteado.com/i/210176938?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!X6rx!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 424w, https://substackcdn.com/image/fetch/$s_!X6rx!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 848w, https://substackcdn.com/image/fetch/$s_!X6rx!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 1272w, https://substackcdn.com/image/fetch/$s_!X6rx!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fce817101-3429-4630-b09f-5147e3a3c8f9_1516x578.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>A GitHub Actions workflow in the template repository triggers a centrally maintained reusable bootstrap workflow when a new repository is created. </p><p>Detecting repository creation is slightly more awkward than it should be. There is a <code>create</code> event, but in my experience it does not fire reliably for repositories instantiated from a template. What has worked consistently is triggering on push and gating the bootstrap job on <code>github.event.created</code>, which the push payload sets to true for the initial push into a new repository. Organization-level webhooks or a GitHub App also work and are likely the more robust option at scale, at the cost of running infrastructure outside Actions.</p><p>That workflow has access to a tightly scoped bootstrap identity and establishes things such as:</p><ul><li><p>isolated Terraform state</p></li><li><p>project-level cloud resources</p></li><li><p>service identities</p></li><li><p>IAM bindings to federated workload identities and deployment permissions</p></li><li><p>the normal Terraform execution path</p></li><li><p>whatever minimum organizational guardrails are required (although these should preferably be inherited from the account or folder level whenever possible)</p></li></ul><p>After bootstrap, the repository manages its own infrastructure through ordinary Terraform.</p><p>The obvious objection is that this makes repository creation a privileged operation. If creating a repository provisions cloud projects and identities, what stops a repository from provisioning whatever it wants?</p><p>The answer is that the bootstrap identity is not bound to the repository. It is bound to the workflow. GitHub&#8217;s OIDC token includes a <code>job_workflow_ref</code> claim identifying the reusable workflow that produced it, and workload identity federation can condition on that claim rather than on repository name. The trust policy says, in effect: I will issue this identity only to tokens minted by this centrally maintained workflow, at this ref.</p><p>A newly created repository can therefore invoke the bootstrap and get exactly what the bootstrap grants. It cannot write its own workflow that assumes the same identity, because its own workflow produces a different <code>job_workflow_ref</code>. The privilege lives in code the application team does not control, invoked from a repository they do.</p><p>The important part is that each repository gets its own state and its own isolated security boundary. One experimental service does not share a Terraform state file with ten unrelated services. Its infrastructure can be modified, broken, recreated, or destroyed independently.</p><p>The repository effectively becomes the root object from which the service hangs. Admittedly, tearing down a service is somewhat less straightforward: reacting to repository deletion <em>requires</em> either a GitHub App or an organization-level webhook capable of receiving the repository deletion event.</p><h2>Infrastructure allocation as an arena</h2><p>The analogy I like is an arena allocator. In it, instead of managing the lifecycle of each object independently, many individual allocations are associated with a larger lifetime.</p><p>The same idea can be useful for cloud infrastructure.</p><p>A developer experimenting with a service should not need to maintain a mental inventory of every type of infrastructure provisioned along with their service: cloud resources, mailing lists, Slack channels, GitHub teams, etc.</p><p>Those resources belong to the service arena, which makes experimentation considerably less intimidating.</p><h2>Composable, rather than prescriptive</h2><p>The other part of the model that I think matters is composability.</p><p>The default template should be minimal, unopinionated and lightweight. It provides good defaults and establishes the lifecycle and security boundaries, but it does not have to become a bespoke internal application framework. </p><p>Higher-level templates can then build on top of it. For example, I might have a <code>cloud-run-service-go</code> template built on top of the basic Google Cloud project template. In addition to the normal project bootstrap, it includes:</p><ul><li><p>a small Hello World application written in Go</p></li><li><p>Terraform for the Cloud Run service</p></li><li><p>the resources required to build and deploy the application</p></li><li><p>a ready-to-use CI/CD pipeline</p></li></ul><p>Creating a repository from that template gives me a working Cloud Run service almost immediately. There is no separate infrastructure provisioning step, no Terraform backend setup, and no need to assemble a deployment pipeline before testing an idea.</p><p>But importantly, the template remains a starting point.</p><p>If I want a database or queue, I can compose with another centrally maintained Terraform module. Or, if an existing module does not fit my use case, I can define the Terraform resources directly. And if I do not want the higher-level Cloud Run template at all, I can still start from the bare-bones cloud project and build something entirely different.</p><p>The abstraction is therefore progressive:</p><p><strong>modules &#8594; minimal project template &#8594; workload-specific templates</strong></p><p>Each layer provides additional convenience, but none needs to hide or replace the layer below it.</p><p>There is a major difference between a platform that says, &#8220;Here is the easiest way to do this,&#8221; and one that says, &#8220;This is the only way you are allowed to do this.&#8221;</p><p>I strongly prefer the former.</p><h2>Centralized machinery, decentralized <em>(but clear)</em> ownership</h2><p>This pattern also creates what I think is a useful division of responsibility.</p><p>Platform or infrastructure teams can centrally maintain the pieces that benefit from standardization:</p><ul><li><p>bootstrap workflows</p></li><li><p>Terraform modules</p></li><li><p>identity configuration</p></li><li><p>security controls</p></li><li><p>CI/CD primitives</p></li><li><p>organization-wide defaults</p></li></ul><p>Application teams still own their repositories and can evolve their infrastructure independently.</p><div class="pullquote"><p>The goal should not be to completely eliminate infrastructure knowledge or operational work from application teams, but rather to eliminate infrastructure boilerplate and <em>ceremony</em>.</p></div><p>There is a meaningful difference between understanding that your Cloud Run service has an identity, a Terraform state, and a deployment pipeline, and having to manually construct all three every time you start a project.</p><h2>Security implications</h2><p>This model also changes where some of the most important security boundaries live, and it&#8217;s important to keep this in mind when adopting this type of platform architecture.</p><p>First, <strong>the ability to create a repository may now imply the ability to provision infrastructure</strong>. If repository creation can automatically trigger bootstrap workflows with privileged cloud identities, repository creation is no longer just a source-control permission. It becomes a general infrastructure operation.</p><p>That may be perfectly acceptable, but it should be treated deliberately. Organizations that need tighter control can place additional authorization checks in the GitHub App or bootstrap workflow, restrict which users or teams may create repositories from infrastructure-enabled templates, or host these repositories in a separate organization with more restrictive creation policies.</p><p>Second, <strong>branch protection becomes a core part of the infrastructure change management security model</strong>. Merge rights become deployment rights.</p><p>Once Terraform and deployment configuration live inside the repository, the ability to modify protected branches can effectively become the ability to modify the service&#8217;s infrastructure. In many cases, changing the Terraform means changing almost everything: identities, permissions, network configuration, databases, deployment targets, and the application itself.</p><p>Repository protections therefore become real access controls rather than merely development-process safeguards. Required reviews, <code>CODEOWNERS</code>, protected branches, restricted force pushes, and appropriate controls around automation credentials all matter considerably more under this model.</p><p>The same is true of the repository containing centralized workflows. Reusable workflows capable of assuming privileged identities form an important part of the organization&#8217;s software and infrastructure supply chain. Changes to those workflows should be treated accordingly: tightly controlled write access, protected branches, careful review, pinned dependencies where appropriate, and strict validation of any inputs that cross the trust boundary between an application repository and a privileged reusable workflow.</p><p>The convenience of repository-scoped infrastructure does not eliminate privileged operations. But it does move them into a smaller number of explicit and auditable control points, and those need to be protected as such.</p><h2>Why I like this model</h2><p>The main advantage, in my experience, is speed.</p><p>The cost of trying and iterating on an idea becomes very low. A developer can create a repository, immediately get an isolated and deployable environment, iterate normally, and discard the entire thing when the experiment is no longer useful.</p><p>At the same time, the organization keeps important guardrails around authentication, identity, state isolation, deployment, and resource ownership.</p><p>This is a combination that is often surprisingly hard to achieve. Fast developer environments sometimes become loosely governed collections of manually created cloud resources. Highly governed environments sometimes turn every new service into a platform request. Repository-scoped infrastructure provides a useful middle ground.</p><h2>Not every repository is a service</h2><p>This pattern should not be interpreted as saying that every repository must correspond to a service. Many repositories clearly do not: shared libraries, documentation, tooling, Terraform modules, reusable workflows, and monorepos are obvious examples.</p><p>The point I&#8217;m trying to make is that <strong>a repository is often a very good lifecycle and ownership boundary for an independently operated service</strong>.</p><p>For many internal services in particular, I have found little reason to introduce another abstraction between the repository and the workload if the repository model itself remains sufficiently flexible. A service can own its application code, infrastructure definition, deployment configuration, identities, and operational metadata in one place without requiring a separate platform-controlled object to represent it.</p><p>There will always be exceptions. Some systems contain several independently deployed services in one repository. Others span several repositories while still representing a single operational system. Shared infrastructure may have a lifecycle entirely independent from application code. A shared production database serving twenty applications should obviously not disappear because one repository was deleted.</p><p>However, <strong>coupling infrastructure lifecycles across service boundaries should be a deliberate decision, made with a clear ownership model and lifecycle plan.</strong> Personally, I would also try to avoid architectures where resources are grouped primarily by their provider (such as a particular cloud or SaaS platform) and their lifecycle is managed collectively. Whenever possible, I prefer resources to follow the lifecycle of the services that actually depend on them.</p><p>The core idea is not to force a one-to-one mapping between repositories and services everywhere. It is to recognize that, where the repository already represents the natural unit of ownership and change, it can also serve as the infrastructure lifecycle root.</p><h2>Cheap creation changes behavior</h2><p>Perhaps the most important effect of this pattern is not technical.</p><p>When services are expensive to create, developers naturally avoid creating them. They extend existing systems, share resources that should not really be shared, or postpone experiments because provisioning another environment feels like too much work.</p><p>But when a deployable service is roughly as cheap to create as a repository (and just as easy to discard), the calculus changes. Experiments can actually be experiments. Small services can have small overhead. And infrastructure can become something developers compose when needed rather than something they accumulate and eventually have to clean up.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.joaopenteado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[“Who has access?” is a trick question]]></title><description><![CDATA[And why most access reviews fail to answer it]]></description><link>https://www.joaopenteado.com/p/who-has-access-is-a-trick-question</link><guid isPermaLink="false">https://www.joaopenteado.com/p/who-has-access-is-a-trick-question</guid><dc:creator><![CDATA[João Penteado]]></dc:creator><pubDate>Tue, 14 Apr 2026 11:13:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/8d9a8e7d-b1d3-4cd6-8ca0-5f67b11bd9a4_1408x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every security team has been asked this question. An auditor walks in, a compliance framework demands it, an incident responder needs it urgently: <em>who has access to this resource?</em></p><p>It sounds like simple question any team should have an immediate answer to. </p><p>However, this question is underspecified in ways that most organizations never bother to articulate, and the result is that access reviews produce answers that range from incomplete to meaningless. Not because the teams doing them are necessarily incompetent, but because the industry lacks a shared understanding for what &#8220;access&#8221; actually means in practice.</p><h2>The problem with &#8220;access&#8221;</h2><p>Suppose you&#8217;re asked: who can edit DNS records in Cloud DNS on Google Cloud Platform?</p><p>Your first instinct might be to check who has the <code>dns.changes.create</code> permission on the DNS zone. You look at any predefined and custom IAM roles containing that permission, IAM bindings for that resource, find the employees with matching roles, and hand over a list.</p><p>Except that list is incomplete in ways you (or your auditor) haven&#8217;t scoped.</p><p>Maybe a user doesn&#8217;t have the permission directly, but they&#8217;re in a group that does. Maybe the binding isn&#8217;t on the zone itself, but on the project that contains it, or the folder above that project, or the organization above that folder. Maybe the user has no binding anywhere, but they can impersonate a service account that does. Maybe they can impersonate a service account that can impersonate <em>another</em> service account that has access through a group membership inherited from a folder three levels up. Maybe the user has no current access at all, but they possess the power to grant it to themselves at any time: a permission that only wakes up when they decide to edit an IAM policy or role somewhere else.</p><p>All of these are real and all of them answer &#8220;who has access&#8221; differently. However, most access reviews stop looking at the first or second layer.</p><p>The root issue is that we&#8217;re conflating fundamentally different things under a single word. To have a useful conversation about access, I believe it&#8217;s necessary to distinguish between three broad categories: <strong>effective access</strong>, <strong>assumable access</strong>, and <strong>escalatable access</strong>.</p><p>The examples throughout this article use Google Cloud Platform because that&#8217;s where I work the most daily, but nothing here is GCP-specific. AWS has an arguably more complex IAM system, with largely the same patterns involving IAM roles, SCPs, resource policies, and cross-account role assumption. Azure has management group inheritance, managed identities, and service principal impersonation. Hence, the underlying problem is structural: any system with hierarchical resource organization, group-based policy resolution, and delegated identity mechanisms will produce the same complexity.</p><h2>Effective access</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!BblO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!BblO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 424w, https://substackcdn.com/image/fetch/$s_!BblO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 848w, https://substackcdn.com/image/fetch/$s_!BblO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 1272w, https://substackcdn.com/image/fetch/$s_!BblO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!BblO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png" width="1240" height="906" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:906,&quot;width&quot;:1240,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:127265,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.joaopenteado.com/i/193027028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!BblO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 424w, https://substackcdn.com/image/fetch/$s_!BblO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 848w, https://substackcdn.com/image/fetch/$s_!BblO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 1272w, https://substackcdn.com/image/fetch/$s_!BblO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F71141d7c-5040-4344-9b07-ee99040ab06a_1240x906.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">All three users have effective access: different paths, same result.</figcaption></figure></div><p>Effective access is the set of permissions a principal can exercise <em>right now</em>, without performing any intermediate action. The principal navigates to the resource, performs the operation, and it works. No impersonation, no credential exchange, no role modification needed.</p><p>This is what most people picture when they hear &#8220;access,&#8221; but even this category is more complex than it first appears. The principal might hold the permission through any of several mechanisms:</p><p>The simplest is a direct binding: the user is bound to a role containing the permission on the resource itself. Then there&#8217;s group-mediated access: the user is a member of a group (or a complex nested chain of groups) that holds the binding. On top of this, there&#8217;s hierarchy inheritance: the binding exists on an ancestor resource, such as a project, folder, or organization, and propagates down.</p><p>These mechanisms are orthogonal and composable: a user might be a member of a group that is a member of another group that has a role binding on a folder, and that binding propagates to a project, which contains the resource. The result is an access path of length four or five, but the access is still <em>effective</em>, meaning the user can exercise the permission immediately, right now, without doing anything first.</p><p>The length of the access path matters, but not for whether the access exists. It matters for auditability. A direct binding on a resource is visible, easy to review and more likely to be intentional, while a nested group membership inherited through three layers of resource hierarchy is harder to discover, and more likely to be accidental or ungoverned. <strong>Access path length can be a useful proxy to determine how likely the access is unintentional.</strong></p><h2>Assumable access</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zg_N!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zg_N!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 424w, https://substackcdn.com/image/fetch/$s_!zg_N!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 848w, https://substackcdn.com/image/fetch/$s_!zg_N!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 1272w, https://substackcdn.com/image/fetch/$s_!zg_N!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zg_N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png" width="1456" height="1099" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1099,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:366562,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.joaopenteado.com/i/193027028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zg_N!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 424w, https://substackcdn.com/image/fetch/$s_!zg_N!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 848w, https://substackcdn.com/image/fetch/$s_!zg_N!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 1272w, https://substackcdn.com/image/fetch/$s_!zg_N!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F12ad2cdd-8dc7-41a7-8b2b-7424b024e2bf_2119x1600.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The principal needs to go through one or more intermediaries who have effective access. </figcaption></figure></div><p>Assumable access is when a principal does not have effective access to a resource, but can obtain it by assuming a different identity. The defining operation here is identity impersonation: the principal temporarily becomes another principal and inherits that principal&#8217;s effective access in whole or in part.</p><p>The most obvious example in GCP is service account impersonation. A user with the <code>iam.serviceAccounts.getAccessToken</code> permission on a service account can mint tokens as that service account. If the service account has effective access to the resource, the user can reach it through impersonation. And just like effective access, the service account&#8217;s path to the resource itself might be mediated through groups, inheritance, or both.</p><p>This creates two independent path dimensions. The <em>assumable path</em> is the chain of identity assumptions: how does the user reach the target identity? The <em>effective path</em> is the chain from that identity to the resource. The composition of both results in the total access path.</p><p>Service account impersonation can also chain. A user impersonates service account A, which can impersonate service account B, which has the role binding. Each hop adds a layer of indirection and, more often than not, a layer of unintentionality.</p><p>But impersonation through the IAM API isn&#8217;t the only way to assume an identity. Consider a user who can SSH into a VM, or deploy code to a Cloud Run service, or trigger a Cloud Build pipeline. Each of these workloads runs with an attached service account. If the user can execute arbitrary code in a workload&#8217;s context, they effectively have access to its credentials. This is identity assumption through compute access rather than through the IAM impersonation API, but the end result is the same: the user acts as the service account.</p><p>These paths are harder to enumerate because they cross the boundary between IAM analysis and infrastructure or application analysis. IAM tooling can tell you who can impersonate a service account. Understanding that a user can push code to a Cloud Run service which runs as that service account requires understanding the deployment pipeline, repository permissions, and CI/CD configuration. The access path exists, but because it lives in a different system it&#8217;s easy to overlook it.</p><h3>Where federated identities fit</h3><p>Identity federation adds another dimension to assumable access. In workload identity federation, an external identity, such as a GitHub Actions runner, an AWS role, or a CI/CD pipeline in another platform, can exchange its native credentials for short-lived GCP credentials. The federation configuration defines which external identities map to which GCP principals.</p><p>This means the question of &#8220;who has access&#8221; can extend outside your cloud boundary entirely. If a GitHub Actions workflow for a specific repository has federated access to a GCP service account, then anyone who can create or modify workflows in that repository effectively has assumable access to whatever that service account can reach. The user&#8217;s credential never touches GCP directly: they push a commit to a repository, a workflow runs, it federates into a GCP identity, and that principal has effective access to your resources.</p><p>Federated identity is particularly challenging for access reviews because the trust boundary is defined in configuration of the workload identity pool and in an external system&#8217;s permission model. You won&#8217;t find these paths by examining role bindings just in the resource&#8217;s platform alone. You need to trace the federation configuration, understand the external identity provider&#8217;s authorization model, and map backward from &#8220;who can assume this external identity&#8221; all the way to &#8220;which principals are able to grant or influence access along that path.&#8221;</p><h3>The AI agent identity problem</h3><p>This is about to get even more complicated. Cloud providers are introducing first-class identity types for AI agents, and these create entirely new assumable access paths.</p><p>Google Cloud&#8217;s Vertex AI Agent Engine now supports <a href="https://docs.cloud.google.com/agent-builder/agent-engine/agent-identity">agent identity</a> as an IAM principal type. Agents get their own principal identifiers in the format <code>principal://TRUST_DOMAIN/NAMESPACE/AGENT_NAME</code>, and you can bind IAM roles to these identities just like you would for users or service accounts. From an access review perspective, this means agents are principals that can hold effective access to resources.</p><p>Where they differ from traditional service accounts is in their constraints. Agent identities cannot be directly impersonated by another principal through the IAM API. There are also no downloadable keys to create, rotate, or leak. The credentials are cryptographically bound to the agent's runtime environment on Vertex AI through certificate-bound tokens and mTLS, which means stolen credentials are not replayable outside that context. In practice, they are more restricted service accounts: scoped to a specific workload, tied to its lifecycle, and stripped of the portability that makes service accounts both useful and dangerous. This matters for access analysis because the assumable access path to an agent's permissions doesn't run through impersonation or credential theft. It only runs through the ability to invoke the agent itself, which is a fundamentally different trust boundary.</p><p>The assumable access question then becomes: who can invoke the agent? If a user can send requests to an agent, and that agent has IAM permissions to perform actions on resources, the user has indirect access to those resources through the agent. The agent is an intermediary identity, similar in structure to a service account, but with a critical difference: the agent&#8217;s behavior is driven by a language model that interprets natural language instructions, not by deterministic code. The blast radius of &#8220;access through the agent&#8221; is harder to scope because it depends not just on what permissions the agent holds, but on what the agent can be instructed to do.</p><p>Agent identity also composes with everything else. An agent might use a service account for some operations, federate into external services for others, and hold direct IAM bindings on GCP resources. Mapping the full access surface of &#8220;user can talk to agent&#8221; requires tracing through all of these paths.</p><p>We are early in understanding what governance looks like for agent identities. But one thing is already clear: any access taxonomy that doesn&#8217;t account for non-human, non-deterministic intermediaries is very likely to age poorly.</p><h2>Escalatable access</h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!vBlh!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!vBlh!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 424w, https://substackcdn.com/image/fetch/$s_!vBlh!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 848w, https://substackcdn.com/image/fetch/$s_!vBlh!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 1272w, https://substackcdn.com/image/fetch/$s_!vBlh!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!vBlh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png" width="1456" height="793" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:793,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:293716,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.joaopenteado.com/i/193027028?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!vBlh!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 424w, https://substackcdn.com/image/fetch/$s_!vBlh!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 848w, https://substackcdn.com/image/fetch/$s_!vBlh!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 1272w, https://substackcdn.com/image/fetch/$s_!vBlh!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2d1fedbd-1057-474e-a6b8-67a24ddd0a85_2180x1188.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image buttonBase-GK1x3M"><svg aria-hidden="true" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg" class="icon-noB79L"><g><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image buttonBase-GK1x3M"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2 icon-noB79L"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">The principal can create access paths that don&#8217;t exist yet.</figcaption></figure></div><p>The third category is fundamentally different from the first two. Effective access and assumable access describe paths that <em>exist</em>. Escalatable access describes the ability to <em>create</em> paths that don&#8217;t exist yet.</p><p>A user with <code>resourcemanager.projects.setIamPolicy</code> on a project can add any role binding they want to that project. They might not have <code>dns.changes.create</code> right now, but they can grant it to themselves at any time. For example, a user who can impersonate a service account that has <code>setIamPolicy</code> on a folder can modify IAM bindings on that folder and everything below it. More subtly, a user might be able to modify their own roles: impersonate a service account that has the ability to edit the role definition or IAM policy that applies to the user&#8217;s own principal.</p><p>This is where I&#8217;d argue that access review becomes vulnerability analysis. The access path doesn&#8217;t exist yet. The user has to construct it. But if they <em>can</em> construct it, the access is effectively attainable from a security perspective.</p><p>Tools like <a href="https://github.com/nccgroup/pmapper">PMapper</a> exist specifically for this kind of analysis. They model IAM as a directed graph and traverse it to find escalation paths: edges that don&#8217;t represent current access, but the ability to create it. This is computationally expensive and, in the worst case, the graph of potential escalation paths grows combinatorially.</p><p>For most access review purposes, escalatable access is where you draw the line and say &#8220;this is a different exercise.&#8221; Certifying effective access is a governance activity. Reviewing assumable access is a more sophisticated governance activity. Finding and closing escalatable access paths is a security engineering problem. The vocabulary matters because it lets you be explicit about which problem you&#8217;re solving and where the boundary is.</p><p>The one exception is privileged access management. PAM and JIT access systems are, by design, sanctioned escalation: a user requests elevated permissions, approval is granted (by a human or a policy), and temporary bindings are created. This is escalatable access that&#8217;s been formalized into a workflow with controls around it. When an auditor asks about access, whether PAM grants count depends entirely on whether they&#8217;re asking about &#8220;access right now&#8221; or &#8220;access that could exist.&#8221;</p><h2>In conclusion</h2><p>Without shared vocabulary, every access review is a negotiation. The auditor asks &#8220;who has access,&#8221; the security team counters with &#8220;what do you mean by access,&#8221; and both parties waste time arriving at an <em>ad hoc</em> definition that won&#8217;t be consistent with the next review or third-party&#8217;s interpretation.</p><p>With a taxonomy, you can scope the question precisely. &#8220;Show me all principals with effective access to production DNS zones&#8221; is tractable and well-defined. &#8220;Show me all principals with assumable access, including compute-mediated impersonation&#8221; is harder but still scoped. &#8220;Show me all escalation paths to DNS write access&#8221; is a different project entirely.</p><p>This vocabulary also gives you a framework for evaluating tooling. Most cloud IAM analysis tools, including the built-in policy analyzers, primarily cover effective access. Some are specifically designed for escalatable access. Almost none handle compute-mediated assumable access or federated identity paths. Knowing which category you&#8217;re trying to cover tells you which tools are fit for purpose and where you have gaps.</p><p>More importantly, it lets you be honest about what you don&#8217;t know. Most organizations think they govern effective access reasonably well, some might even partially cover assumable access through service account reviews, but very few touch escalatable access. That is defensible, as long as it is known, understood, and stated explicitly. Issues arise when partial coverage is treated as a complete picture of access.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.joaopenteado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Stop asking your team for updates]]></title><description><![CDATA[On removing the interrogation loop from engineering check-ins]]></description><link>https://www.joaopenteado.com/p/stop-asking-your-team-for-updates</link><guid isPermaLink="false">https://www.joaopenteado.com/p/stop-asking-your-team-for-updates</guid><dc:creator><![CDATA[João Penteado]]></dc:creator><pubDate>Thu, 19 Mar 2026 17:47:46 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/3118d0ea-0f2c-4b14-91f7-42d6bca26a82_1408x768.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Most standups are a polite form of interrogation.</p><p>You ask what someone is working on. They tell you mostly things you already knew. You then ask if there are blockers. They say no, or mention one, and it gets noted somewhere and the meeting moves on. This happens every morning, or three times a week, or whatever cadence the team has settled on. Everyone is present, but almost no one is really listening.</p><p>The problem isn&#8217;t the frequency or the length. It&#8217;s the structure. It puts the team on the hook for producing information that, in many cases, everyone in the room already has, or that no one in the room actually needs at that moment.</p><p>The worst version of this is when someone has to repeatedly announce that nothing has happened on a six-month project that was never going to move this week. That&#8217;s not information. That&#8217;s management theater at best and a slow tax on morale at worst. But the ritual demands it, so someone performs it.</p><h3>The setup</h3><p>I&#8217;ve been running my security team&#8217;s check-ins differently for the past year or so, and I want to describe the part that I think matters most.</p><p>We meet on Mondays, Wednesdays, and Fridays. Wednesdays and Fridays are short syncs, usually under fifteen minutes. Monday is the main meeting and runs for about an hour. </p><p>The gap between meetings is intentional: enough time for things to actually move, not so long that anything slips through.</p><p>The Monday structure is straightforward: I open with announcements, urgent or unplanned work, kudos where they&#8217;re due, and what&#8217;s top of mind for me on both the security and business side. Then each person goes through what they&#8217;re working on, what they need help with, what they&#8217;re stuck on.</p><p>None of that is unusual. The part that&#8217;s different happens roughly every two weeks, at the end of one of those Monday meetings, after everyone has had their turn.</p><p>I go through every project on our pipeline and I state my understanding of its current status. Not as a question. As a statement. I say what I think is happening, what I think the blockers are, what I think the next steps are. The team corrects me where I&#8217;m wrong. Mostly, I&#8217;m right.</p><p>We also run a monthly retrospective focused on team health and process improvements, but it&#8217;s separate from this mechanism.</p><h3>Shifting the burden</h3><p>The reason it works, I think, comes down to who carries the tracking burden.</p><p>In a traditional standup, that burden sits with the team. They&#8217;re responsible for surfacing progress, flagging issues, and producing updates on demand. The manager is the receiver. The implicit contract is: I ask, you report, and I follow up if something seems off.</p><p>This model inverts the contract. I absorb whatever surfaces naturally in normal conversation or check-ins over the course of two weeks. I don&#8217;t prompt for project-level status. <strong>I don&#8217;t ask &#8220;where are we on X?&#8221; in sync meetings, ever.</strong> Long-running work that hasn&#8217;t moved doesn&#8217;t need to be announced as unmoved. It surfaces when there&#8217;s something worth saying about it.</p><p>Then, at the biweekly recap, I carry the synthesis myself. I&#8217;m the one who has to demonstrate that I&#8217;ve been paying attention. If my understanding is wrong or outdated, that&#8217;s on me to correct, not on someone else to have proactively flagged.</p><p>The effect on the team is real, even if it&#8217;s subtle. Nobody has to say &#8220;no progress this week&#8221; on a project they haven&#8217;t touched, because I&#8217;m the one stating the status. The pressure to produce the update disappears. What remains is just the conversation. The team can instead focus exclusively on producing high-value signals.</p><p>There&#8217;s a flattening effect too. When the manager is the one producing the update rather than extracting it, the dynamic stops feeling like a reporting line and starts feeling more like a team that shares context.</p><h3>The catch</h3><p>There&#8217;s a prerequisite worth being honest about: <strong>this only works if the manager is actually engaged.</strong> The biweekly recap isn&#8217;t a technique for reducing your own involvement. It&#8217;s a forcing function for maintaining it. You have to be absorbing what people say in normal conversation accurately enough to reconstruct the state of the entire project pipeline every two weeks. If you&#8217;re not doing that, the recap falls apart immediately and you&#8217;ve just created a new awkward meeting format.</p><p>I would argue that engagement is the job anyway. Knowing the current state of your team&#8217;s work, understanding why things are moving or not, and being able to communicate that clearly to stakeholders is not a bonus feature of good management. It&#8217;s the baseline. This mechanic just makes it visible and creates a regular moment where you have to prove it to yourself and your team.</p><h3>The upstream benefit</h3><p>One more thing worth naming: it helps me upstream as much as it helps the team. I&#8217;m regularly synthesizing project status in a form that&#8217;s ready to communicate to stakeholders. The biweekly recap isn&#8217;t just a check-in tool. It&#8217;s also how I keep my own model current and accurate enough to answer questions quickly when someone above me asks.</p><p>I&#8217;m not claiming this is novel or scientifically validated. My sample size is one and every team is different. Other people have almost certainly arrived at versions of this and built better systems around it. But it has worked well enough that the mechanism is worth describing: Don&#8217;t ask. Absorb. Carry. Read it back<strong>.</strong> Let your team correct you.</p><p>The correction is the update. <strong>The absence of interrogation is the point.</strong></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.joaopenteado.com/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Thanks for reading! Subscribe for free to receive new posts and support my work.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item></channel></rss>