mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 15:08:51 +00:00
fix(agent,service): SEC-002 — validate certificate_id shape + contain key path
Sprint 1 unified-master-audit closure. Pre-fix the agent built its
on-disk key path via:
keyPath := filepath.Join(a.config.KeyDir, job.CertificateID+".key")
migrations/000001_initial_schema.up.sql declares managed_certificates.id
as TEXT PRIMARY KEY with no shape constraint, so a compromised control
plane (or a poisoned database row) could deliver a job whose
certificate_id is '../../etc/passwd', '/absolute/path', a NUL-byte
payload, or a Windows-separator-laden string — driving arbitrary
file write or read on the agent host.
Fix (two ends; both load-bearing):
Server side:
- New internal/validation/certificate_id.go: ValidateCertificateID
pins the canonical TEXT-PK shape (^[A-Za-z0-9._-]{1,128}$, plus
explicit '.'/'..' rejection).
- CertificateService.Create now invokes ValidateCertificateID after
the existing required-fields check; malformed IDs are refused
before persistence or downstream job creation.
Agent side:
- cmd/agent/keymem.go: validateAgentCertID mirrors the server-side
shape regex. safeAgentKeyPath additionally asserts the joined
path is contained within KeyDir via filepath.Rel — even if a
future refactor bypasses the shape check, a path that escapes
KeyDir fails closed.
- poll.go + deploy.go: both filepath.Join call sites routed
through safeAgentKeyPath; rejection surfaces via reportJobStatus
so the control plane sees the failure.
Regression coverage:
- internal/validation/certificate_id_test.go: production shapes
accepted; explicit rejection table for empty, overlong, posix
traversal, absolute, Windows traversal, Windows separator, NUL
byte, newline/tab injection, drive prefix, space, unicode dots.
- cmd/agent/keymem_test.go: validateAgentCertID acceptance +
rejection tables; safeAgentKeyPath happy path + the 8 audit
vectors plus empty-keyDir refusal.
Closes SEC-002.
This commit is contained in:
+18
-3
@@ -11,7 +11,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/certctl-io/certctl/internal/connector/target"
|
||||
@@ -105,8 +104,24 @@ func (a *Agent) executeDeploymentJob(ctx context.Context, job JobItem) {
|
||||
// Split PEM into cert and chain (separated by double newline between PEM blocks)
|
||||
certOnly, chainPEM := splitPEMChain(certPEM)
|
||||
|
||||
// Check for locally-stored private key (agent keygen mode)
|
||||
keyPath := filepath.Join(a.config.KeyDir, job.CertificateID+".key")
|
||||
// Check for locally-stored private key (agent keygen mode).
|
||||
//
|
||||
// SEC-002 closure (Sprint 1, 2026-05-16): safeAgentKeyPath validates
|
||||
// the certificate_id shape AND asserts the joined path is contained
|
||||
// within a.config.KeyDir. A crafted certificate_id (path traversal,
|
||||
// absolute path, NUL byte, Windows separators) fails closed before
|
||||
// any disk I/O. See cmd/agent/keymem.go for the helper.
|
||||
keyPath, kerr := safeAgentKeyPath(a.config.KeyDir, job.CertificateID)
|
||||
if kerr != nil {
|
||||
a.logger.Error("agent key path validation failed for deployment",
|
||||
"job_id", job.ID,
|
||||
"certificate_id", job.CertificateID,
|
||||
"error", kerr)
|
||||
if reportErr := a.reportJobStatus(ctx, job.ID, "Failed", fmt.Sprintf("key path validation failed: %v", kerr)); reportErr != nil {
|
||||
a.logger.Error("failed to report job status to server", "job_id", job.ID, "error", reportErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
var keyPEM string
|
||||
keyData, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user