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/backupsbecomes something likefse7a9pk. Someone reading the binary can still see that a function exists and what it does; they can no longer see that it was calledRestoreFromS3. - 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 reportsunknown. - 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:
- 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;
- builds the obfuscated binary alongside the plain one;
- 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.
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 -mno longer works on an obfuscated binary, so binary-based dependency and vulnerability scanning (govulncheckagainst 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.ymlmove together.