Skip to content

Add optional built-in HTTPS/TLS - #144

Merged
LarsLaskowski merged 3 commits into
mainfrom
claude/issue-41-8wklad
Aug 28, 2026
Merged

Add optional built-in HTTPS/TLS#144
LarsLaskowski merged 3 commits into
mainfrom
claude/issue-41-8wklad

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Aug 28, 2026

Copy link
Copy Markdown
Owner

📖 Description

README/SECURITY.md previously assumed a reverse proxy (nginx, Caddy, ...) terminates TLS. For a simple single-Pi setup, the server can now terminate TLS itself: setting both tls_cert and tls_key starts it with (*http.Server).ListenAndServeTLS instead of plain ListenAndServe. Plain HTTP remains the default when both are left empty.

This is an additive feature, not a breaking change.

🎫 Issues

Closes #41

👩‍💻 Reviewer Notes

  • internal/config/config.go: new TLSCertFile/TLSKeyFile fields (tls_cert/tls_key YAML keys) and a Validate() check rejecting the case where only one is set.
  • internal/httpapi/server.go: Config gained matching TLSCertFile/TLSKeyFile fields; ListenAndServe picks ListenAndServeTLS when both are set, plain ListenAndServe otherwise.
  • cmd/pimonitor/main.go: wires the resolved config values into httpapi.Config.
  • packaging/pimonitor.example.yaml and SECURITY.md document the new option.

Smoke test: run with tls_cert/tls_key pointing at a self-signed cert (e.g. via openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 1 -subj /CN=localhost) and confirm curl -k https://localhost:8080/healthz works, while leaving both unset still serves plain HTTP as before.

📑 Test Plan

  • internal/config/config_test.go: TestValidate_TLS covers the three acceptance-criteria branches (both set → valid, neither set → valid, only one set → error), plus two new rows in TestValidate_RejectsBadValues. TestLoad_TLSFromYAML / TestLoad_TLSOnlyCertIsRejected cover YAML wiring end-to-end through Load.
  • internal/httpapi/server_test.go (new): TestListenAndServe_TLSConfigured generates a throwaway self-signed cert, starts the server with ListenAndServe, and confirms a tls.Dial handshake succeeds while a plain HTTP request to the same address is rejected. TestListenAndServe_PlainHTTPByDefault confirms the unset case still serves plain HTTP.
  • go build ./..., go vet ./..., go test ./... -race -cover, and gofmt -l . all pass locally.
  • golangci-lint run could not be run in this environment: the installed golangci-lint binary (built with go1.25) refuses to load the project's go 1.26.7 go.mod (pre-existing environment/toolchain mismatch, unrelated to this change) — please run it in CI or a matching local toolchain.

✅ Checklist

General

  • I have added/updated tests for my changes (go test ./... -race -cover passes locally).
  • go vet ./... and golangci-lint run are clean. (go vet clean; golangci-lint could not be run locally, see Test Plan above)
  • I have tested my changes.
  • I have read the CONTRIBUTING documentation and followed the project's code style guidelines.
  • I have updated ARCHITECTURE.md if this changes a documented design decision. (config/HTTP-layer addition only, no architectural change)

REST API / configuration / packaging

  • I have updated docs/API.md to reflect a REST API change. (no API shape change, per issue scope)
  • No breaking change to /api/v1/... response shapes, or a new API version (/api/v2/...) was introduced instead.
  • I have updated README.md / packaging/pimonitor.example.yaml to reflect a new or changed configuration option.
  • I have updated packaging/install.sh or the systemd units if this changes installation/packaging, and kept the unprivileged/privileged service split intact (see SECURITY.md). (no packaging changes needed; cert/key file permissions are documented in the example config)

⏭ Next Steps

None.

claude added 2 commits August 28, 2026 15:35
README/SECURITY.md previously assumed a reverse proxy terminates TLS.
For a simple single-Pi setup, the server can now terminate TLS itself:
setting both tls_cert and tls_key makes it listen with
(*http.Server).ListenAndServeTLS instead of plain ListenAndServe.
Plain HTTP remains the default when both are left empty; setting only
one is rejected by config validation rather than silently falling back.

Closes #41
SonarCloud's quality gate flagged coverage on new code below the 80%
threshold, and CI's golangci-lint failed on unchecked error returns in
the new TLS server test.

- Factor the httpapi.Config construction in run() into a serverConfig
  helper (mirroring the existing clientConfig), so the TLSCertFile/
  TLSKeyFile wiring is unit-testable instead of only reachable through
  the untested run() function.
- Silence errcheck on intentionally-ignored Close()/Shutdown() calls in
  internal/httpapi/server_test.go, matching the _ = ... convention
  already used elsewhere in the codebase.

@LarsLaskowski LarsLaskowski left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Reviewed against the project checklist (error handling, resource cleanup, command-execution safety, privilege separation, /api/v1 stability, test coverage, dependencies). Verified locally on 6a5a86b: go build ./..., go vet ./... clean; go test ./... -race -cover passes (internal/httpapi 98.6%, internal/config 96.0%).

No blocking issues. /api/v1/... response shapes are untouched, no new dependency, no new exec.Command or root-only file dependency, and the both-or-neither Validate() rule is covered by table-driven tests plus end-to-end Load cases.

Two non-blocking findings inline: a diagnosability/flakiness problem in the new server_test.go, and two operational gaps the example-config comment should cover (privileged-port binding under the hardened unit, and certificate renewal requiring a restart).

Comment thread internal/httpapi/server_test.go
Comment thread packaging/pimonitor.example.yaml
…LS ops gotchas

- server_test.go: thread the ListenAndServe error channel through
  waitUntilUp so a startup failure (bad cert pair, lost port race) fails
  the test immediately with the real cause instead of a generic "did not
  come up in time" timeout after 2s of polling. Add a package comment
  explaining why this file binds a real socket instead of following the
  httptest convention docs/TESTS.md describes for internal/httpapi.
- pimonitor.example.yaml: note that tls_cert/tls_key requires a restart
  to pick up a renewed certificate, and that a privileged listen_addr
  (e.g. ":443") needs AmbientCapabilities=CAP_NET_BIND_SERVICE since the
  service otherwise has no capability to bind it.
@sonarqubecloud

Copy link
Copy Markdown

@LarsLaskowski
LarsLaskowski merged commit b0306f7 into main Aug 28, 2026
8 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-41-8wklad branch August 28, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

E1: Optional built-in HTTPS / TLS

2 participants