Obfuscating .NET builds in CI/CD

Wire the obfuscation API into GitHub Actions, Azure Pipelines or a local build script so every release artifact is protected automatically.

A manual protection step is a step that eventually gets skipped — usually on the hotfix release going out at speed. Moving obfuscation into the pipeline makes it automatic and, more importantly, makes it impossible to forget.

Pro plan required. API access, 100 MB files and batches of up to 20 are Pro features. See pricing, then collect your key from your account page.

Where it belongs in the pipeline

The order that works, and why:

  1. Build in Release configuration.
  2. Test against the unobfuscated output.
  3. Obfuscate the artifacts you are shipping.
  4. Smoke-test the obfuscated build.
  5. Sign, package and publish.

Two details matter here. Run your main test suite before obfuscation, because test code reaches internals by name and reflection in ways your shipped code does not — testing the protected build directly produces failures that tell you nothing useful. But do run a smoke test after, since that is the only thing standing between a rename-induced runtime break and your users.

Sign after obfuscating. Obfuscation rewrites the assembly, which invalidates any signature applied beforehand.

GitHub Actions

Store the key as a repository secret named FO_API_KEY under Settings → Secrets and variables → Actions.

name: release

on:
  push:
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-dotnet@v4
        with:
          dotnet-version: '8.0.x'

      - name: Build
        run: dotnet build -c Release

      - name: Test
        run: dotnet test -c Release --no-build

      - name: Obfuscate
        env:
          FO_API_KEY: ${{ secrets.FO_API_KEY }}
        run: |
          curl -sS --fail-with-body \
            -X POST https://freeobfuscator.com/api/v1/obfuscate \
            -H "X-Api-Key: $FO_API_KEY" \
            -F "file=@bin/Release/net8.0/MyApp.dll" \
            -F "renamePublic=false" \
            -o MyApp.protected.dll

      - name: Verify the output is a real assembly
        run: |
          test -s MyApp.protected.dll
          head -c 2 MyApp.protected.dll | grep -q 'MZ'

      - uses: actions/upload-artifact@v4
        with:
          name: protected-build
          path: MyApp.protected.dll

--fail-with-body is the flag that matters. Without it curl exits successfully on an HTTP error and writes the JSON error response into your output file — producing a green build that ships a corrupt artifact. With it, curl returns a non-zero exit code and prints the error, so the job fails loudly.

The two-line verification step is cheap insurance: a non-empty file starting with the MZ signature is a real PE image rather than an error page.

Azure Pipelines

steps:
  - task: DotNetCoreCLI@2
    inputs:
      command: build
      arguments: '-c Release'

  - task: PowerShell@2
    displayName: Obfuscate
    env:
      FO_API_KEY: $(FoApiKey)   # mark as secret in the pipeline UI
    inputs:
      targetType: inline
      script: |
        $form = @{
          file         = Get-Item "bin/Release/net8.0/MyApp.dll"
          renamePublic = "false"
        }

        Invoke-RestMethod `
          -Uri "https://freeobfuscator.com/api/v1/obfuscate" `
          -Method Post `
          -Headers @{ "X-Api-Key" = $env:FO_API_KEY } `
          -Form $form `
          -OutFile "MyApp.protected.dll"

        if ((Get-Item "MyApp.protected.dll").Length -eq 0) {
          throw "Obfuscation produced an empty file"
        }

Invoke-RestMethod throws on non-success status codes by default, so a failed call fails the task without any extra flags.

A local build script

For a solution with several projects, a small script keeps things consistent. Read the key from the environment — never commit it.

param([string]$Config = "Release")

$ErrorActionPreference = "Stop"
if (-not $env:FO_API_KEY) { throw "FO_API_KEY is not set" }

dotnet build -c $Config

$targets = @(
  "src/MyApp/bin/$Config/net8.0/MyApp.dll",
  "src/MyApp.Core/bin/$Config/net8.0/MyApp.Core.dll"
)

New-Item -ItemType Directory -Force -Path artifacts | Out-Null

foreach ($t in $targets) {
  $name = Split-Path $t -Leaf
  Write-Host "Obfuscating $name"

  Invoke-RestMethod `
    -Uri "https://freeobfuscator.com/api/v1/obfuscate" `
    -Method Post `
    -Headers @{ "X-Api-Key" = $env:FO_API_KEY } `
    -Form @{ file = Get-Item $t; renamePublic = "false" } `
    -OutFile "artifacts/$name"
}

Write-Host "Done - artifacts/ holds the protected assemblies"

Handling failures properly

The API returns meaningful status codes, and treating them distinctly saves debugging time later:

StatusWhat it means in a pipeline
401The secret is missing, empty, or was rotated without updating CI.
402The subscription lapsed — usually an expired card.
413The artifact grew past 100 MB.
415Wrong path — you sent something that is not a .dll or .exe.
422The file is not a managed assembly. Check for a NativeAOT publish.
503Server busy. Retry after the Retry-After interval — worth a retry loop in CI.

401 after a key rotation is the single most common CI failure here. Rotating a key invalidates the old one immediately, so update the secret in the same change.

Keeping the key safe

  • Store it in the CI platform's secret store, never in the repository.
  • Pass it via environment variable rather than a command-line argument — arguments are visible in process listings and often in build logs.
  • Rotate from your account page if it is ever exposed, and update CI in the same change.
  • Beware of set -x or verbose logging in shell steps, which will happily echo your key into the build output.

What to smoke-test afterwards

Keep it short enough that it runs on every release. The goal is catching a rename-induced break, not re-running your suite:

  • The application starts and reaches its main screen or first request.
  • One round-trip through your serialization path.
  • One dependency-injection resolution of a top-level service.
  • Licence or activation logic, if you have any.

Those four cover the overwhelming majority of obfuscation regressions. Obfuscation and reflection explains why each of them is a risk and how to make them rename-safe.

Try it on your own assembly

Upload a DLL or EXE and download the obfuscated result. Free, no signup, nothing stored.