Skip to content
← Todas las publicaciones
· Delta1 Labs Tutorial.NETCI/CD

Ofuscar .NET en Azure DevOps Pipelines

Un pipeline de YAML de Azure DevOps concreto que ofusca tu aplicación .NET solo en Release, licencia únicamente el agente de compilación y conserva el mapa de símbolos como artefacto.

Si tu equipo distribuye software .NET desde Azure DevOps, la pregunta sobre la protección es en realidad una pregunta sobre el pipeline. No quieres que cada desarrollador ejecute un ofuscador en su portátil, y no quieres tener que acordarte de proteger una compilación a mano. Quieres que el pipeline produzca un artefacto — el que reciben los clientes — ya endurecido, y que todo lo demás se mantenga limpio y depurable. Este artículo es el YAML concreto para hacer eso en Azure Pipelines: ofuscar solo en Release, licenciar únicamente el agente y conservar el mapa de símbolos.

He escrito por separado la versión de GitHub Actions de este flujo de trabajo — los principios son idénticos, así que este se centra en las particularidades de Azure DevOps en lugar de repetir los fundamentos.

Resumen rápido

  • Ofusca en el pipeline, no en las máquinas de desarrollo. Los desarrolladores compilan binarios limpios y depurables; el agente produce el único artefacto endurecido que distribuyes.
  • Solo en Release. Condiciona el paso de ofuscación a la configuración de compilación o a la rama para que las compilaciones de validación se mantengan rápidas.
  • Licencia el agente, no al equipo. Para los agentes alojados por Microsoft (efímeros) usa un archivo de licencia firmado sin conexión desde un secreto; NEBULA_LICENSE es una ruta de archivo, no una clave.
  • El orden importa: publicar → ofuscar → firmar. Firmar primero invalida la firma y los hashes de anti-manipulación.
  • Conserva el mapa .symbols.json como artefacto por versión para poder leer las trazas de pila de producción.

Por qué el pipeline es el lugar correcto

.NET compila a IL, y el IL se decompila de vuelta a C# casi original en segundos — abre cualquiera de tus propias DLL en ILSpy o Glass.NET y lo verás. La ofuscación reescribe ese IL para que resista la lectura y la reconstrucción. Pero dónde lo ejecutas decide si la práctica sobrevive al contacto con un equipo real:

  • En las máquinas de los desarrolladores: todos necesitan la herramienta y una licencia, las compilaciones locales se vuelven difíciles de depurar y alguien acaba distribuyendo una compilación sin proteger por accidente.
  • En el pipeline (recomendado): la ofuscación se ejecuta una vez, en la compilación oficial de Release, en un agente que controlas. Los desarrolladores compilan con normalidad, solo el agente está licenciado y el artefacto publicado siempre está protegido.

Este es el modelo de solo-en-el-servidor-de-compilación, y las fases de Azure DevOps encajan en él con limpieza.

La configuración

Nebula.NET se controla por completo mediante una configuración JSON que lista los ensamblados a proteger y qué hacerles. Añade un pequeño nebula.config.json a tu repositorio:

{
  "inputs": ["publish/MyApp.dll"],
  "outputDirectory": "protected",
  "encryptStrings": true,
  "controlFlow": true,
  "antiTamper": true
}

Lista todos tus propios ensamblados interdependientes juntos en inputs para que el renombrado se mantenga coherente entre ellos. Nebula escribe las copias protegidas, un mapa de renombrado MyApp.symbols.json y un informe en outputDirectory.

El pipeline

Aquí tienes un azure-pipelines.yml de dos fases. La fase Build se ejecuta en cada push y produce una compilación normal, sin ofuscar, contra la que se ejecutan tus pruebas. La fase Release se ejecuta solo para compilaciones etiquetadas/de main y es la que ofusca y publica el artefacto protegido.

trigger:
  branches:
    include: ['main']
  tags:
    include: ['v*']

variables:
  buildConfiguration: 'Release'

stages:
- stage: Build
  jobs:
  - job: build_and_test
    pool:
      vmImage: 'windows-latest'
    steps:
      - task: UseDotNet@2
        inputs:
          version: '8.0.x'
      - script: dotnet build -c $(buildConfiguration)
        displayName: 'Build'
      - script: dotnet test -c $(buildConfiguration)
        displayName: 'Test (unprotected)'

- stage: ReleaseProtected
  # Only protect real releases — not every CI validation build.
  condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/v'))
  jobs:
  - job: publish_protected
    pool:
      vmImage: 'windows-latest'
    steps:
      - task: UseDotNet@2
        inputs:
          version: '8.0.x'

      # 1. Publish the Release output as usual.
      - script: dotnet publish src/MyApp/MyApp.csproj -c $(buildConfiguration) -o publish
        displayName: 'Publish'

      # 2. Make the Nebula.NET CLI available on the agent. The CLI ships inside the
      #    Nebula download (from delta1labs.com/download) — stage your copy and add it to PATH.
      - powershell: |
          Invoke-WebRequest -Uri "$(NEBULA_CLI_URL)" -OutFile nebula.zip
          Expand-Archive nebula.zip -DestinationPath "$(Agent.TempDirectory)/nebula"
          Write-Host "##vso[task.prependpath]$(Agent.TempDirectory)/nebula"
        displayName: 'Install Nebula.NET CLI'

      # 3. Write the offline license file from a secret variable.
      #    NEBULA_LICENSE is a PATH to the file, not a key string.
      - powershell: |
          Set-Content -Path "$(Agent.TempDirectory)/nebula-license.json" -Value "$(NEBULA_LICENSE_FILE)"
        displayName: 'Write license'

      # 4. Obfuscate — Nebula is config-driven (there is no "obfuscate" verb).
      - script: nebula --config nebula.config.json
        displayName: 'Obfuscate (Release only)'
        env:
          NEBULA_LICENSE: '$(Agent.TempDirectory)/nebula-license.json'

      # 5. (If you sign) sign AFTER obfuscation — never before.
      #    - script: signtool sign ... protected/MyApp.dll

      # 6. Publish the protected assemblies as the release artifact.
      - task: PublishPipelineArtifact@1
        inputs:
          targetPath: 'protected'
          artifact: 'MyApp-protected'
        displayName: 'Publish protected artifact'

      # 7. Publish the symbol map SEPARATELY — keep it, never ship it inside the product.
      - task: PublishPipelineArtifact@1
        inputs:
          targetPath: 'protected/MyApp.symbols.json'
          artifact: 'symbol-map'
        displayName: 'Publish symbol map'

