<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Azure Archives - Linuxcent</title>
	<atom:link href="https://linuxcent.com/tag/azure/feed/" rel="self" type="application/rss+xml" />
	<link>https://linuxcent.com/tag/azure/</link>
	<description>Infrastructure security, from the kernel up.</description>
	<lastBuildDate>Mon, 27 Jul 2026 11:57:05 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=7.0.4</generator>

<image>
	<url>https://linuxcent.com/wp-content/uploads/2026/04/favicon-512x512-1-150x150.png</url>
	<title>Azure Archives - Linuxcent</title>
	<link>https://linuxcent.com/tag/azure/</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">211632295</site>	<item>
		<title>One Blueprint, Six Clouds — Multi-Provider OS Image Builds</title>
		<link>https://linuxcent.com/linux-hardening-multi-cloud/</link>
					<comments>https://linuxcent.com/linux-hardening-multi-cloud/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Sun, 26 Apr 2026 19:41:36 +0000</pubDate>
				<category><![CDATA[OS Image Builder]]></category>
		<category><![CDATA[AWS]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[BakeX]]></category>
		<category><![CDATA[DevSecOps]]></category>
		<category><![CDATA[GCP]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Multi-Cloud]]></category>
		<category><![CDATA[OS Hardening]]></category>
		<guid isPermaLink="false">https://linuxcent.com/linux-hardening-multi-cloud/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span>One HardeningBlueprint YAML, six cloud providers: AWS, GCP, Azure, DigitalOcean, Linode, Proxmox. How Stratum handles provider differences so your compliance intent stays portable.</p>
