Running SentinelOne Deployment Safely from Azure DevOps

A production-ready Azure DevOps pipeline pattern for running the SentinelOne Azure VM deployment script with secure secret handling.

This companion guide shows how to run the SentinelOne deployment script from Azure DevOps without storing API keys or site tokens in source control. The pipeline pattern below uses secure variables or Azure Key Vault, limits execution scope, publishes the CSV result, and keeps rollout controls explicit.

Architecture at a glance

The production pattern is simple:

  1. Store the SentinelOne API key and site tokens as protected secrets.
  2. Run the PowerShell script from an Azure DevOps pipeline agent.
  3. Pass only the subscription scope and operational switches at runtime.
  4. Publish the generated CSV report as a build artifact.
  5. Review failures, stopped VMs, and exceptions outside the pipeline.

What changed in the script

The deployment script now supports three secret sources, in this order:

  1. Explicit PowerShell parameters.
  2. Environment variables.
  3. Azure Key Vault secrets.

That means the pipeline does not need to edit the script before each run. It only needs to inject the correct values at execution time.

If you use environment variables, align with the names the script already resolves:

Secret Environment variable
SentinelOne console API key SENTINELONE_API_KEY
Production site token S1_SITE_TOKEN_PROD
Pre-production site token S1_SITE_TOKEN_PREPROD
QA site token S1_SITE_TOKEN_QA
Development site token S1_SITE_TOKEN_DEV

If you use Azure Key Vault, the script defaults to these secret names:

Secret Key Vault secret name
SentinelOne console API key sentinelone-console-api-key
Production site token sentinelone-site-token-prod
Pre-production site token sentinelone-site-token-preprod
QA site token sentinelone-site-token-qa
Development site token sentinelone-site-token-dev

Option 1: Use Azure DevOps secret variables

This is the quickest secure starting point if you are not yet standardised on Key Vault.

Create a variable group with secret variables for:

  1. SENTINELONE_API_KEY
  2. S1_SITE_TOKEN_PROD
  3. S1_SITE_TOKEN_PREPROD
  4. S1_SITE_TOKEN_QA
  5. S1_SITE_TOKEN_DEV

Then reference that group from the pipeline.

Option 2: Use Azure Key Vault

For longer-term operations, Key Vault is the better pattern because it separates secret lifecycle management from the pipeline definition.

In that model:

  1. The pipeline service connection reads secrets from Key Vault.
  2. The pipeline exports those values as environment variables for the script.
  3. The script runs unchanged.

You can also let the script read Key Vault directly by passing -KeyVaultName and, if required, -KeyVaultSubscriptionId. In Azure DevOps, I prefer retrieving secrets in the pipeline first because it keeps the execution contract visible in one place.

Production-ready pipeline example

The YAML below is designed for a manually triggered operational run. It supports dry runs, targeted subscription scope, optional remediation of failed extensions, and CSV artifact publishing.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
trigger: none
pr: none

parameters:
  - name: subscriptionIds
    displayName: Subscription IDs
    type: string
    default: ''

  - name: excludedLocations
    displayName: Excluded locations
    type: string
    default: 'ukwest'

  - name: excludedVmNames
    displayName: Excluded VM names
    type: string
    default: 'ufwduks001v,ufwduks002v,uavsjbtempuks001'

  - name: autoStartVMs
    displayName: Auto-start stopped VMs
    type: boolean
    default: false

  - name: reinstallFailedExtensions
    displayName: Reinstall failed extensions
    type: boolean
    default: false

  - name: whatIf
    displayName: Dry run only
    type: boolean
    default: true

variables:
  - group: sentinelone-deployment-secrets
  - name: scriptPath
    value: scripts/Sentinel_Check_&_Install_v1.ps1
  - name: reportPath
    value: $(Build.ArtifactStagingDirectory)/sentinelone/SentinelCheck_Results.csv

pool:
  vmImage: windows-latest

