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.
Where it belongs in the pipeline
The order that works, and why:
- Build in Release configuration.
- Test against the unobfuscated output.
- Obfuscate the artifacts you are shipping.
- Smoke-test the obfuscated build.
- 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:
| Status | What it means in a pipeline |
|---|---|
401 | The secret is missing, empty, or was rotated without updating CI. |
402 | The subscription lapsed — usually an expired card. |
413 | The artifact grew past 100 MB. |
415 | Wrong path — you sent something that is not a .dll or .exe. |
422 | The file is not a managed assembly. Check for a NativeAOT publish. |
503 | Server 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 -xor 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.