Guarda NEBULA_LICENSE_FILE y NEBULA_CLI_URL como variables secretas del pipeline (o en un grupo de variables / Azure Key Vault), no en el YAML.

Licenciar el agente

Hay dos mecanismos, y cuál uses depende del agente:

  • Los agentes alojados por Microsoft son una VM nueva por ejecución, así que usa un archivo de licencia firmado sin conexión: guarda su contenido en una variable secreta, escríbelo en disco en el trabajo y apunta NEBULA_LICENSE a esa ruta (como arriba). No necesita activación en línea, así que nunca consumes un puesto por ejecución. NEBULA_LICENSE es una ruta de archivo — una clave LIC-… ahí no se resolverá.
  • Los agentes autoalojados persisten, así que puedes en su lugar activar una clave en línea una sola vez en la máquina y dejar que cada compilación reutilice ese puesto. No lo hagas en agentes alojados, o cada ejecución efímera parecerá una “máquina nueva” y agotará tus puestos.

Mantener la fase de Release genuinamente solo-de-Release

La condition de la fase ReleaseProtected es la que hace el trabajo de verdad: solo las compilaciones etiquetadas se ofuscan. Eso importa por dos razones. Primera, tus compilaciones de validación de PR y CI se mantienen rápidas y producen binarios depurables contra los que tus pruebas se ejecutan sin proteger — que es donde quieres estar depurando. Segunda, nunca proteges (ni ralentizas) por accidente una compilación del ciclo interno.

Si prefieres ofuscar durante la compilación en lugar de como un paso aparte, la integración de MSBuild de Nebula se ejecuta después de compilar cuando la propiedad NebulaObfuscate está establecida. Condiciónala a una variable del pipeline que solo exista en la fase de Release y tu comando dotnet build no cambia. Ten en cuenta que la integración de compilación es una función licenciada — el paso de CLI de arriba también funciona en la edición gratuita (con límites), la vía de MSBuild requiere una licencia en el agente.

Verifica, y cuida el orden

Nunca confíes en un paso de protección que no hayas inspeccionado. Descarga el artefacto y abre la DLL en ILSpy, dnSpy o Glass.NET: los métodos aplanados deberían mostrar un laberinto while(true){ switch }, las cadenas deberían haber desaparecido y los nombres estar eliminados excepto la API pública y cualquier cosa que necesite la reflexión. Luego ejecuta tu batería de pruebas contra el artefacto protegido, no solo contra el limpio — consulta cómo ofuscar un ensamblado .NET para saber qué comprobar.

Dos reglas de orden evitan la mayoría de los disgustos:

  • Ofusca antes de firmar. La ofuscación reescribe el ensamblado, así que firmar primero invalida tanto la firma como los hashes de integridad de anti-manipulación. El orden es publicar → ofuscar → firmar.
  • Ofusca antes de empaquetar la salida de archivo único/recortada. Protege primero los ensamblados, luego empaqueta, y prueba esa combinación en concreto.

Los límites honestos

Un pipeline hace que la protección sea coherente; no la hace absoluta. Todo lo de aquí es endurecimiento del lado del cliente — eleva el coste de la ingeniería inversa y del parcheo, no hace que tu código sea inquebrantable. La reflexión, la serialización, la DI y XAML resuelven miembros por nombre en tiempo de ejecución, así que un renombrado que afecte a uno de esos es el clásico fallo de “funcionaba en desarrollo, se rompió en producción”; usa una herramienta que los detecte y preserve, y excluye cualquier cosa personalizada. Y la imposición de licencias que tiene que ser fiable pertenece a un servidor que controles, no al cliente por bien protegido que esté.

Leer los fallos de producción

Como renombraste todo, una traza de pila de producción vuelve en galimatías — a menos que hayas conservado el mapa. Por eso el pipeline publica MyApp.symbols.json como su propio artefacto en cada versión. Para convertir una traza renombrada de vuelta a los nombres originales:

nebula deobfuscate --map MyApp.symbols.json --input crash.txt

Trata el mapa como un PDB: archivado por versión, mantenido en privado, nunca distribuido dentro del producto.

Empezar

Consigue la edición gratuita y conecta primero el paso de CLI en un pipeline desechable — se ejecutará de principio a fin, con límites en un par de funciones. Cuando estés listo para distribuir en todo un ensamblado, Nebula.NET te da aplanado del flujo de control, cifrado de cadenas, anti-manipulación y licenciamiento compatible con agentes; los precios tienen las ediciones. Protege el artefacto que distribuye el pipeline, conserva el mapa y deja que los desarrolladores conserven sus compilaciones limpias.

Prueba Nebula.NET

Endurece tu código .NET en minutos — empieza con la edición gratuita.