Skip to content
← All posts
· Delta1 Labs Tutorial.NETCI/CD

How to Obfuscate a .NET App in GitHub Actions (CI/CD)

Obfuscate your .NET app automatically in GitHub Actions: a step-by-step CI/CD workflow that protects your Release build on the runner, licenses only the build server, and keeps developer builds clean.

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 inputs so renaming stays consistent across them — see multiple assemblies. The obfuscated copies, the .symbols.json map, and a report are written to outputDirectory.

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

Licensing the runner: two ways (pick by runner type)

There are two Nebula licensing mechanisms, and which you use depends on whether your runner is ephemeral or persistent:

  • GitHub-hosted (ephemeral) runners — a fresh VM every run. Use an offline signed license file (as in the workflow above): store the file’s contents in a secret, write it to disk in the job, and point NEBULA_LICENSE at that file path. It needs no online activation, so you never burn a seat per run. NEBULA_LICENSE is a file path, not a key — a LIC-… string there won’t resolve.

  • Self-hosted / persistent runners — the same machine each time. You can instead activate your online key once:

    - name: Activate license
      run: nebula register --key ${{ secrets.NEBULA_KEY }}

    This uses one seat for that machine and every build reuses it — but don’t do this on ephemeral runners, or each run activates a “new machine” and exhausts your seats.

Store either secret under Settings → Secrets and variables → Actions. 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, licenseFile in the config, or the NebulaLicenseFile MSBuild 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.

Try Nebula.NET

Harden your .NET code in minutes — start with the free edition.