Azure DevOps to On-Prem Deployments with WinRM over HTTPS

How I proved a secure deployment path for on-prem Web, App, DB, and file-system changes without exposing servers to the public internet.

Why This Matters in Delivery

When Azure DevOps is internet-facing, security teams often reject direct on-prem deployment access. The challenge is not only to make the deployment work, but to make it work with a narrow and auditable security model.

What I Actually Faced

I had to prove that Azure DevOps could deploy to on-prem servers securely for Web, App, DB, and file-system tasks. The concern was that if the deployment path was not tight enough, the servers would be exposed more than the security team could accept.

Once I proved the command execution path and permissions model, the architect team accepted the design.

Security Questions We Had to Answer

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
QuestionExpectationResulting Control
Can servers stay off the public internet?Yes, only private connectivity should be used.Use ExpressRoute/private network path.
Can remoting be encrypted end to end?Yes, no plain-text command channel.WinRM over HTTPS on port 5986.
Can access be limited?Yes, only required ports and accounts.Firewall allowlist + least-privilege deployment account.
Can we prove it before rollout?Yes, with a small smoke test.Pipeline creates and deletes a test file on each target host.

The Secure Design I Chose

The deployment path is private, encrypted, and tightly scoped. Azure DevOps orchestrates the release, but the actual command execution happens through WinRM over HTTPS inside the private network boundary.

  • Only the required WinRM HTTPS port is exposed on the on-prem servers.
  • Certificate-backed listeners are used instead of plain HTTP WinRM.
  • The deployment account is restricted to only the target actions it needs.
  • Ports for application, database, and file-system activity are only opened when the deployment actually needs them.

WinRM HTTPS Setup

On the target Windows server, I used an HTTPS listener with a certificate and kept unencrypted remoting disabled. That gave us a remoting channel that the security team could reason about and audit.

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
ItemValueWhy
WinRM HTTPS5986Encrypted PowerShell remoting channel
WinRM HTTP5985Not the secure path for this design
CertificateInternal CA cert on each target serverProves server identity during remoting
FirewallAllow only required source networksMinimise attack surface

Sample Release Pipeline

The pipeline first checks connectivity, then runs a safe smoke action, then performs the actual deployment steps. That sequence gave the team confidence that access and permissions were in place before any real change ran.

trigger: none
pool:
  name: SelfHostedWindows

steps:
- powershell: |
    $servers = @('web01','app01','db01','fs01')
    foreach ($server in $servers) {
      Test-WSMan -ComputerName $server -UseSSL
    }
  displayName: 'Validate WinRM over HTTPS'

- powershell: |
    $servers = @('web01','app01','db01','fs01')
    $cred = New-Object System.Management.Automation.PSCredential($env:DEPLOY_USER,(ConvertTo-SecureString $env:DEPLOY_PASS -AsPlainText -Force))
    foreach ($server in $servers) {
      Invoke-Command -ComputerName $server -UseSSL -Credential $cred -ScriptBlock {
        New-Item -Path 'C:\DeploySmoke' -ItemType Directory -Force | Out-Null
        Set-Content -Path 'C:\DeploySmoke\pipeline.txt' -Value (Get-Date).ToString('o')
        Remove-Item -Path 'C:\DeploySmoke\pipeline.txt' -Force
      }
    }
  displayName: 'Smoke test permissions'

Validation Commands

Server-side checks

Enable-PSRemoting -Force
Set-Item WSMan:\localhost\Service\AllowUnencrypted $false
New-Item -Path WSMan:\localhost\Listener -Transport HTTPS -Address * -CertificateThumbPrint <THUMBPRINT> -Force
Get-ChildItem WSMan:\localhost\Listener

Connectivity checks

Test-NetConnection server01 -Port 5986
Test-WSMan -ComputerName server01 -UseSSL
Invoke-Command -ComputerName server01 -UseSSL -Credential $cred -ScriptBlock { hostname }

What the Pipeline Proved

Real Scenario: Deployment Approval Blocked Until Connectivity Was Proven

The release was blocked because nobody wanted to approve a deployment path that sounded risky. Once the WinRM HTTPS listener, certificate validation, and file smoke test were demonstrated, the proposal became much easier to approve.

Data Panel
Reference table for quick scanning and comparison.
Reference Live Status
SymptomWhat It MeantWhat I Checked
Agent cannot reach serverPrivate routing or firewall issueExpressRoute path, NSG/firewall, port 5986
WinRM works but command failsPermission issueDeployment account rights and target folder ACLs
HTTPS listener not trustedCert or listener mismatchListener thumbprint, certificate store, server name

Delivery Lessons