Skip to main content
Version: 2.1.0

Obfuscated release builds

The released hitechcloud and hitechadmin binaries can be built with garble, which rewrites the program's source before it is compiled so that the names in the finished binary no longer mean anything.

This page exists because obfuscation is easy to oversell. What follows is what it actually buys and what it does not.

What it does

  • Names are gone. Package paths, type names, function and method names and struct field names are replaced with short generated ones. internal/modules/backups becomes something like fse7a9pk. Someone reading the binary can still see that a function exists and what it does; they can no longer see that it was called RestoreFromS3.
  • The module manifest is gone. A normal Go binary embeds the full list of modules it was built from, readable with go version -m. An obfuscated one reports unknown.
  • Control flow is harder to follow. Rewritten and inlined code, with no names to anchor on, takes considerably longer to read than the same code with its original identifiers.

Together that turns "open the binary and read the package layout" into "sit down with a disassembler and work it out". It raises the cost. It is a speed bump, not a wall.

What it does not do

  • It does not prevent decompilation. The binary is machine code. Every tool that could disassemble or decompile it before can still do so; the output is just less readable. Nothing here stops anyone determined.
  • It does not stop the program being run, traced or observed. The binary still runs on hardware the customer controls. strace, ltrace, a debugger, a packet capture, /proc, and an eBPF probe all work exactly as they did. Anything the panel sends over the network, writes to disk, or passes to MySQL is observable in full.
  • It does not hide secrets. Anything the program needs at runtime - a key, a token, an API endpoint - is still in the binary and still recoverable. A secret in a binary is a published secret, obfuscated or not.
  • It is not a licence enforcement mechanism. It makes patching the licence check more tedious. It does not make it hard.
  • It does not remove strings. Log messages, SQL, template names and URLs are still there in plain text.

When a release actually ships an obfuscated binary

Obfuscation is a whole-program rewrite, and its failures are quiet rather than loud. The panel renders every page with html/template, which resolves {{ .Title }} by looking up a struct field by name at runtime. Rename the field and the compiler is perfectly happy; the page just stops rendering halfway down. The same applies to anything else that works from names at runtime - encoding/json struct tags, database/sql column scanning, reflection of any kind.

So the release workflow does not simply obfuscate and publish. For each binary it:

  1. runs that module's whole test suite with every package obfuscated, and runs it plain first as a control, so an already-broken suite is not blamed on obfuscation;
  2. builds the obfuscated binary alongside the plain one;
  3. runs the obfuscated binary and checks it starts and reports the version it was stamped with.

Only a binary that passes all three replaces the plain one. If any step fails, the plain build is published and the job output says which step failed and why. A panel that will not start is much worse than a panel someone can read.

Because that decision is made per release and per architecture, the release's own BUILD-PROVENANCE.txt is the authoritative answer, not this page:

hitechcloud-linux-amd64
sha256: 6c2f…
obfuscated: yes
reason: obfuscated build passed the gate and its startup check
go: go1.25.0
garble: v0.15.0
garble-seed: 0zM0+r/+dlXm994JTmp+dg==

BUILD-PROVENANCE.txt is covered by SHA256SUMS like every other release asset.

NOTE

As of version 2.1.1 neither binary passes the gate, so releases ship the plain build. Obfuscating the panel renames the page-data struct fields that html/template looks up by name, and 36 of the panel's 67 test packages fail as a result - every page that renders through a struct comes out truncated. The machinery is in place and will start publishing obfuscated binaries when that is fixed; until then the gate is doing its job by refusing.

Reading a crash report from an obfuscated build

A stack trace from an obfuscated binary names nothing useful on its own. To turn it back into real names you need garble reverse, run against the same source, the same Go version, the same garble version and the same seed that built it. All four are recorded:

  • the Go and garble versions and the seed are in BUILD-PROVENANCE.txt;
  • the source is the git tag the release was cut from;
  • the seed is derived from the release version, so it is reproducible rather than a one-off.

Match the binary on the customer's server to a build by hashing it - sha256sum /usr/local/bin/hitechcloud - and finding that hash in BUILD-PROVENANCE.txt. An obfuscated binary carries no readable build metadata of its own, so its hash is the identifier.

Line numbers are deliberately kept. garble's -tiny mode would strip them and shrink the binary further, at the cost of panics that name no file and no line. That trade is not worth taking on software that runs on other people's servers.

Costs to be aware of

  • go version -m no longer works on an obfuscated binary, so binary-based dependency and vulnerability scanning (govulncheck against the binary) has nothing to read. Scan the source instead.
  • Builds are slower - garble recompiles the standard library and patches the linker.
  • garble is pinned to the Go release. garble v0.15.0 accepts go1.25.x and refuses anything older or newer, so the two version pins in .github/workflows/release.yml move together.