<p>The post <a href="https://linuxcent.com/linux-hardening-multi-cloud/">One Blueprint, Six Clouds — Multi-Provider OS Image Builds</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 7</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><em>OS Hardening as Code, Episode 3</em><br />
<em><a href="https://linuxcent.com/cloud-ami-security-risks-custom-os-images/">Cloud AMI Security Risks</a> · <a href="https://linuxcent.com/linux-hardening-as-code/">Linux Hardening as Code</a> · </em><em>Multi-Cloud OS Hardening</em>**</p>
<blockquote>
<p><strong>Note:</strong> the tool in this series was released as <strong>Stratum</strong> and renamed to <strong>BakeX</strong> at<br />
v0.6.0 — same project, same license, same team. Commands below use the current <code class="" data-line="">bakex</code><br />
CLI. If you arrived here looking for <code class="" data-line="">stratum</code> or <code class="" data-line="">pip install stratumoss</code>, you&#8217;re in the<br />
right place: <a href="https://github.com/invicton/bakex">github.com/invicton/bakex</a>.</p>
</blockquote>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Multi-cloud OS hardening with separate scripts per provider means three scripts that drift within weeks</li>
<li>A HardeningBlueprint YAML separates compliance intent (portable) from provider details (handled by BakeX&#8217;s provider layer)</li>
<li>You keep one blueprint file per provider — and every section except <code class="" data-line="">target</code> stays byte-identical across all six. The diff below proves it</li>
<li>Provider-specific differences — disk names, cloud-init ordering, base image identifiers — are abstracted away from the blueprint author</li>
<li>The compliance posture becomes reviewable in a pull request: a control change touches six files identically, and a reviewer can see that at a glance</li>
<li>Six providers ship as working blueprints today: AWS, GCP, Azure, DigitalOcean, Linode, Proxmox</li>
</ul>
<hr />
<h2 id="the-problem-three-clouds-three-scripts-three-ways-to-drift">The Problem: Three Clouds, Three Scripts, Three Ways to Drift</h2>
<pre><code class="" data-line="">AWS hardening script          GCP hardening script          Azure hardening script
├── /dev/xvd* disk refs       ├── /dev/sda* disk refs       ├── /dev/sda* disk refs
├── 169.254.169.254 IMDS      ├── 169.254.169.254 IMDS      ├── 169.254.169.254 IMDS
├── cloud-init order A        ├── cloud-init order B        ├── cloud-init order C
└── Updated: Jan 2025         └── Updated: Aug 2024         └── Updated: Mar 2024
                                         │
                                         └─ 5 months behind
                                            on CIS updates
</code></pre>
<p>Multi-cloud OS hardening starts as a copy-paste of the AWS script. Within a month, the clouds diverge.</p>
<p>EP02 showed that a HardeningBlueprint YAML eliminates the skip-at-2am problem by making hardening a build artifact. What it assumed — quietly — is that you&#8217;re building for one provider. The moment you expand to a second cloud, the provider-specific details in the blueprint become a problem: disk names differ, cloud-init fires in a different order, and AWS-specific assumptions break silently on GCP.</p>
<hr />
<p>We expanded from AWS to GCP six months ago. The EC2 hardening script had been working reliably for over a year. The GCP engineer took the AWS script, made some quick changes, and started building images.</p>
<p>The first GCP images had a subtle problem: the <code class="" data-line="">/tmp</code> and <code class="" data-line="">/home</code> separate partition entries in <code class="" data-line="">/etc/fstab</code> referenced <code class="" data-line="">/dev/xvdb</code> — an AWS disk naming convention. GCP uses <code class="" data-line="">/dev/sdb</code>. The fstab entries were silently ignored. The mounts existed but weren&#8217;t restricted. The CIS controls for separate filesystem partitions were listed as passing in the scan output because the Ansible task had &#8220;run successfully&#8221; — it just hadn&#8217;t done what we thought.</p>
<p>It took a pentest three months later to catch it. The finding: six production GCP instances with <code class="" data-line="">/tmp</code> not mounted with <code class="" data-line="">noexec, nosuid, nodev</code> — despite our &#8220;CIS L1 hardened&#8221; label.</p>
<p>The root cause wasn&#8217;t the engineer. It was a hardening approach that required cloud-specific knowledge embedded in the script rather than in a provider abstraction layer.</p>
<hr />
<h2 id="how-bakex-separates-compliance-intent-from-provider-details">How BakeX Separates Compliance Intent from Provider Details</h2>
<p>Multi-cloud OS hardening works when the compliance intent and the provider details are kept strictly separate.</p>
<pre><code class="" data-line="">HardeningBlueprint YAML
(compliance intent — portable)
         │
         ▼
  BakeX Provider Layer
  ┌─────────────────────────────────────────────┐
  │  AWS         │  GCP         │  Azure        │
  │  /dev/xvd*   │  /dev/sda*   │  /dev/sda*    │
  │  IMDS v2     │  GCP IMDS    │  Azure IMDS   │
  │  cloud-init  │  cloud-init  │  waagent       │
  │  order A     │  order B     │  order C       │
  └─────────────────────────────────────────────┘
         │
         ▼
  Ansible-Lockdown + Provider-Aware Configuration
         │
         ▼
  OpenSCAP Scan
         │
         ▼
  Golden Image (AMI / GCP Image / Azure Image)
</code></pre>
<p>The blueprint author declares <strong>what</strong> should be true about the OS. BakeX&#8217;s provider layer handles <strong>how</strong> that&#8217;s achieved on each cloud.</p>
<p>The disk naming, cloud-init sequencing, metadata endpoint configuration, and provider-specific package repositories are all abstracted into the provider layer. They never appear in the blueprint file.</p>
<hr />
<h2 id="the-same-blueprint-across-six-providers">The Same Blueprint Across Six Providers</h2>
<p>Here is the part people expect to be a flag, and isn&#8217;t. There is no <code class="" data-line="">--provider</code> switch. The<br />
provider is a field <em>inside</em> the blueprint, so you keep one file per target:</p>
<pre><code class="" data-line="">$ ls blueprints/ubuntu/22.04/
cis-l1-aws.yaml           cis-l1-digitalocean.yaml  cis-l1-linode.yaml
cis-l1-azure.yaml         cis-l1-gcp.yaml           cis-l1-proxmox.yaml

# Validate all six at once — offline, no cloud API calls
$ bakex validate blueprints/ubuntu/22.04/*.yaml
OK    blueprints/ubuntu/22.04/cis-l1-aws.yaml  (HardeningBlueprint &#039;ubuntu22-cis-l1-aws&#039;)
...
6/6 blueprint(s) valid.

# Build one
$ bakex build blueprints/ubuntu/22.04/cis-l1-gcp.yaml
Building &#039;ubuntu22-cis-l1-gcp&#039; (gcp) → job 7f3c9e82-…
</code></pre>
<p>That design choice looks like more files, and it is. What you get for it is that a blueprint<br />
is completely self-describing: the file names its own cloud and its own base image, so it<br />
builds the same way on your laptop, in CI, and on a colleague&#8217;s machine with no flags to<br />
forget and no environment to match.</p>
<p><strong>The claim worth testing: how much actually differs between those six files?</strong></p>
<p>I parsed all six and compared every section except <code class="" data-line="">metadata</code> and <code class="" data-line="">target</code>:</p>
<pre><code class="" data-line="">compliance    identical across all 6
controls      identical across all 6   (same rules, same enable state)
filesystem    identical across all 6
users         identical across all 6
system        identical across all 6
</code></pre>
<p>Only the <code class="" data-line="">target</code> block changes, and it changes in exactly the way you&#8217;d expect:</p>
<table>
<thead>
<tr>
<th>Provider</th>
<th><code class="" data-line="">instance_type</code></th>
<th><code class="" data-line="">base_image</code></th>
</tr>
</thead>
<tbody>
<tr>
<td>aws</td>
<td><code class="" data-line="">t3.medium</code></td>
<td><code class="" data-line="">ami-0c7217cdde317cfec</code></td>
</tr>
<tr>
<td>gcp</td>
<td><code class="" data-line="">e2-medium</code></td>
<td><code class="" data-line="">projects/ubuntu-os-cloud/global/images/family/ubuntu-2204-lts</code></td>
</tr>
<tr>
<td>azure</td>
<td><code class="" data-line="">Standard_B2s</code></td>
<td><code class="" data-line="">Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest</code></td>
</tr>
<tr>
<td>digitalocean</td>
<td><code class="" data-line="">s-2vcpu-4gb</code></td>
<td><code class="" data-line="">ubuntu-22-04-x64</code></td>
</tr>
<tr>
<td>linode</td>
<td><code class="" data-line="">g6-standard-2</code></td>
<td><code class="" data-line="">linode/ubuntu22.04</code></td>
</tr>
<tr>
<td>proxmox</td>
<td><code class="" data-line="">2c-4g</code></td>
<td><code class="" data-line="">9000</code> (VE template VMID)</td>
</tr>
</tbody>
</table>
<p>Six wildly different ways of naming &#8220;Ubuntu 22.04 LTS&#8221; and sizing a 2 vCPU / 4 GB box. That<br />
is the entire provider-specific surface. The CIS benchmark, the profile, the datastream, the<br />
mount options, the locked root account, the control overrides and their justifications — all<br />
byte-identical.</p>
<p>One honest caveat, because I checked rather than assumed: the six files are <em>semantically</em><br />
identical but not textually so. The justification string on one disabled SELinux rule is<br />
worded slightly differently between files — same rule, same <code class="" data-line="">enabled: false</code>, same meaning,<br />
different prose. It&#8217;s a cosmetic inconsistency in the shipped library, not a behavioural one,<br />
and it&#8217;s the kind of thing you only find by diffing rather than trusting the header comment.</p>
<p>If you change the compliance posture — add a control override, tighten a mount option — you<br />
change it identically in six files and rebuild. A reviewer sees six identical hunks in the<br />
diff. A sixth hunk that looks different is a bug, and it&#8217;s visible in code review rather than<br />
three months later in a pentest.</p>
<hr />
<h2 id="what-the-provider-layer-handles">What the Provider Layer Handles</h2>
<p>The provider layer is where the cloud-specific knowledge lives, so the blueprint author doesn&#8217;t have to carry it:</p>
<p><strong>Disk naming:</strong></p>
<table>
<thead>
<tr>
<th>Provider</th>
<th>OS disk</th>
<th>Ephemeral</th>
<th>Data</th>
</tr>
</thead>
<tbody>
<tr>
<td>AWS</td>
<td><code class="" data-line="">/dev/xvda</code></td>
<td><code class="" data-line="">/dev/xvdb</code></td>
<td><code class="" data-line="">/dev/xvdc+</code></td>
</tr>
<tr>
<td>GCP</td>
<td><code class="" data-line="">/dev/sda</code></td>
<td>—</td>
<td><code class="" data-line="">/dev/sdb+</code></td>
</tr>
<tr>
<td>Azure</td>
<td><code class="" data-line="">/dev/sda</code></td>
<td><code class="" data-line="">/dev/sdb</code> (temp disk)</td>
<td><code class="" data-line="">/dev/sdc+</code></td>
</tr>
<tr>
<td>DigitalOcean</td>
<td><code class="" data-line="">/dev/vda</code></td>
<td>—</td>
<td><code class="" data-line="">/dev/vdb+</code></td>
</tr>
</tbody>
</table>
<p>The CIS controls for separate <code class="" data-line="">/tmp</code> and <code class="" data-line="">/home</code> partitions reference disk paths that differ across these providers. The provider layer translates the blueprint&#8217;s <code class="" data-line="">filesystem.tmp</code> declaration into the correct fstab entries for the target cloud.</p>
<p><strong>Cloud-init ordering:</strong></p>
<p>Different providers initialize services in different orders. On AWS, the network is available before cloud-init runs most tasks. On GCP, some network configuration happens after cloud-init starts. On Azure, the waagent handles some configuration that cloud-init handles elsewhere.</p>
<p>The provider layer sequences the hardening steps to run in the correct order for each provider — specifically, it waits for network availability before applying network-level hardening, and ensures the package manager is configured before running Ansible roles that require package installation.</p>
<p><strong>Metadata endpoint configuration:</strong></p>
<p>CIS controls include restrictions on access to the instance metadata service (IMDSv2 enforcement on AWS, equivalent controls on GCP/Azure). The provider layer applies the correct restriction for each cloud — the blueprint just declares <code class="" data-line="">compliance: benchmark: cis-l1</code>.</p>
<hr />
<h2 id="building-every-provider">Building Every Provider</h2>
<p>There is no built-in fan-out flag, and honestly none is needed — the CLI is exit-code shaped,<br />
so the shell already does this well:</p>
<pre><code class="" data-line=""># Validate everything first; stop before spending money if anything is wrong
bakex validate blueprints/ubuntu/22.04/*.yaml || exit 1

# Then build each target
for bp in blueprints/ubuntu/22.04/cis-l1-*.yaml; do
  bakex build &quot;$bp&quot; --json &gt; &quot;builds/$(basename &quot;$bp&quot; .yaml).json&quot; &amp;
done
wait
</code></pre>
<p><code class="" data-line="">--json</code> emits the job record — id, profile name, provider, status, artifact ID, error — which<br />
is what you want when six builds are writing to six files at once. Every build either lands a<br />
<code class="" data-line="">complete</code> status with an artifact ID, or a <code class="" data-line="">failed</code> status with the reason. Nothing produces<br />
a half-hardened image.</p>
<p>The validate-then-build ordering matters more than it looks. Validation is offline and takes<br />
milliseconds; a build takes 15–25 minutes and costs money. Catching a malformed blueprint or an<br />
unsupported OS/provider pair in the first step means you never launch the instance.</p>
<hr />
<h2 id="blueprint-versioning-and-drift">Blueprint Versioning and Drift</h2>
<p>Version-controlling the blueprint file solves a problem multi-cloud environments hit<br />
consistently: knowing what your OS security posture was six months ago. The blueprint is the<br />
answer — it&#8217;s a file, in git, with a commit history and a reviewer&#8217;s name on every change.</p>
<p>Re-scanning a <em>running</em> instance against the posture that built it is a separate job, and it<br />
does not live in the CLI. <code class="" data-line="">bakex validate</code> and <code class="" data-line="">bakex build</code> are the two CLI verbs; scanning<br />
and drift comparison are in the web UI and the HTTP API, where the scan results have somewhere<br />
to live. EP04 covers that surface in detail — the A–F grade, the SARIF export, and comparing a<br />
current scan against a stored baseline.</p>
<p>The useful discipline in the meantime is unglamorous: rebuild from the blueprint rather than<br />
patching running instances. An instance that drifted is a symptom; the blueprint is the cure,<br />
and re-baking is cheaper than reconciling.</p>
<hr />
<h2 id="production-gotchas">Production Gotchas</h2>
<p><strong>Provider-specific CIS controls exist.</strong> CIS AWS Foundations Benchmark and CIS GCP Benchmark include cloud-specific controls (VPC flow logs, CloudTrail, etc.) that are separate from the OS-level CIS controls. The blueprint handles OS-level controls. Cloud-level controls (IAM, logging, network configuration) belong in your cloud security posture management tooling.</p>
<p><strong>Build costs vary by provider.</strong> On AWS, the build instance is a <code class="" data-line="">t3.medium</code> for 15–20 minutes (~$0.02). On GCP and Azure, equivalent pricing applies. For multi-provider builds, run them in regions close to your primary workloads to minimize image transfer time.</p>
<p><strong>Proxmox is a template VMID, not an image name.</strong> Every cloud provider names its base image with a string; Proxmox names it with a number — the VE template&#8217;s VMID (<code class="" data-line="">9000</code> in the shipped blueprint). The provider talks to the Proxmox API remotely via <code class="" data-line="">proxmoxer</code>, so no agent on the host is required, but it does need <code class="" data-line="">host</code> credentials and it discovers the built VM&#8217;s IP through the QEMU guest agent <em>inside</em> the VM. If the guest agent isn&#8217;t installed in your template, the build will provision and then hang waiting for an address.</p>
<p><strong>KVM and Proxmox are deliberately different providers.</strong> They look interchangeable and aren&#8217;t: a KVM target names a downloadable cloud image, a Proxmox target names a VE template that already exists on your host. Don&#8217;t assume a blueprint written for one works on the other.</p>
<p><strong>GCP image sharing across projects requires explicit IAM.</strong> GCP machine images aren&#8217;t automatically available to other projects in the organization. BakeX builds the image; sharing it is a GCP IAM operation you configure at the project or organization level — there&#8217;s no BakeX command that grants cross-project access for you.</p>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Multi-cloud OS hardening with separate scripts per provider creates inevitable drift; a provider-abstracted blueprint eliminates it</li>
<li>BakeX ships working blueprints for AWS, GCP, Azure, DigitalOcean, Linode, and Proxmox — one file per provider, with <code class="" data-line="">target</code> as the only section that differs</li>
<li>The provider is a field in the blueprint, not a CLI flag: every file is self-describing and builds identically in CI, locally, or on a teammate&#8217;s machine</li>
<li>Fan-out is a shell loop over exit codes, not a framework feature — validate all six offline first, then build in parallel with <code class="" data-line="">--json</code></li>
<li>Blueprint version control is the single source of truth for OS security posture history — and a compliance change that isn&#8217;t identical across all six providers shows up as an odd hunk in code review</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>Six providers, one compliance posture, and a diff that proves it. EP03 showed that the multi-cloud drift problem disappears when provider details are confined to a single block of the blueprint.</p>
<p>What neither EP02 nor EP03 answered is the auditor&#8217;s question: how do you know the image is actually compliant? &#8220;We ran CIS L1&#8221; is not an answer. &#8220;Grade A, 98/100 controls, SARIF export attached&#8221; is.</p>
<p>EP04 covers automated OpenSCAP compliance: the post-build scan in detail — how the A-F grade is calculated, what controls block an A grade, how SARIF exports work, and how drift detection catches what changed after deployment.</p>
<p><em>Next: <a href="/automated-compliance-scanning-openscap/">automated OpenSCAP compliance — CIS benchmark grading before deployment</a></em></p>
<p>Get EP04 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe">linuxcent.com/subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&amp;linkname=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Flinux-hardening-multi-cloud%2F&#038;title=One%20Blueprint%2C%20Six%20Clouds%20%E2%80%94%20Multi-Provider%20OS%20Image%20Builds" data-a2a-url="https://linuxcent.com/linux-hardening-multi-cloud/" data-a2a-title="One Blueprint, Six Clouds — Multi-Provider OS Image Builds"></a></p><p>The post <a href="https://linuxcent.com/linux-hardening-multi-cloud/">One Blueprint, Six Clouds — Multi-Provider OS Image Builds</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/linux-hardening-multi-cloud/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1774</post-id>	</item>
		<item>
		<title>Azure RBAC Explained: Management Groups, Subscriptions, and Scope</title>
		<link>https://linuxcent.com/azure-rbac-entra-id-guide/</link>
					<comments>https://linuxcent.com/azure-rbac-entra-id-guide/#respond</comments>
		
		<dc:creator><![CDATA[Vamshi Krishna Santhapuri]]></dc:creator>
		<pubDate>Thu, 16 Apr 2026 17:44:58 +0000</pubDate>
				<category><![CDATA[Cloud IAM]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Azure Active Directory]]></category>
		<category><![CDATA[Azure RBAC]]></category>
		<category><![CDATA[Cloud Security]]></category>
		<category><![CDATA[Entra ID]]></category>
		<category><![CDATA[IAM]]></category>
		<category><![CDATA[Managed Identity]]></category>
		<category><![CDATA[PIM]]></category>
		<guid isPermaLink="false">https://linuxcent.com/azure-rbac-entra-id-guide/</guid>

					<description><![CDATA[<p><span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span>Azure RBAC and Entra ID deep dive: role definitions, assignments, managed identities, Privileged Identity Management, and federated credentials for workloads.</p>
<p>The post <a href="https://linuxcent.com/azure-rbac-entra-id-guide/">Azure RBAC Explained: Management Groups, Subscriptions, and Scope</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></description>
										<content:encoded><![CDATA[<span class="span-reading-time rt-reading-time" style="display: block;"><span class="rt-label rt-prefix">Reading Time: </span> <span class="rt-time"> 11</span> <span class="rt-label rt-postfix">minutes</span></span><style>
pre{position:relative;background:#1e1e1e;color:#d4d4d4;
    padding:16px 16px 16px 20px;border-radius:6px;overflow-x:auto;
    font-family:'JetBrains Mono','Fira Code','Cascadia Code',Consolas,'Courier New',monospace;
    font-size:.88em;line-height:1.6;border-left:4px solid #555}
code{background:#f4f4f4;padding:2px 5px;border-radius:3px;font-size:.9em}
pre code{background:transparent;padding:0;color:inherit}
pre[data-lang="bash"],pre[data-lang="sh"],
pre[data-lang="shell"],pre[data-lang="zsh"]{border-left-color:#4ec9b0}
pre[data-lang="yaml"],pre[data-lang="json"],
pre[data-lang="toml"],pre[data-lang="xml"]{border-left-color:#569cd6}
pre[data-lang="python"],pre[data-lang="go"],pre[data-lang="rust"],
pre[data-lang="java"],pre[data-lang="c"],pre[data-lang="cpp"]{border-left-color:#c586c0}
pre[data-lang="text"],pre[data-lang="output"],
pre[data-lang="console"]{border-left-color:#888}
.lc-copy-btn{position:absolute;top:8px;right:8px;background:#2d2d2d;color:#ccc;
    border:1px solid #444;border-radius:4px;padding:3px 9px;font-size:.75em;
    font-family:system-ui,sans-serif;cursor:pointer;opacity:0;
    transition:opacity .15s,background .15s;line-height:1.6}
pre:hover .lc-copy-btn{opacity:1}
.lc-copy-btn:hover{background:#3a3a3a;color:#fff}
.lc-copy-btn.copied{color:#4ec9b0;border-color:#4ec9b0}
.lc-lang-badge{position:absolute;top:8px;left:20px;font-family:system-ui,sans-serif;
    font-size:.7em;color:#666;text-transform:uppercase;letter-spacing:.04em;
    line-height:1;pointer-events:none;opacity:0;transition:opacity .15s}
pre:hover .lc-lang-badge{opacity:1}
table{border-collapse:collapse;width:100%;margin:16px 0}
th,td{border:1px solid #ddd;padding:10px 14px;text-align:left}
th{background:#f0f0f0;font-weight:600}
tr:nth-child(even){background:#fafafa}
</style>
<p><script>
(function(){
  if(window.__lcCodeEnhanced)return;
  window.__lcCodeEnhanced=true;
  function enhance(){
    document.querySelectorAll('pre').forEach(function(pre){
      var code=pre.querySelector('code');
      var lang='';
      if(code){var m=(code.className||'').match(/language-(\S+)/);if(m)lang=m[1].toLowerCase();}
      if(lang)pre.setAttribute('data-lang',lang);
      if(lang){var badge=document.createElement('span');badge.className='lc-lang-badge';badge.textContent=lang;pre.insertBefore(badge,pre.firstChild);}
      var btn=document.createElement('button');
      btn.className='lc-copy-btn';btn.textContent='Copy';btn.setAttribute('aria-label','Copy code to clipboard');
      pre.appendChild(btn);
      btn.addEventListener('click',function(){
        var text=code?code.innerText:pre.innerText;
        if(navigator.clipboard&&window.isSecureContext){
          navigator.clipboard.writeText(text).then(function(){ok(btn);}).catch(function(){fb(text,btn);});
        }else{fb(text,btn);}
      });
    });
  }
  function ok(btn){btn.textContent='Copied!';btn.classList.add('copied');setTimeout(function(){btn.textContent='Copy';btn.classList.remove('copied');},2000);}
  function fb(text,btn){
    try{var ta=document.createElement('textarea');ta.value=text;ta.style.cssText='position:fixed;left:-9999px;top:-9999px;opacity:0';document.body.appendChild(ta);ta.select();document.execCommand('copy');document.body.removeChild(ta);ok(btn);}
    catch(e){btn.textContent='✗ Failed';setTimeout(function(){btn.textContent='Copy';},2000);}
  }
  if(document.readyState==='loading'){document.addEventListener('DOMContentLoaded',enhance);}else{enhance();}
})();
</script></p>
<p><a href="/what-is-cloud-iam/">What Is Cloud IAM</a> → <a href="/authentication-vs-authorization-iam/">Authentication vs Authorization</a> → <a href="/iam-roles-policies-permissions-explained/">IAM Roles vs Policies</a> → <a href="/aws-iam-deep-dive/">AWS IAM Deep Dive</a> → <a href="/gcp-iam-deep-dive/">GCP Resource Hierarchy IAM</a> → <strong>Azure RBAC Scopes</strong></p>
<hr />
<h2 id="tldr">TL;DR</h2>
<ul>
<li>Entra ID and Azure RBAC are two separate authorization planes — Entra ID roles control the identity system; RBAC roles control Azure resources. Global Administrator doesn&#8217;t grant VM access.</li>
<li>Azure RBAC role assignments inherit downward through the hierarchy: Management Group → Subscription → Resource Group → Resource</li>
<li>Use managed identities for all Azure-hosted workloads — system-assigned for one-to-one resource binding, user-assigned for shared access across multiple resources</li>
<li><code class="" data-line="">Contributor</code> is the right role for most service identities — full resource management without the ability to modify RBAC assignments</li>
<li>The <code class="" data-line="">Actions</code> vs <code class="" data-line="">DataActions</code> split means you can audit management access and data access independently — an incomplete audit checks only one</li>
<li>PIM (Privileged Identity Management) should govern all Entra ID privileged roles — nobody should permanently hold Global Admin or Subscription Owner</li>
</ul>
<hr />
<h2 id="the-big-picture">The Big Picture</h2>
<pre><code class="" data-line="">         Azure: Two Separate Authorization Planes
─────────────────────────────────────────────────────────
  Entra ID (Identity Plane)      Azure RBAC (Resource Plane)
  ─────────────────────────      ───────────────────────────
  Controls:                      Controls:
  · Users, groups, apps          · Azure resources
  · Tenant settings              · Management groups
  · App registrations            · Subscriptions
  · Conditional access           · Resource groups
                                 · Individual resources

  Roles (examples):              Scope hierarchy:
  · Global Administrator         Management Group
  · User Administrator             └─ Subscription
  · Security Reader                     └─ Resource Group
  · Application Administrator                └─ Resource

  Scope: tenant-wide             Role assignment at any level
                                 inherits down to all nodes below

  Both planes use Entra ID identities.
  Authorization in each plane is completely independent.
  Global Admin ≠ Subscription Owner.
</code></pre>
<p>Azure RBAC scopes determine how far a role assignment reaches — and the blast radius of a misconfiguration scales directly with how high in the hierarchy it sits.</p>
<hr />
<h2 id="introduction">Introduction</h2>
<p>Azure RBAC scopes define where a role assignment applies and everything it inherits. A role at the Management Group level touches every subscription, every resource group, and every resource across your entire Azure estate. A role at the resource level touches only that resource. Understanding scope before making any assignment is the difference between &#8220;access for this storage account&#8221; and &#8220;access for your entire org.&#8221;</p>
<p>When I first worked seriously in Azure environments, I had a mental model carried over from Active Directory administration. Users, groups, directory roles — I knew how that worked. I assumed Azure&#8217;s IAM would be an extension of the same system, just with cloud resources bolted on.</p>
<p>That assumption got me into trouble within the first week.</p>
<p>I was trying to understand why an engineer had Global Administrator access in Entra ID but couldn&#8217;t see the resources in a Subscription. In Active Directory terms, if you&#8217;re a Domain Admin, you can see everything. In Azure, it doesn&#8217;t work that way.</p>
<p>Entra ID roles and Azure RBAC roles are <strong>two different systems</strong>. Global Administrator is an Entra ID role — it controls who can manage the identity plane: create users, manage app registrations, configure tenant settings. It has nothing to do with Azure resources like virtual machines, storage accounts, or Kubernetes clusters. Those are governed by Azure RBAC, which is an entirely separate authorization system.</p>
<p>I spent two hours trying to understand why a Global Admin couldn&#8217;t list VMs before someone explained this. I&#8217;m putting it at the top of this episode so you don&#8217;t lose those two hours.</p>
<hr />
<h2 id="entra-id-vs-azure-rbac-the-two-separate-planes">Entra ID vs Azure RBAC — The Two Separate Planes</h2>
<table>
<thead>
<tr>
<th></th>
<th>Entra ID</th>
<th>Azure RBAC</th>
</tr>
</thead>
<tbody>
<tr>
<td>Controls access to</td>
<td>Entra ID itself — users, groups, apps, tenant settings</td>
<td>Azure resources — VMs, storage, databases, subscriptions</td>
</tr>
<tr>
<td>Role types</td>
<td>Entra ID directory roles</td>
<td>Azure resource roles</td>
</tr>
<tr>
<td>Example roles</td>
<td>Global Admin, User Admin, Security Reader</td>
<td>Owner, Contributor, Storage Blob Data Reader</td>
</tr>
<tr>
<td>Scope</td>
<td>Tenant-wide</td>
<td>Management group → Subscription → Resource Group → Resource</td>
</tr>
<tr>
<td>Managed via</td>
<td>Entra ID admin center</td>
<td>Azure portal / ARM / Azure CLI</td>
</tr>
</tbody>
</table>
<p>A user can be Global Administrator — the highest Entra ID role — and have zero access to Azure resources unless explicitly assigned an Azure RBAC role. And vice versa: a user with Subscription Owner (highest Azure RBAC role) has no ability to manage Entra ID user accounts without an Entra ID role assignment.</p>
<p>These are not the same system. They&#8217;re connected — both use Entra ID identities as principals — but authorization in each plane is independent.</p>
<hr />
<h2 id="the-azure-resource-hierarchy">The Azure Resource Hierarchy</h2>
<p>Azure RBAC role assignments can be made at any level of the resource hierarchy, and they inherit downward:</p>
<pre><code class="" data-line="">Tenant (Entra ID)
  └── Management Group  (policy and RBAC inheritance across subscriptions)
        └── Management Group  (nested, up to 6 levels)
              └── Subscription  (billing and resource boundary)
                    └── Resource Group  (logical container for resources)
                          └── Resource  (VM, storage account, key vault, AKS cluster...)
</code></pre>
<p>A role assigned at the Subscription level applies to every resource group and resource in that subscription. A role at the Management Group level applies to every subscription beneath it.</p>
<p>The blast radius of a misconfiguration scales with how high in the hierarchy it sits. Subscription Owner at the subscription level is contained to that subscription. Management Group Contributor at the root management group touches your entire Azure estate.</p>
<pre><code class="" data-line=""># View management group hierarchy
az account management-group list --output table

# List subscriptions
az account list --output table

# View all role assignments at a scope — start here in any audit
az role assignment list \
  --scope /subscriptions/SUB_ID \
  --include-inherited \
  --output table
</code></pre>
<hr />
<h2 id="principal-types-in-azure-rbac">Principal Types in Azure RBAC</h2>
<table>
<thead>
<tr>
<th>Type</th>
<th>What It Is</th>
<th>Best For</th>
</tr>
</thead>
<tbody>
<tr>
<td>User</td>
<td>Entra ID user account</td>
<td>Human access</td>
</tr>
<tr>
<td>Group</td>
<td>Entra ID security group</td>
<td>Team-based access</td>
</tr>
<tr>
<td>Service Principal</td>
<td>App registration with credentials (secret or cert)</td>
<td>External systems, apps with their own identity</td>
</tr>
<tr>
<td>Managed Identity</td>
<td>Credential-less identity for Azure-hosted workloads</td>
<td>Everything running in Azure</td>
</tr>
</tbody>
</table>
<h3 id="managed-identities-the-right-model-for-workloads">Managed Identities — The Right Model for Workloads</h3>
<p>Managed identities are Azure&#8217;s answer to AWS instance profiles and GCP service accounts attached to compute. Azure manages the entire credential lifecycle — tokens are issued automatically, there&#8217;s nothing to create, rotate, or revoke manually.</p>
<p><strong>System-assigned managed identity</strong> is tied to a specific Azure resource. When the resource is deleted, the identity is deleted. One-to-one, no sharing.</p>
<pre><code class="" data-line=""># Enable system-assigned managed identity on a VM
az vm identity assign \
  --name my-vm \
  --resource-group rg-prod

# Get the principal ID (needed to assign RBAC roles to it)
az vm show \
  --name my-vm \
  --resource-group rg-prod \
  --query identity.principalId \
  --output tsv
</code></pre>
<p><strong>User-assigned managed identity</strong> is a standalone resource that can be attached to multiple Azure resources and persists independently. This is the right model when multiple services need the same access — instead of assigning the same RBAC roles to ten separate system-assigned identities, you create one user-assigned identity, grant it the roles, and attach it to all ten resources.</p>
<pre><code class="" data-line=""># Create a user-assigned managed identity
az identity create \
  --name app-backend-identity \
  --resource-group rg-identities

# Get its identifiers
az identity show \
  --name app-backend-identity \
  --resource-group rg-identities \
  --query &#039;{principalId:principalId, clientId:clientId}&#039;

# Attach to a VM
az vm identity assign \
  --name my-vm \
  --resource-group rg-prod \
  --identities /subscriptions/SUB/resourceGroups/rg-identities/providers/Microsoft.ManagedIdentity/userAssignedIdentities/app-backend-identity
</code></pre>
<p>Code running inside an Azure VM or App Service with a managed identity gets tokens via IMDS, with no credential management required:</p>
<pre><code class="" data-line="">from azure.identity import DefaultAzureCredential
from azure.storage.blob import BlobServiceClient

# DefaultAzureCredential automatically picks up the managed identity in Azure
credential = DefaultAzureCredential()
client = BlobServiceClient(
    account_url=&quot;https://myaccount.blob.core.windows.net&quot;,
    credential=credential
)
</code></pre>
<p>The <code class="" data-line="">DefaultAzureCredential</code> chain: managed identity → environment variables → workload identity → Visual Studio / VS Code authentication → Azure CLI. In Azure-hosted services, the managed identity path is used automatically. In local development, it falls through to the developer&#8217;s <code class="" data-line="">az login</code> session.</p>
<hr />
<h2 id="azure-role-definitions-understanding-actions-vs-dataactions">Azure Role Definitions — Understanding Actions vs DataActions</h2>
<p>A role definition specifies what actions it grants. Azure distinguishes two planes:</p>
<ul>
<li><strong>Actions:</strong> Control plane — managing the resource itself (create, delete, configure)</li>
<li><strong>DataActions:</strong> Data plane — accessing data within the resource (read blob contents, get secrets)</li>
<li><strong>NotActions / NotDataActions:</strong> Exceptions carved out from the grant</li>
</ul>
<pre><code class="" data-line="">{
  &quot;Name&quot;: &quot;Storage Blob Data Reader&quot;,
  &quot;IsCustom&quot;: false,
  &quot;Actions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/read&quot;,
    &quot;Microsoft.Storage/storageAccounts/blobServices/generateUserDelegationKey/action&quot;
  ],
  &quot;NotActions&quot;: [],
  &quot;DataActions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read&quot;
  ],
  &quot;NotDataActions&quot;: [],
  &quot;AssignableScopes&quot;: [&quot;/&quot;]
}
</code></pre>
<p>The control/data plane split matters in audits. An identity with <code class="" data-line="">Microsoft.Storage/storageAccounts/read</code> (an Action) can see the storage account exists and view its properties. To actually read blob contents, it needs the DataAction <code class="" data-line="">Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read</code>. These are separate grants. In an access audit, checking only Actions and missing DataActions is an incomplete picture.</p>
<h3 id="built-in-roles-worth-understanding">Built-in Roles Worth Understanding</h3>
<table>
<thead>
<tr>
<th>Role</th>
<th>Scope</th>
<th>What It Grants</th>
</tr>
</thead>
<tbody>
<tr>
<td>Owner</td>
<td>Any</td>
<td>Full access + can manage RBAC assignments — the highest trust role</td>
</tr>
<tr>
<td>Contributor</td>
<td>Any</td>
<td>Full resource management, but cannot manage RBAC</td>
</tr>
<tr>
<td>Reader</td>
<td>Any</td>
<td>Read-only on all resources</td>
</tr>
<tr>
<td>User Access Administrator</td>
<td>Any</td>
<td>Can manage RBAC assignments, no resource access</td>
</tr>
<tr>
<td>Storage Blob Data Contributor</td>
<td>Storage</td>
<td>Read/write/delete blob data</td>
</tr>
<tr>
<td>Storage Blob Data Reader</td>
<td>Storage</td>
<td>Read blob data only</td>
</tr>
<tr>
<td>Key Vault Secrets Officer</td>
<td>Key Vault</td>
<td>Manage secrets, not keys or certificates</td>
</tr>
<tr>
<td>AcrPush / AcrPull</td>
<td>Container Registry</td>
<td>Push or pull images</td>
</tr>
</tbody>
</table>
<p>The gap between <code class="" data-line="">Owner</code> and <code class="" data-line="">Contributor</code> is important: <code class="" data-line="">Contributor</code> can do everything to a resource except manage who has access to it. This is the right role for most service identities and automation — they need to manage resources, not manage permissions. If a compromised Contributor identity can&#8217;t modify RBAC assignments, it can&#8217;t grant itself or an attacker additional access.</p>
<p><code class="" data-line="">Owner</code> should be granted to people, not service identities, and only at the narrowest scope necessary.</p>
<h3 id="custom-roles">Custom Roles</h3>
<pre><code class="" data-line="">cat &gt; custom-app-storage.json &lt;&lt; &#039;EOF&#039;
{
  &quot;Name&quot;: &quot;App Storage Blob Reader&quot;,
  &quot;IsCustom&quot;: true,
  &quot;Description&quot;: &quot;Read app blobs only — no container management, no key operations&quot;,
  &quot;Actions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/read&quot;
  ],
  &quot;DataActions&quot;: [
    &quot;Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read&quot;
  ],
  &quot;NotActions&quot;: [],
  &quot;NotDataActions&quot;: [],
  &quot;AssignableScopes&quot;: [&quot;/subscriptions/SUB_ID&quot;]
}
EOF

az role definition create --role-definition custom-app-storage.json

# Assign it — specifically to this storage account
az role assignment create \
  --assignee-object-id &quot;$(az identity show --name app-backend-identity -g rg-identities --query principalId -o tsv)&quot; \
  --assignee-principal-type ServicePrincipal \
  --role &quot;App Storage Blob Reader&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/appstore
</code></pre>
<hr />
<h2 id="role-assignments-where-access-is-actually-granted">Role Assignments — Where Access Is Actually Granted</h2>
<p>The assignment brings everything together: principal + role + scope. This is the actual grant.</p>
<pre><code class="" data-line=""># Assign to a user (less common — prefer group assignments)
az role assignment create \
  --assignee alice@company.com \
  --role &quot;Storage Blob Data Reader&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/prodstore

# Assign to a group (better — one assignment, maintained via group membership)
GROUP_ID=$(az ad group show --group &quot;Backend-Team&quot; --query id -o tsv)
az role assignment create \
  --assignee-object-id &quot;$GROUP_ID&quot; \
  --assignee-principal-type Group \
  --role &quot;Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-dev

# Assign to a managed identity
MI_PRINCIPAL=$(az identity show --name app-backend-identity --resource-group rg-identities --query principalId -o tsv)
az role assignment create \
  --assignee-object-id &quot;$MI_PRINCIPAL&quot; \
  --assignee-principal-type ServicePrincipal \
  --role &quot;Storage Blob Data Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/appstore

# Audit all assignments at and below a scope (including inherited)
az role assignment list \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod \
  --include-inherited \
  --output table
</code></pre>
<p>Group-based assignments are the right model for humans at scale. When an engineer joins the Backend team, they join the Entra ID group. Their access follows. When they leave, you remove them from the group or disable their account. You never need to hunt down individual role assignments.</p>
<hr />
<h2 id="entra-id-roles-the-other-layer">Entra ID Roles — The Other Layer</h2>
<p>Entra ID roles control the identity infrastructure itself. These are distinct from Azure RBAC roles and deserve separate treatment:</p>
<table>
<thead>
<tr>
<th>Role</th>
<th>What It Controls</th>
</tr>
</thead>
<tbody>
<tr>
<td>Global Administrator</td>
<td>Everything in the tenant — highest privilege</td>
</tr>
<tr>
<td>Privileged Role Administrator</td>
<td>Assign and remove Entra ID roles</td>
</tr>
<tr>
<td>User Administrator</td>
<td>Create and manage users and groups</td>
</tr>
<tr>
<td>Application Administrator</td>
<td>Register and manage app registrations</td>
</tr>
<tr>
<td>Security Administrator</td>
<td>Manage security features and read reports</td>
</tr>
<tr>
<td>Security Reader</td>
<td>Read-only on security features</td>
</tr>
</tbody>
</table>
<p>Global Administrator in Entra ID is one of the most powerful identities in a Microsoft environment. It can modify any user, any app registration, any conditional access policy. Combined with the fact that Entra ID is also the identity provider for Microsoft 365, a Global Admin compromise can extend far beyond Azure resources into email, Teams, SharePoint — the entire Microsoft 365 estate.</p>
<p>Nobody should hold Global Administrator as a permanent assignment. This is where Privileged Identity Management (PIM) matters.</p>
<h3 id="privileged-identity-management-just-in-time-elevated-access">Privileged Identity Management — Just-in-Time Elevated Access</h3>
<p>PIM is Azure&#8217;s answer to the problem of permanent privileged role assignments. Instead of permanently holding Global Admin or Subscription Owner, users are made <em>eligible</em> for these roles. When they need elevated access, they activate it with a justification (and optionally an approval and MFA requirement). The access is time-limited — typically 8 hours — and automatically expires.</p>
<pre><code class="" data-line=""># List roles where the user is eligible (not permanently assigned)
az rest --method GET \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules&quot; \
  --query &quot;value[?principalId==&#039;USER_OBJECT_ID&#039;]&quot;

# A user activates an eligible role (calls this themselves when needed)
az rest --method POST \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignmentScheduleRequests&quot; \
  --body &#039;{
    &quot;action&quot;: &quot;selfActivate&quot;,
    &quot;principalId&quot;: &quot;USER_OBJECT_ID&quot;,
    &quot;roleDefinitionId&quot;: &quot;ROLE_DEF_ID&quot;,
    &quot;directoryScopeId&quot;: &quot;/&quot;,
    &quot;justification&quot;: &quot;Investigating security alert in tenant audit logs&quot;,
    &quot;scheduleInfo&quot;: {
      &quot;startDateTime&quot;: &quot;2026-04-16T00:00:00Z&quot;,
      &quot;expiration&quot;: { &quot;type&quot;: &quot;AfterDuration&quot;, &quot;duration&quot;: &quot;PT8H&quot; }
    }
  }&#039;
</code></pre>
<p>PIM is the right model for any role that could be used to escalate privileges: Global Administrator, Subscription Owner, Privileged Role Administrator, User Access Administrator. Nobody should have these permanently assigned unless there&#8217;s a strong operational reason — and even then, the assignment should be reviewed quarterly.</p>
<p>In one Azure environment I audited, I found 11 permanent Global Administrator assignments. The team thought this was normal because they&#8217;d all been made admins when the tenant was set up two years earlier and nobody had revisited it. Of the 11, three were former employees whose Entra ID accounts had been disabled — but the Global Admin role assignment was still there. Disabled users can&#8217;t use their accounts, but this is not a pattern you want to rely on.</p>
<hr />
<h2 id="federated-identity-for-external-workloads">Federated Identity for External Workloads</h2>
<p>For GitHub Actions, Kubernetes workloads, and other external systems that need to call Azure APIs, federated credentials eliminate service principal secrets:</p>
<pre><code class="" data-line=""># Create an app registration
APP_ID=$(az ad app create --display-name &quot;github-actions-deploy&quot; --query appId -o tsv)
SP_ID=$(az ad sp create --id &quot;$APP_ID&quot; --query id -o tsv)

# Add a federated credential for a specific GitHub repo and branch
az ad app federated-credential create \
  --id &quot;$APP_ID&quot; \
  --parameters &#039;{
    &quot;name&quot;: &quot;github-main-branch&quot;,
    &quot;issuer&quot;: &quot;https://token.actions.githubusercontent.com&quot;,
    &quot;subject&quot;: &quot;repo:my-org/my-repo:ref:refs/heads/main&quot;,
    &quot;audiences&quot;: [&quot;api://AzureADTokenExchange&quot;]
  }&#039;

# Grant the service principal an RBAC role
az role assignment create \
  --assignee-object-id &quot;$SP_ID&quot; \
  --role &quot;Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod
</code></pre>
<p>GitHub Actions — no secrets stored in GitHub:</p>
<pre><code class="" data-line="">jobs:
  deploy:
    permissions:
      id-token: write   # required for OIDC token request
    steps:
      - uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - run: az storage blob upload --account-name prodstore ...
</code></pre>
<p>The <code class="" data-line="">client-id</code>, <code class="" data-line="">tenant-id</code>, and <code class="" data-line="">subscription-id</code> values are not secrets — they&#8217;re identifiers. The actual authentication is the OIDC JWT from GitHub, verified against GitHub&#8217;s public keys, subject-matched against the configured condition (<code class="" data-line="">repo:my-org/my-repo:ref:refs/heads/main</code>). If the repo or branch doesn&#8217;t match, the token exchange fails. If it matches, a short-lived Azure token is issued.</p>
<hr />
<h2 id="production-gotchas"><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/26a0.png" alt="⚠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Production Gotchas</h2>
<p><strong>Global Admin ≠ Azure resource access</strong><br />
This trips up every team migrating from on-prem AD. Entra ID roles and Azure RBAC roles are independent systems. A Global Admin with no RBAC assignments cannot list VMs. Don&#8217;t assume directory privilege translates to resource access.</p>
<p><strong>Permanent Global Admin assignments are a standing breach risk</strong><br />
In the environment I audited: 11 permanent Global Admins, three of them disabled accounts. Disabled accounts can&#8217;t authenticate, but relying on that is not a security control. PIM eligible assignments + regular access reviews is the right answer.</p>
<p><strong><code class="" data-line="">Owner</code> on service identities lets compromised workloads modify RBAC</strong><br />
If a managed identity or service principal holds Owner, a compromised workload can grant additional permissions to itself or an attacker. Use <code class="" data-line="">Contributor</code> for workloads — full resource management, no RBAC modification.</p>
<p><strong>Checking only <code class="" data-line="">Actions</code> misses data-plane access</strong><br />
An audit that enumerates role <code class="" data-line="">Actions</code> and ignores <code class="" data-line="">DataActions</code> will miss identities with read access to blob contents, Key Vault secrets, or database records. Both planes need to be in scope.</p>
<p><strong>System-assigned identity is deleted with the resource</strong><br />
If you delete and recreate a VM using a system-assigned identity, the new identity is different. Any RBAC assignments made to the old identity are gone. User-assigned identities persist independently — use them for workloads where the resource lifecycle is separate from the identity lifecycle.</p>
<hr />
<h2 id="quick-reference">Quick Reference</h2>
<pre><code class="" data-line=""># Audit all role assignments at a subscription (including inherited)
az role assignment list \
  --scope /subscriptions/SUB_ID \
  --include-inherited \
  --output table

# Find all Owner assignments at subscription scope
az role assignment list \
  --scope /subscriptions/SUB_ID \
  --role Owner \
  --output table

# Get principal ID of a VM&#039;s managed identity
az vm show \
  --name my-vm \
  --resource-group rg-prod \
  --query identity.principalId \
  --output tsv

# View role definition — check Actions AND DataActions
az role definition list --name &quot;Storage Blob Data Reader&quot; --output json \
  | jq &#039;.[0] | {Actions: .permissions[0].actions, DataActions: .permissions[0].dataActions}&#039;

# List management group hierarchy
az account management-group list --output table

# Create user-assigned managed identity
az identity create --name app-identity --resource-group rg-identities

# Assign role to managed identity at resource scope
az role assignment create \
  --assignee-object-id &quot;$(az identity show -n app-identity -g rg-identities --query principalId -o tsv)&quot; \
  --assignee-principal-type ServicePrincipal \
  --role &quot;Storage Blob Data Contributor&quot; \
  --scope /subscriptions/SUB_ID/resourceGroups/rg-prod/providers/Microsoft.Storage/storageAccounts/mystore

# Check PIM eligible roles for a user
az rest --method GET \
  --uri &quot;https://graph.microsoft.com/v1.0/roleManagement/directory/roleEligibilitySchedules&quot; \
  --query &quot;value[?principalId==&#039;USER_OBJECT_ID&#039;].{role:roleDefinitionId,scope:directoryScopeId}&quot;
</code></pre>
<hr />
<h2 id="framework-alignment">Framework Alignment</h2>
<table>
<thead>
<tr>
<th>Framework</th>
<th>Reference</th>
<th>What It Covers Here</th>
</tr>
</thead>
<tbody>
<tr>
<td>CISSP</td>
<td>Domain 5 — Identity and Access Management</td>
<td>Azure&#8217;s directory-centric model; managed identities and PIM are the primary IAM constructs</td>
</tr>
<tr>
<td>CISSP</td>
<td>Domain 3 — Security Architecture</td>
<td>Entra ID spans Azure, M365, and third-party SaaS — scope boundaries determine the blast radius of a compromise</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.15 Access control</td>
<td>Azure RBAC role definitions and assignments implement access control policy</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.16 Identity management</td>
<td>Entra ID is the identity management platform — user lifecycle, group management, application registrations</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>8.2 Privileged access rights</td>
<td>PIM (Privileged Identity Management) directly implements JIT controls for privileged roles</td>
</tr>
<tr>
<td>ISO 27001:2022</td>
<td>5.18 Access rights</td>
<td>Role assignment scoping, managed identity provisioning, federated credential lifecycle</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.1</td>
<td>Managed identities and RBAC are the primary technical controls for CC6.1 in Azure-hosted environments</td>
</tr>
<tr>
<td>SOC 2</td>
<td>CC6.3</td>
<td>PIM activation expiry and access reviews directly satisfy time-bound access removal requirements</td>
</tr>
</tbody>
</table>
<hr />
<h2 id="key-takeaways">Key Takeaways</h2>
<ul>
<li>Entra ID and Azure RBAC are separate authorization planes — Entra ID roles control the identity system; RBAC roles control Azure resources. Global Administrator doesn&#8217;t grant VM access.</li>
<li>Use managed identities for all Azure-hosted workloads — system-assigned for one-to-one, user-assigned for shared identities across multiple resources</li>
<li><code class="" data-line="">Contributor</code> is the right role for most service identities — full resource management without RBAC modification ability</li>
<li>The control/data plane split (<code class="" data-line="">Actions</code> vs <code class="" data-line="">DataActions</code>) in role definitions means you can grant management access without data access or vice versa — use this</li>
<li>PIM should govern all Entra ID privileged roles and high-scope Azure roles — nobody should permanently hold Global Admin or Subscription Owner</li>
<li>Federated identity credentials replace service principal secrets for external workloads — no secrets stored in CI/CD systems</li>
</ul>
<hr />
<h2 id="whats-next">What&#8217;s Next</h2>
<p>EP07 goes cross-cloud: workload identity federation — the shift away from static credentials entirely, with IRSA for EKS, GKE Workload Identity, AKS workload identity, and GitHub Actions-to-all-three-clouds patterns.</p>
<p><em>Next: <a href="/workload-identity-oidc-service-accounts/">OIDC Workload Identity</a> — Eliminate Cloud Access Keys Entirely.</em></p>
<p>Get EP07 in your inbox when it publishes → <a href="https://linuxcent.com/subscribe/">subscribe</a></p>
<p><a class="a2a_button_mastodon" href="https://www.addtoany.com/add_to/mastodon?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Mastodon" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_reddit" href="https://www.addtoany.com/add_to/reddit?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Reddit" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_x" href="https://www.addtoany.com/add_to/x?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="X" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_copy_link" href="https://www.addtoany.com/add_to/copy_link?linkurl=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&amp;linkname=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" title="Copy Link" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share#url=https%3A%2F%2Flinuxcent.com%2Fazure-rbac-entra-id-guide%2F&#038;title=Azure%20RBAC%20Explained%3A%20Management%20Groups%2C%20Subscriptions%2C%20and%20Scope" data-a2a-url="https://linuxcent.com/azure-rbac-entra-id-guide/" data-a2a-title="Azure RBAC Explained: Management Groups, Subscriptions, and Scope"></a></p><p>The post <a href="https://linuxcent.com/azure-rbac-entra-id-guide/">Azure RBAC Explained: Management Groups, Subscriptions, and Scope</a> appeared first on <a href="https://linuxcent.com">Linuxcent</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://linuxcent.com/azure-rbac-entra-id-guide/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">1479</post-id>	</item>
	</channel>
</rss>

<!--
Performance optimized by W3 Total Cache. Learn more: https://www.boldgrid.com/w3-total-cache/?utm_source=w3tc&utm_medium=footer_comment&utm_campaign=free_plugin

Page Caching using Disk: Enhanced 

Served from: linuxcent.com @ 2026-08-22 12:17:53 by W3 Total Cache
-->