Sentinel as code: greenfield CI/CD design
Chapter 1 explained Terraform. This chapter applies it to a whole Sentinel estate built from nothing: what can be code, what cannot, what to set up on day zero in Azure and in your DevOps platform, and a pipeline design you can defend in front of a security architect.
Two decisions that come before any code
1. The portal is no longer a choice.
- Sentinel in the Azure portal retires on 31 March 2027. After that it exists only in the Microsoft Defender portal.
- Since July 2025, a new customer's first workspace is onboarded to the Defender portal automatically, if the person onboarding is a subscription Owner or User Access Administrator and is not working through Azure Lighthouse delegation. Consultants often are delegated, so plan for the manual step.
- The Defender XDR connector is enabled automatically on Defender portal onboarding. Do not script it separately.
- Runbooks, RBAC design and analyst training should target the Defender portal, not the Azure blade.
2. Which tables go to which storage tier.
| Analytics tier | Data lake tier | |
|---|---|---|
| For | Detection, hunting, workbooks, incidents | Long retention, historical investigation, KQL and notebook jobs |
| Analytic rules can use it | Yes | No, it is not real-time |
| Retention | 30 days by default (90 free for Sentinel solution tables), up to 2 years | Up to 12 years |
| Typical tables | Identity, endpoint, cloud control plane: anything a rule fires on | Firewall, proxy, NetFlow, verbose audit kept for forensics |
Analytics-tier data is mirrored to the lake at no extra charge. Sending data to the lake only has its own ingestion and processing meters. Since 23 September 2026, data lake onboarding is part of Sentinel onboarding, and you enable the lake tier per table in Table management. Write the tiering decision down per table, with an owner. It is the biggest lever on the bill, and the one most often decided by accident.
What is Terraform, what is ARM, what stays human
| Human step | Who | Why it resists automation |
|---|---|---|
| Onboard the workspace to the Defender portal | Security Administrator or Global Administrator | A tenant-level action, not a resource |
| Consent for Microsoft 365 and Entra connectors | Global Administrator | Tenant-wide consent, outside the resource plane |
| Third-party credentials | Application owner | The credential cannot live in the repository |
| Authorise Logic App API connections | Engineer | Each connection needs an interactive sign-in once, or a managed identity granted a role |
| Enable the data lake tier per table | Security Administrator | Portal-driven in Table management |
| UEBA data source selection | Security Administrator | Per-tenant choice with Entra and Defender for Identity prerequisites |
The greenfield build, in dependency order
| Phase | What gets built | How |
|---|---|---|
| 0. Landing zone | Resource groups, tags, locks, the Log Analytics workspace, RBAC groups, platform diagnostic settings | Terraform |
| 1. Platform | Enable Sentinel, onboard to the Defender portal, table tiering and retention, UEBA and anomalies | Terraform, plus a human for Defender onboarding and an API call or portal step for UEBA |
| 2. Ingestion | First-party connectors, third-party and codeless connectors, data collection rules and transformations | Terraform for fixed connectors and DCRs, ARM for codeless, a human for consent and credentials |
| 3. Content | Content Hub solutions, analytic rules, hunting queries, parsers, watchlists, workbooks | Terraform, with Content Hub through ARM |
| 4. Operations | Playbooks, automation rules, incident settings, SOC runbooks and RACI | Terraform for resources, people for the operating model |
Repository design
One repository, one folder per kind of content, one Terraform root, and a separate state file per environment. The branch and the pipeline stage decide the target environment. The repository does not.
rules/ analytic rules, one YAML per detection
automation/ automation rules
watchlists/ CSV per watchlist + small YAML header
hunting/ hunting queries and parsers (KQL files)
workbooks/ workbook JSON
playbooks/ exported Logic App ARM templates
arm/ Content Hub and codeless connector templates
terraform/
providers.tf variables.tf outputs.tf
platform.tf workspace, onboarding, tables, RBAC
content.tf reads rules/, automation/, watchlists/, hunting/
environments/dev.tfvars environments/prod.tfvars
scripts/validate_rules.py the quality gate, runs with no Azure access
azure-pipelines.yml or .github/workflows/
docs/runbook-human-steps.md the consent steps, with owners- State per environment: same storage account, different blob key:
sentinel-dev.tfstateandsentinel-prod.tfstate. One environment's run can never touch the other's state. - Platform and content can split later. When the content team and the platform team are different people, give each its own Terraform root and state so a rule change never plans against the workspace.
- Rule identity is the GUID, not the file name or display name. Never reuse or change one.
Identities and roles
Least privilege means scoping each identity to the smallest thing that works:
| Identity | Role | Scope |
|---|---|---|
| Pipeline identity | Microsoft Sentinel Contributor | The workspace |
| Pipeline identity | Storage Blob Data Contributor | The state storage account |
| Pipeline identity, if it owns the workspace, tables or DCRs | Log Analytics Contributor (or Contributor) | The Sentinel resource group |
| Pipeline identity, if it deploys playbooks | Logic App Contributor | The playbook resource group |
| Pipeline identity, only if Terraform creates role assignments | Role Based Access Control Administrator, with conditions | As narrow as possible |
| Sentinel itself (Azure Security Insights) | Microsoft Sentinel Automation Contributor | The playbook resource group, so automation rules can run playbooks |
| Person running the one-time bootstrap | Owner | The subscription, for the bootstrap only |
Day-zero setup, first time ever
The order matters. Azure comes first because the DevOps platform needs something to connect to.
Step 1: Azure foundation (once, by a subscription Owner, in Cloud Shell or the CLI)
- Create the resource group and the Log Analytics workspace, and enable Sentinel on it.
- Create the state storage account: Entra-only access, public blob access off, versioning on, and a
tfstatecontainer. - Onboard the workspace to the Defender portal, if that did not happen automatically.
This is the one part that cannot itself be in the pipeline, because the pipeline needs the state storage and its identity to exist before it can run. Keep it in a reviewed bootstrap script in the repository.
Step 2: the DevOps platform. Pick one.
Azure DevOps is a separate service at dev.azure.com, not a resource you deploy in the Azure portal. The portal's app launcher has a shortcut to it.
dev.azure.com with a Microsoft work, school or personal account, or with GitHub. Choose New organization, pick a name and a hosting geography, and link an Azure subscription for billing. The free tier (five Basic users, one Microsoft-hosted parallel job with up to 1,800 minutes a month, one self-hosted job) only applies once billing is linked. For a company, connect the organisation to the Entra tenant under Organization settings → Microsoft Entra.The pipeline design
| Stage | Runs on | Needs Azure? | Purpose |
|---|---|---|---|
| Validate | Every pull request | No | Schema, KQL hygiene, MITRE names, entity mappings, the validator's own tests, terraform fmt and validate |
| Plan | Every pull request | Read only | Shows the reviewer exactly what will change in Azure |
| Apply to dev | Merge to main | Write, dev | Lands the change where it can be tested |
| Smoke check | After dev apply | Read, dev | Does the rule run and fire on replayed test data? |
| Apply to prod | After approval | Write, prod | The only path to production |
| Drift plan | Nightly schedule | Read only | Flags anything changed in the portal, without reverting it blindly |
The pipeline file
The production stage, in full. The dev stage is the same with the sentinel-dev variable group and environment and no approval.
trigger:
branches: { include: [main] }
paths: { include: [rules/*, terraform/*] }
variables:
serviceConnection: sc-sentinel-oidc # the service connection's name
stages:
- stage: validate
jobs:
- job: validate
pool: { vmImage: ubuntu-latest }
steps:
- script: |
pip install -r requirements-dev.txt
python scripts/validate_rules.py --strict
pytest -q
displayName: Validate rules (no Azure access)
# ... plan and deploy_dev stages follow the same shape ...
- stage: deploy_prod
dependsOn: deploy_dev
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
variables:
- group: sentinel-prod
jobs:
- deployment: apply
environment: sentinel-prod # the Approvals check lives here
pool: { vmImage: ubuntu-latest }
strategy:
runOnce:
deploy:
steps:
- checkout: self # deployment jobs do not check out by default
- script: |
curl -sSLo tf.zip https://releases.hashicorp.com/terraform/1.9.8/terraform_1.9.8_linux_amd64.zip
unzip -o tf.zip -d "$(Agent.ToolsDirectory)/tf"
echo "##vso[task.prependpath]$(Agent.ToolsDirectory)/tf"
displayName: Install pinned Terraform
- task: AzureCLI@2
displayName: terraform apply
inputs:
azureSubscription: ${{ variables.serviceConnection }}
scriptType: bash
scriptLocation: inlineScript
workingDirectory: terraform
inlineScript: |
terraform init -input=false \
-backend-config="resource_group_name=$(TFSTATE_RESOURCE_GROUP)" \
-backend-config="storage_account_name=$(TFSTATE_STORAGE_ACCOUNT)" \
-backend-config="container_name=tfstate" \
-backend-config="key=sentinel-prod.tfstate" \
-backend-config="use_oidc=true" \
-backend-config="use_azuread_auth=true"
terraform apply -input=false -auto-approve \
-var-file=environments/prod.tfvars
env:
ARM_USE_OIDC: true
ARM_CLIENT_ID: $(AZURE_CLIENT_ID)
ARM_TENANT_ID: $(AZURE_TENANT_ID)
ARM_SUBSCRIPTION_ID: $(AZURE_SUBSCRIPTION_ID)
ARM_ADO_PIPELINE_SERVICE_CONNECTION_ID: $(SERVICE_CONNECTION_ID)
SYSTEM_ACCESSTOKEN: $(System.AccessToken)
SYSTEM_OIDCREQUESTURI: $(System.OidcRequestUri)
TF_VAR_log_analytics_workspace_id: $(LOG_ANALYTICS_WORKSPACE_ID)AzureCLI@2 task references the service connection, which makes the pipeline's OIDC token available. The azurerm provider and backend read SYSTEM_ACCESSTOKEN, SYSTEM_OIDCREQUESTURI and the service connection ID, request the token, and exchange it with Entra. No password is involved at any point.use_azuread_auth=true, the azurerm backend tries to fetch the storage account key. A least-privilege pipeline identity has only a data-plane role on the state account, so init fails. Granting key access to make it work would undo the point of OIDC.Three ways to do Sentinel CI/CD
| Approach | Strengths | Weaknesses | Use when |
|---|---|---|---|
| Terraform | Drift detection, dependencies, one language for platform and content, a real plan to review | State to protect; gaps for Content Hub, settings and codeless connectors | You own the whole build |
| Sentinel Repositories (built in) | Set up from the portal; no state; deploys ARM and Bicep content; parameter files per workspace | Five connections per workspace; deletions are not propagated; content only; needs Owner on the resource group to connect | A content team that wants Git without running Terraform |
| ARM or Bicep directly | Covers everything the API covers | Verbose; no plan; no drift detection | The gaps: Content Hub, codeless connectors, summary rules |
The defensible answer in a design review is a hybrid: Terraform for the platform and content lifecycle, ARM for the gaps (deployed from the same pipeline), and a written runbook for the consent steps. To build all of this yourself, part 3 is a hands-on lab.
Traps that cost a day each
What to automate next, in order
- Analytic rules: the highest change rate and the clearest review value. Start here.
- Automation rules and watchlists: small, stable, and rules depend on them.
- Hunting queries and parsers: saved searches, same pattern.
- Workbooks: JSON in the repository, one resource each.
- Playbooks: exported ARM templates, plus the connection runbook step.
- Content Hub solutions: ARM from the solution package, deployed by the same pipeline.
- Tables, retention and DCRs: once the tiering decision is written down.
Test yourself
Sources
Checked 25 October 2026. Re-check retirement dates and onboarding behaviour before quoting them to a client: the Azure portal retirement has already moved once.
- Sentinel in the Azure portal: retirement timeline
- Microsoft Sentinel in the Defender portal
- Manage data tiers and retention
- Manage content as code with Sentinel repositories
- Azure DevOps: create an organization and free tier of parallel jobs
- Azure DevOps: workload identity service connections
- azurerm provider: authenticating with OIDC and the azurerm backend