Short answer: to obfuscate a .NET app in GitHub Actions, add a step after dotnet publish that runs the obfuscator on your published assemblies — with Nebula.NET that's nebula --config nebula.config.json, with the license supplied from a GitHub secret — then upload the protected output as your artifact. Obfuscating in CI (not on developer machines) means your team keeps clean, debuggable local builds while the build server produces the single hardened binary you ship.
Why obfuscate in CI, not on developer machines
.NET compiles to IL, which decompiles back to near-original C# in seconds (try it in ILSpy). Obfuscation rewrites that IL so it's hard to read and reconstruct. But where you run it matters:
- On dev machines: every developer needs the tool and a license, local builds become hard to debug, and it's easy to ship an unprotected build by mistake.
- In CI (recommended): obfuscation runs once, on the official Release build, on the build server. Developers build normally; only the runner is licensed; and the artifact you publish is always protected.
How to obfuscate a .NET app in GitHub Actions (step by step)
First, add a small nebula.config.json to your repo. Nebula is driven entirely by this config — it lists the assemblies to protect and where to write them (see the full configuration reference):
{
"inputs": ["publish/MyApp.dll"],
"outputDirectory": "protected",
"encryptStrings": true,
"controlFlow": true
}
List all your own interdependent assemblies together in
inputsso renaming stays consistent across them — see multiple assemblies. The obfuscated copies, the.symbols.jsonmap, and a report are written tooutputDirectory.
Then the workflow — obfuscation goes between dotnet publish and the artifact upload:
name: build-protected
on:
workflow_dispatch:
push:
tags: ['v*']
jobs:
publish:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
# 1. Build the Release output as usual
- name: Publish
run: dotnet publish src/MyApp/MyApp.csproj -c Release -o publish
# 2. Install the Nebula.NET CLI on the runner
- name: Install Nebula.NET
run: dotnet tool install --global Nebula.NET.Tool
# 3. Supply the license from a secret. NEBULA_LICENSE is a PATH to a license file;
# an offline signed file is ideal for CI (no online seat activation on ephemeral runners).
- name: Write license
shell: bash
run: echo "${{ secrets.NEBULA_LICENSE_FILE }}" > nebula-license.json
# 4. Obfuscate — Nebula is config-driven (there is no "obfuscate" verb)
- name: Obfuscate
env:
NEBULA_LICENSE: nebula-license.json
run: nebula --config nebula.config.json
# 5. Ship the protected output (obfuscated assemblies + runtime sidecars)
- uses: actions/upload-artifact@v4
with:
name: MyApp-protected
path: protected
Store your license as a GitHub secret (Settings → Secrets and variables → Actions). For CI, an offline signed license file is the cleanest choice: ephemeral runners get a fresh machine each run and shouldn't consume online seat activations. See licensing only the build server and the CLI reference for all options.
Option B: obfuscate during the build (MSBuild)
If you'd rather not add a separate step, Nebula.NET's MSBuild integration runs obfuscation after compile when the NebulaObfuscate property is set. Because MSBuild reads properties from the environment, you gate it on a variable that only exists on the build agent — same dotnet build, no code changes:
NebulaObfuscate=true
NEBULA_LICENSE=<path to your build-server license file>
Add Nebula's build-time package to the project (a build-only dependency, no source or runtime coupling):
<PackageReference Include="Nebula.NET.MSBuild" Version="*" PrivateAssets="all" />
Developer machines — where NebulaObfuscate isn't set — build unobfuscated as before.
Build integration is a licensed (paid) feature. The Free edition is the CLI and desktop app only; obfuscating as part of a build requires a valid license on the build machine (via
NEBULA_LICENSE,licenseFilein the config, or theNebulaLicenseFileMSBuild property). The CLI in Option A also runs on Free, with caps on a few features.
Verify it actually worked
Never trust a protection step you haven't checked. Open the output DLL from protected/ in ILSpy or dnSpy: methods should show a while(true){ switch } control-flow maze, strings should be gone, and names stripped (except the public API and anything reflection needs). Nebula.NET verifies every transform runs identically to the original build, but you should still run your test suite against the protected artifact before releasing.
Nebula writes a MyApp.symbols.json rename map next to the output — archive it as a CI artifact, because you'll need it to read production stack traces:
nebula deobfuscate --map MyApp.symbols.json --input crash.txt
Pitfalls to avoid
- Reflection / serialization / DI / XAML rely on names at runtime — make sure the tool detects and preserves those, and exclude anything custom. This is the #1 cause of "it worked in dev, broke in prod."
- Obfuscate before code-signing. If you sign first and obfuscate second, you invalidate the signature (and anti-tamper hashes). Order: publish → obfuscate → sign.
-
Keep the symbol map (
.symbols.json) per release so you can de-obfuscate crash reports later. - Watch single-file / trimming. If you publish single-file or trimmed, obfuscate the assemblies before packing, and test that combination.
Next steps
Grab the free edition to try the workflow, or read how to obfuscate a .NET assembly for the fundamentals. For production, Nebula.NET gives you control-flow flattening, string encryption, anti-tamper, and CI-friendly licensing that won't break your build.
FAQ
Can you obfuscate .NET code in a CI/CD pipeline?
Yes. Run a .NET obfuscator as a step after you publish the Release build, so only the shippable artifact is protected, and license just the build runner rather than every developer machine.
How do I obfuscate a .NET app in GitHub Actions?
Publish your app, install the Nebula.NET CLI on the runner, run nebula --config nebula.config.json (with the license from a GitHub secret), then upload the protected output as your release artifact.
Should I obfuscate on developer machines or only in CI?
Only in CI. Developers build unobfuscated, fully debuggable binaries locally; the build server produces the single hardened artifact you ship — which keeps debugging clean and limits licensing to the runner.
Will obfuscation break my app in the pipeline?
It can if something resolved by name at runtime (reflection, serialization, DI, XAML) is renamed. Use a tool that detects and preserves those, obfuscate before signing, and run your tests against the protected build.
Do I need a Nebula.NET license on every developer's machine to obfuscate in CI?
No. With build-server obfuscation you license only the CI runner(s). An offline signed license file is ideal for ephemeral runners — it needs no online seat activation — and you point NEBULA_LICENSE at the file.
Top comments (0)