stages:
  - stage: DeploySentinelOne
    displayName: Deploy SentinelOne
    jobs:
      - job: RunDeployment
        displayName: Run deployment script
        steps:
          - checkout: self

          - task: AzurePowerShell@5
            displayName: Run SentinelOne deployment script
            inputs:
              azureSubscription: 'sc-azure-platform-prod'
              ScriptType: InlineScript
              azurePowerShellVersion: LatestVersion
              Inline: |
                $subscriptionIds = @()
                if ('${{ parameters.subscriptionIds }}'.Trim()) {
                  $subscriptionIds = '${{ parameters.subscriptionIds }}'.Split(',').Trim() | Where-Object { $_ }
                }

                $excludedLocations = '${{ parameters.excludedLocations }}'.Split(',').Trim() | Where-Object { $_ }
                $excludedVmNames = '${{ parameters.excludedVmNames }}'.Split(',').Trim() | Where-Object { $_ }

                $scriptArguments = @{
                  SubscriptionIds = $subscriptionIds
                  ExcludedLocations = $excludedLocations
                  ExcludedVMNames = $excludedVmNames
                  CsvPath = '$(reportPath)'
                }

                if ('${{ parameters.autoStartVMs }}' -eq 'true') {
                  $scriptArguments['AutoStartVMs'] = $true
                }

                if ('${{ parameters.reinstallFailedExtensions }}' -eq 'true') {
                  $scriptArguments['ReinstallFailedExtensions'] = $true
                }

                if ('${{ parameters.whatIf }}' -eq 'true') {
                  $scriptArguments['WhatIf'] = $true
                }

                & '$(Build.SourcesDirectory)/$(scriptPath)' @scriptArguments
            env:
              SENTINELONE_API_KEY: $(SENTINELONE_API_KEY)
              S1_SITE_TOKEN_PROD: $(S1_SITE_TOKEN_PROD)
              S1_SITE_TOKEN_PREPROD: $(S1_SITE_TOKEN_PREPROD)
              S1_SITE_TOKEN_QA: $(S1_SITE_TOKEN_QA)
              S1_SITE_TOKEN_DEV: $(S1_SITE_TOKEN_DEV)

          - task: PublishBuildArtifacts@1
            displayName: Publish SentinelOne report
            condition: succeededOrFailed()
            inputs:
              PathtoPublish: $(Build.ArtifactStagingDirectory)/sentinelone
              ArtifactName: sentinelone-report
              publishLocation: Container

Key pipeline decisions

This example deliberately makes a few opinionated choices:

Decision Reason
trigger: none Deployment should be an intentional operational action, not a CI side effect.
Manual parameters Operators can narrow scope without editing YAML.
whatIf defaults to true Safe by default. Live execution is a conscious change.
condition: succeededOrFailed() on artifact publish You still want the CSV output when some installs fail.
Windows hosted agent Keeps execution consistent with Azure PowerShell task behavior.

Key Vault-backed variable groups

If you already use Azure Key Vault with Azure DevOps variable groups, the pipeline gets even cleaner. In that setup:

  1. Link the variable group to your Key Vault.
  2. Map the Key Vault secrets to the same variable names used above.
  3. Keep the YAML unchanged.

This avoids duplicating secret values between Azure DevOps and Key Vault.

Guardrails I recommend in production

For a real enterprise rollout, add these controls around the pipeline:

  1. Restrict who can queue the pipeline.
  2. Separate non-production and production service connections.
  3. Use approvals for production stages if you later split the pipeline into environments.
  4. Log the approved subscription list for every execution.
  5. Keep exception VM names and excluded regions under review.

The pipeline is easier to maintain when the repo structure is explicit:

1
2
3
4
5
6
azure-pipelines/
  sentinelone-deployment.yml
scripts/
  Sentinel_Check_&_Install_v1.ps1
docs/
  sentinelone-runbook.md

If the script currently lives outside source control, move it into a controlled repository before putting the pipeline into service. That gives you traceability for every operational change.

Operational runbook flow

The process for an operator should be straightforward:

  1. Queue the pipeline.
  2. Start with a non-production subscription and dry run enabled.
  3. Review the pipeline log and generated CSV.
  4. Re-run live only after the dry-run scope looks correct.
  5. Review failed installs, stopped VMs, and skipped subscriptions separately.

Final thoughts

The safest way to run the SentinelOne deployment script is to treat it like an operational control, not an admin laptop task. Azure DevOps gives you repeatability, approvals, artifacts, and audit history. Combined with the script’s new secret-resolution logic, that gives you a workable production model without putting credentials in the repo.