Skip to content

[-] keep the Hetzner API password out of the process list - #425

Open
pashagolub wants to merge 5 commits into
masterfrom
fix/hetzner-credentials-exposure
Open

pashagolub wants to merge 5 commits into
masterfrom
fix/hetzner-credentials-exposure

Conversation

@pashagolub

Copy link
Copy Markdown
Collaborator

The Robot API was queried by shelling out to "curl --ipv4 -u user:password",
so the password stood in the command line of that process for as long as it
ran. /proc//cmdline is readable by every local user, and process
accounting and audit logs pick command lines up as well.

The API is queried with net/http now and the credentials travel in the
Authorization header. The reason given for curl - that selecting IPv4, which
is all the Robot API listens on, is not trivial in Go - no longer holds: the
transport uses a dialer that turns "tcp" into "tcp4". That also removes curl
as a runtime dependency of a component that has to work during a failover, and
the answer of the API is no longer read through a shell.

While at it:

  • the credentials are parsed as key=value instead of by offset. The old code
    needed exactly user="value" and silently dropped the last character of a
    value that was not quoted, which failed later as an unexplained
    authentication error. Spaces around the equals sign, single quotes and the
    long forms username/password are accepted now.

  • a credentials file that is readable by group or others is reported. It is a
    warning, not a refusal, so that an existing installation keeps working.

@pashagolub pashagolub self-assigned this Sep 10, 2026
@coveralls

coveralls commented Sep 10, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 93.007% (+0.9%) from 92.15% — fix/hetzner-credentials-exposure into master

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Moderate issues remain with quote parsing, proxy support, and IPv4 transport coverage.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Replaces curl-based Hetzner API calls with an IPv4-only Go HTTP client and improves credential handling.

Changes:

  • Uses HTTP Basic Authentication.
  • Supports flexible credential formats.
  • Warns about insecure credential-file permissions.
  • Expands API and integration tests.
File summaries
File Summary
README.md Documents credential-file permission guidance.
ipmanager/hetznerConfigurer.go Implements HTTP access, credential parsing, and permission warnings.
ipmanager/hetznerConfigurer_test.go Tests HTTP behavior, parsing, errors, permissions, and edge cases.
Review details

Suppressed comments (2)

ipmanager/hetznerConfigurer.go:68

  • This custom transport leaves Proxy nil, so net/http will bypass HTTPS_PROXY/ALL_PROXY even though the previous curl invocation honored those environment settings. Deployments that reach the Robot API only through an outbound proxy will now fail; preserve the existing behavior by setting Proxy: http.ProxyFromEnvironment alongside the IPv4 dialer.
		Transport: &http.Transport{
			DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {

ipmanager/hetznerConfigurer.go:72

  • The new transport is the part that replaces curl --ipv4, but all HTTP stubs bind to an IPv4 loopback address, so these tests pass even if the IPv4 pinning is removed or broken. Add a transport-level assertion that the dialer receives/uses tcp4 (or an equivalent dual-stack test) so a future change cannot silently make the Robot API resolve over IPv6.
			DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
				if network == "tcp" || network == "tcp6" {
					network = "tcp4"
				}
				return dialer.DialContext(ctx, network, address)
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ipmanager/hetznerConfigurer.go Outdated
if len(line) > 6 {
password = line[6 : len(line)-1]
}
value = strings.Trim(strings.TrimSpace(value), `"'`)
Comment thread ipmanager/hetznerConfigurer_test.go Outdated
pashagolub and others added 5 commits September 14, 2026 17:25
The Robot API was queried by shelling out to "curl --ipv4 -u user:password",
so the password stood in the command line of that process for as long as it
ran. /proc/<pid>/cmdline is readable by every local user, and process
accounting and audit logs pick command lines up as well.

The API is queried with net/http now and the credentials travel in the
Authorization header. The reason given for curl - that selecting IPv4, which
is all the Robot API listens on, is not trivial in Go - no longer holds: the
transport uses a dialer that turns "tcp" into "tcp4". That also removes curl
as a runtime dependency of a component that has to work during a failover, and
the answer of the API is no longer read through a shell.

While at it:

* the credentials are parsed as key=value instead of by offset. The old code
  needed exactly user="value" and silently dropped the last character of a
  value that was not quoted, which failed later as an unexplained
  authentication error. Spaces around the equals sign, single quotes and the
  long forms username/password are accepted now.

* a credentials file that is readable by group or others is reported. It is a
  warning, not a refusal, so that an existing installation keeps working.
The answer of the Robot API was taken apart with unchecked type assertions,
so anything that did not look exactly as expected took the whole process down
- in the middle of a failover, which is the one moment when it has to keep
running. A real error answer of the API reaches this: only one that carries
status, code and message together survived, and "{"error":{"code":...}}"
without a status was enough to panic on a nil interface conversion.

Every field is read with a checked assertion now and an answer that does not
fit is reported as an error, which the callers already handle by keeping the
cached state at unknown. The test that pinned the panic down as expected
behaviour asserts an error instead.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@pashagolub
pashagolub force-pushed the fix/hetzner-credentials-exposure branch from 69fcb95 to 8cbfc71 Compare September 14, 2026 15:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: In progress

Development

Successfully merging this pull request may close these issues.

3 participants