I spent the last few months building the Azure and Microsoft 365 foundation for a large enterprise. The shape of the work is the useful part anyway.
The brief was not "set up Azure." The brief was "build the thing every future project deploys from." Multiple countries, multiple Azure regions, hundreds of applications, acquisitions on the roadmap, regulated workloads, and a security team that answers to auditors twice a year. One environment they can reproduce instead of rebuilding by hand each time a business unit asks for a subscription.
Enterprise Azure work tends to fail the same way. Someone enables every service, wires it together through the portal, and two years later nobody can explain why a storage account in Singapore accepts traffic from the internet. I wanted the opposite. Every service gets a decision, the decision gets written down, and the whole platform lives in Git.
This is the plain version of what went into it.
Phase zero: assume nothing is empty
The first thing I did was look, read-only, at what already existed.
Nobody hands you a clean tenant. This one had a tenant from a 2019 Office 365 rollout, four subscriptions from three different teams, a Conditional Access policy somebody wrote in a hurry during Covid, an on-prem AD with Connect sync, and an old certificate authority that two applications still depend on.
The inventory covered tenants, domains, licenses, admin role assignments, subscriptions, management groups, existing policy assignments, RBAC, resources by region, IP ranges in use, ExpressRoute circuits, DNS zones, Intune enrollment, Defender coverage, and what the security team was already paying for from other vendors.
That last one matters. If a company already owns a third-party endpoint agent with two years left on the contract, a design that assumes Defender for Endpoint everywhere is a design that gets rejected in a budget meeting.
Rule I stuck to: never overwrite an existing control without documenting what breaks.
Start with Landing Zones, not a resource group
Microsoft publishes a reference architecture for this called Azure Landing Zones. It splits the tenant into a management group hierarchy: one branch for platform services like networking, identity, and logging, another for the workloads business teams build.
The reason it matters is boring and important. Policy and access inherit downward. Set a rule at the right level once and every subscription underneath picks it up. Set it in the wrong place and you're clicking through 200 subscriptions in six months.
The hierarchy separates platform subscriptions from workload subscriptions, then splits workloads into corporate-connected, internet-facing, non-production, and sandbox. Sandbox rules are looser on purpose. People need somewhere to break things that isn't production.
Subscription vending
The part the business notices.
A team wants a new environment. Instead of a three-week ticket chain, they submit a short request with owner, cost center, environment type, data classification, and region. Automation returns a subscription that already has:
management group placement
Entra groups and RBAC assignments
a network spoke with an IP range allocated from central IPAM
DNS wired to the private resolver
Azure Policy applied by inheritance
Defender plans enabled
diagnostic settings pointed at the central Log Analytics workspace
a budget with alerts
required tags
a pipeline that can deploy into it
Nobody has to remember the security steps because nobody performs the security steps.
Identity is the perimeter now
Entra ID does the work the firewall used to do.
The design covers custom domains, MFA for all users, phishing-resistant sign-in with FIDO2 keys and passkeys for anyone privileged, and Conditional Access as the enforcement point. Legacy authentication is blocked. Standing Global Administrator assignments are gone.
Admins hold separate privileged accounts and request elevation through PIM with approval and an expiry. Two break-glass accounts sit outside all of it: cloud-only, excluded from Conditional Access, monitored with an alert that pages someone if either signs in. Microsoft recommends exactly two, and the reason gets obvious the first time a Conditional Access policy locks a tenant out at 2am.
Applications use managed identities and workload identity federation instead of a client secret pasted into a pipeline variable. Joiner, mover, and leaver runs through lifecycle workflows, so access disappears when people do. Access reviews run on a schedule. Guest accounts expire.
Graph API permissions get reviewed too. An app registration with Directory.ReadWrite.All that nobody owns is a bigger risk than most of the firewall rules people argue about.
Endpoints
Intune manages Windows, macOS, iOS, Android, and the Linux machines that qualify. Autopilot for Windows provisioning, Apple Automated Device Enrollment for Macs and iPhones, Android Enterprise for the rest. Corporate devices get full management. BYOD gets app protection policies and nothing else, which keeps the legal team calm.
The link back to Conditional Access is what makes it worth the effort. A device reports its compliance state to Intune. Conditional Access reads that signal before granting access to email or the Azure portal. An unpatched laptop with encryption switched off doesn't get to open SharePoint.
Underneath: BitLocker and FileVault with keys escrowed, Defender for Endpoint, attack surface reduction rules, Windows LAPS, local admin rights removed and replaced with Endpoint Privilege Management for the few tasks that need elevation, and update rings that stage releases so a bad patch hits a few hundred pilot machines instead of the whole fleet.
Developers and executives get their own rings. Developers break things. Executives get phished.
Certificates, Wi-Fi, and VPN profiles push from Intune against the existing PKI, which is the part that took the longest because the old CA had no documented issuance policy.
Networking
The first real argument was Azure Virtual WAN versus traditional hub and spoke. Both are documented Microsoft patterns and both are defensible. Virtual WAN gives you a managed hub and easier branch onboarding at scale. Hub and spoke gives you more control over routing and, for some estates, a lower bill.
I wrote it up as a decision record with requirements, options, cost, and operational burden, then picked one. Anyone who joins in 2027 and wonders why can read the page.
The rest: a central IP address plan so two acquisitions don't arrive with overlapping 10.0.0.0/16 ranges, Azure Firewall for egress and east-west inspection, private endpoints for sensitive PaaS, private DNS zones resolved through DNS Private Resolver, ExpressRoute with a redundant circuit, VPN for branches and remote users, Application Gateway with WAF or Front Door for public entry points, and Bastion instead of public RDP. No VM gets a public IP unless somebody signs for it.
Compute and data patterns
Rather than let each team invent its own, the platform ships approved patterns.
For compute: hardened images in Azure Compute Gallery, Trusted Launch with Secure Boot and vTPM, disk encryption, managed identity instead of stored credentials, Update Manager for patching, Defender for Servers, backup enabled at deploy time, and no inbound public access. AKS, Container Apps, App Service, and Functions each have their own version of the same baseline. Azure Arc pulls the remaining on-prem servers into the same policy and monitoring plane.
For data: private access by default, TLS 1.2 minimum, Entra authentication over SQL logins where the app supports it, auditing and threat protection on databases, defined backup retention, and geo-replication where residency rules allow. Storage accounts have public blob access disabled and shared key access turned off wherever an app can be moved to managed identity.
Data residency drove several of these. A regulated workload in one country couldn't replicate to the paired region on the other side of a border, so that workload got a different DR design instead of the standard one.
Secrets, keys, and certificates
Key Vault per environment with RBAC rather than access policies, private endpoints, soft delete, and purge protection. Managed HSM only where a regulator asked for it, because it costs real money.
The goal was to stop storing secrets at all. Most application secrets vanished once the app moved to a managed identity. The ones that remain have a rotation schedule and an owner. Certificate expiry alerts go to a team, not to the person who happened to request the cert in 2022.
Governance without breaking production
Azure Policy is where enterprise rollouts hurt themselves. Somebody assigns a Deny policy across the tenant on Tuesday and half the pipelines fail on Wednesday.
Everything ran in Audit mode first. Watch what trips, fix the real gaps, then move to Deny. Policies cover allowed regions, allowed resource types where it makes sense, required tags, naming, encryption, TLS versions, public network access, diagnostic settings, backup, and Key Vault protections.
Exceptions exist because reality exists. Each one carries a named owner, an expiry date, a compensating control, and a review. An exception with no expiry date is a permanent hole with paperwork attached.
Security operations
Defender for Cloud across every subscription, with the specific plans chosen per workload type rather than switched on everywhere. Defender XDR covering identity, endpoints, email, and SaaS apps. Sentinel as the SIEM, with detection rules, playbooks for the repetitive response steps, and an incident workflow that puts a named human on each severity level.
The Microsoft Cloud Security Benchmark gives the team a score to measure against, which turns "are we secure" into a number a board can read.
The SOC model was its own decision. Central team, follow-the-sun, or a managed provider for out-of-hours. That choice changes the Sentinel design more than any technical setting does.
Logging, and what not to log
Central Log Analytics, diagnostic settings applied by policy, data collection rules for VMs, activity logs, Entra sign-in and audit logs, network flow logs, and Microsoft 365 audit feeds into Sentinel.
I also wrote down what we deliberately don't ingest. Sentinel bills by the gigabyte. Verbose debug logs from a dev App Service can cost more per year than the App Service. Some data belongs in cheap archive tiers, some belongs in a basic table, and some belongs nowhere. Security logs that regulators care about go to immutable retention. The rest gets a lifespan.
Alerting was scoped the same way. An alert nobody acts on is noise that trains the on-call engineer to ignore the channel.
Microsoft 365
The Azure side gets the attention, but most of the company's data sits in Exchange, SharePoint, OneDrive, and Teams.
Defender for Office 365 handles anti-phishing, Safe Links, and Safe Attachments. External sharing is scoped per site rather than left open at tenant level. Guest access in Teams has a defined lifecycle. Retention policies and eDiscovery hold requirements come from legal, not from IT preference. Audit logging stays on with the retention the compliance team asked for.
Purview and data governance
Sensitivity labels, DLP, endpoint DLP, retention, and records management, applied where they earn their keep.
The labeling scheme started small on purpose. Four labels people can explain beat fourteen nobody applies. Auto-labeling handles the obvious cases like payment card numbers and national ID formats.
Insider risk features came with a legal review first. Worker monitoring rules differ by country, and several of the countries in scope restrict what an employer can collect. The design records which capabilities are enabled in which jurisdictions and who signed off.
Cost
Budgets per subscription with alerts to the owning team, tags that make chargeback possible, anomaly detection, reservations and savings plans for steady-state compute, Azure Hybrid Benefit for Windows licenses the company already owns, auto-shutdown for dev and test, storage lifecycle rules, and a monthly review where a human looks at the top ten line items.
Cost ownership sits with the workload team. A central platform team that owns the bill for everyone ends up owning nothing.
Backup and recovery
Azure Backup with immutable vaults for the data ransomware would hit first, backup isolation so a compromised admin account can't delete the recovery point, Site Recovery where the RTO justifies the spend, database failover groups, and geo-redundant storage where residency allows.
RPO and RTO come per application tier, not as one global number. A tier-one payments system and an internal wiki don't deserve the same recovery budget.
Restore testing is scheduled and recorded. A backup nobody has restored is a hypothesis.
The licensing conversation nobody enjoys
Half the controls above depend on specific licenses. PIM, Identity Protection, access reviews, and entitlement management need Entra ID P2. Insider risk, advanced eDiscovery, and some Purview features need E5 or the compliance add-on. Defender plans bill per resource per month. Sentinel bills per gigabyte ingested.
I built a licensing matrix that maps each control to what it costs to keep, then split the design into a mandatory baseline the company already had licenses for and a second tier that needed a purchase decision. The security team could then argue for the second tier with real numbers instead of a wish list.
Preview features got flagged separately. Nothing in Preview went into the mandatory baseline, because Preview services carry no SLA and can change without warning.
Rollout
The work ran in phases: discovery, requirements, architecture, threat model, code, static testing, sandbox, pilot, production, validation, handover.
The threat model deserves its own mention. Before any of it deployed, we mapped where an attacker would go first. That exercise moved break-glass account handling and backup isolation up the priority list and pushed a few nice-to-haves down.
Nothing tenant-wide got applied without showing the blast radius first. Conditional Access rolled out in report-only mode against a pilot group before enforcement. Policy Deny rules were tested against real deployments before broad assignment.
All of it is code
The platform is Terraform, built on Azure Verified Modules and Microsoft's Landing Zone accelerator rather than modules written from scratch. Pipelines authenticate with OIDC federation, so no long-lived secret sits in the CI system. State is remote and locked. Module versions are pinned. Drift detection runs on a schedule and reports when somebody changed something in the portal at midnight.
On top of that sits a catalog of workload templates: network spoke, Windows VM, Linux VM, App Service, Function App, container app, AKS cluster, storage, SQL, PostgreSQL, Cosmos, Key Vault, private endpoint, Application Gateway with WAF, Front Door, API Management, monitoring, backup, and a full standard production application that stitches several of them together.
Each template inherits the central security settings without the app team configuring anything. A team that wants a web app gets a compliant web app. They don't get a decision to make about TLS.
Documentation lives in the same repo: architecture, diagrams, runbooks for deployment and rollback, incident response, ransomware recovery, break-glass procedure, onboarding a new application, offboarding, a decision log, and a risk register. Docs that live somewhere else go stale within a quarter.
The register that keeps everyone honest
The piece I'd recommend to anyone doing this work is a service disposition register. One table listing the relevant Azure and Microsoft 365 services, and for each one a decision:
Adopt for standard supported services
Available for services teams can use with review
Restricted for services with conditions attached
Exception only for services that need a business case
Blocked for services with no place in this estate
Not applicable
Each row records the approved use case, prohibited scenarios, security requirements, networking pattern, data classification limits, owner, cost notes, the matching IaC module, the Microsoft source behind the decision, and a review date.
It solves two problems at once. Nothing gets forgotten, because the whole catalog sits on the page. And nothing gets deployed for the sake of existing, because deploying a service now requires writing a sentence about why.
When an auditor asks whether the organization considered a control, the answer is a row with a date on it.
Proving it works
Deployment succeeding proves the templates ran. It proves nothing about the platform.
So the validation phase tested the claims instead of the resources. Sign-in policies blocking what they should block. PIM elevation requiring approval. A non-compliant device getting refused. Traffic between two spokes hitting the firewall rule that should stop it. Private DNS resolving to the private endpoint and not the public one. Logs arriving in Sentinel within the expected window. A backup restoring into a clean resource group. A failover test against the DR region.
And the one that matters most: a brand new subscription coming out of the vending process with every control attached and no human touching it.
What I'd do differently
Two things.
I underestimated the PKI work. The certificate estate was older and less documented than anyone thought, and it held up the endpoint rollout by weeks. If I did this again I'd inventory certificates in week one, not week six.
I also built the labeling scheme before talking to enough of the people who'd apply it. The first version had too many labels and a naming convention only the compliance team understood. Cutting it down took a rewrite that better questions early would have avoided.
The reusable part held up, though. A future deployment needs a small config file with company name, tenant ID, domains, regions, IP ranges, environment, owners, data classification, cost center, and recovery targets. The automation does the rest. That was the whole point of the exercise.
Related Articles
You might also like
View all
Automate Virtual Machine Creation in Proxmox with Cloud-Init: Complete Guide

How to Install Bitninja for Cloudpanel Control for Server Security
Unleashing the Power of Hetzner Cloud with Bash and API Magic
Comments (0)
No comments yet. Be the first to comment!
