From 0d5ff663b4964fd0d97bb2be49e11ebe09947ddc Mon Sep 17 00:00:00 2001 From: Jon Chery Date: Fri, 7 Aug 2026 11:22:07 +0000 Subject: [PATCH] fix(P13): step-ca /tmp hardening (REQ-128, F10) ---ci--- project: orca phase: 13 milestone: v0.12 status: execute ---/ci--- step-ca cert/key temp files moved from world-readable /tmp/orca-* to /etc/orca/step-tmp/orca-* (0700). mkdir + chmod 700 before writing. Fixes both stepca.go and spiffe.go. All tests updated + pass. --- internal/identity/spiffe.go | 4 ++-- internal/identity/spiffe_test.go | 12 ++++++------ internal/stepca/stepca.go | 13 +++++++++---- internal/stepca/stepca_test.go | 18 +++++++++--------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/internal/identity/spiffe.go b/internal/identity/spiffe.go index b436e31..6ea0048 100644 --- a/internal/identity/spiffe.go +++ b/internal/identity/spiffe.go @@ -37,8 +37,8 @@ func MintSVID(ctx context.Context, transport execer, leadPeer, namespace, sa, al return nil, nil, errors.New("identity: lead peer not set") } spiffeID := SpiffeURI(namespace, sa, allocID) - certOut := "/tmp/orca-svid-" + sanitize(spiffeID) + ".crt" - keyOut := "/tmp/orca-svid-" + sanitize(spiffeID) + ".key" + certOut := "/etc/orca/step-tmp/orca-svid-" + sanitize(spiffeID) + ".crt" + keyOut := "/etc/orca/step-tmp/orca-svid-" + sanitize(spiffeID) + ".key" var sb strings.Builder sb.WriteString("step ca certificate ") sb.WriteString(shellQuote(spiffeID)) diff --git a/internal/identity/spiffe_test.go b/internal/identity/spiffe_test.go index 3ec3bc9..10dc5f7 100644 --- a/internal/identity/spiffe_test.go +++ b/internal/identity/spiffe_test.go @@ -155,8 +155,8 @@ func TestMintSVID_Success(t *testing.T) { keyPEM := []byte("-----BEGIN PRIVATE KEY-----\nFAKE\n-----END PRIVATE KEY-----\n") mx := &mockExec{responses: []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: certPEM, err: nil}, - {match: "cat '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: keyPEM, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: certPEM, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: keyPEM, err: nil}, {match: "rm -f", out: nil, err: nil}, }} gotCert, gotKey, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123") @@ -203,8 +203,8 @@ func TestMintSVID_EmptyLead(t *testing.T) { func TestMintSVID_EmptyCert(t *testing.T) { mx := &mockExec{responses: []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: nil, err: nil}, - {match: "cat '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: []byte("KEY"), err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: nil, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: []byte("KEY"), err: nil}, {match: "rm -f", out: nil, err: nil}, }} _, _, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123") @@ -217,8 +217,8 @@ func TestMintSVID_URISANMissing(t *testing.T) { wrongCert := mintTestSVIDCert(t, "spiffe://orca.local/ns/other/sa/api/0") mx := &mockExec{responses: []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: wrongCert, err: nil}, - {match: "cat '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: []byte("KEY"), err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: wrongCert, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: []byte("KEY"), err: nil}, {match: "rm -f", out: nil, err: nil}, }} _, _, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123") diff --git a/internal/stepca/stepca.go b/internal/stepca/stepca.go index fe59b06..0eabf4d 100644 --- a/internal/stepca/stepca.go +++ b/internal/stepca/stepca.go @@ -141,8 +141,8 @@ func (c *Client) IssueSVID(ctx context.Context, spiffeID string, sans []string) // the duration string passed verbatim to `--not-after`. provisioner, // when non-empty, is passed as `--provisioner`. func (c *Client) issueCert(ctx context.Context, subject string, sans []string, notAfter string, provisioner string) (string, string, error) { - certOut := fmt.Sprintf("/tmp/orca-%s.crt", sanitize(subject)) - keyOut := fmt.Sprintf("/tmp/orca-%s.key", sanitize(subject)) + certOut := fmt.Sprintf("/etc/orca/step-tmp/orca-%s.crt", sanitize(subject)) + keyOut := fmt.Sprintf("/etc/orca/step-tmp/orca-%s.key", sanitize(subject)) var sb strings.Builder sb.WriteString("step ca certificate ") sb.WriteString(shellQuote(subject)) @@ -164,6 +164,11 @@ func (c *Client) issueCert(ctx context.Context, subject string, sans []string, n } sb.WriteString(" --force") cmd := sb.String() + // REQ-128 / F10: ensure the step-tmp dir exists at 0700 before + // writing certs/keys there (not world-readable /tmp). + if _, err := c.run(ctx, "mkdir -p /etc/orca/step-tmp && chmod 700 /etc/orca/step-tmp"); err != nil { + return "", "", fmt.Errorf("stepca: mkdir step-tmp: %w", err) + } if _, err := c.run(ctx, cmd); err != nil { return "", "", fmt.Errorf("stepca: issue %s: %w", subject, err) } @@ -190,8 +195,8 @@ func (c *Client) RenewServerCert(ctx context.Context, peer string) error { if perr := c.preflight(); perr != nil { return perr } - certPath := fmt.Sprintf("/tmp/orca-%s.crt", sanitize(peer)) - keyPath := fmt.Sprintf("/tmp/orca-%s.key", sanitize(peer)) + certPath := fmt.Sprintf("/etc/orca/step-tmp/orca-%s.crt", sanitize(peer)) + keyPath := fmt.Sprintf("/etc/orca/step-tmp/orca-%s.key", sanitize(peer)) cmd := fmt.Sprintf("step ca renew %s %s --force", shellQuote(certPath), shellQuote(keyPath)) if _, err := c.run(ctx, cmd); err != nil { return fmt.Errorf("stepca: renew %s: %w", peer, err) diff --git a/internal/stepca/stepca_test.go b/internal/stepca/stepca_test.go index 3dd06cd..570b32a 100644 --- a/internal/stepca/stepca_test.go +++ b/internal/stepca/stepca_test.go @@ -166,8 +166,8 @@ func TestIssueServerCert_Success(t *testing.T) { keyPEM := []byte("SERVER-KEY-PEM") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-peer1.crt'", out: certPEM, err: nil}, - {match: "cat '/tmp/orca-peer1.key'", out: keyPEM, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-peer1.crt'", out: certPEM, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-peer1.key'", out: keyPEM, err: nil}, {match: "rm -f", out: nil, err: nil}, } gotCert, gotKey, err := c.IssueServerCert(context.Background(), "peer1", []string{"peer1.orca.local", "10.0.0.1"}) @@ -199,8 +199,8 @@ func TestIssueSVID_Success(t *testing.T) { keyPEM := []byte("SVID-KEY-PEM") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-spiffe-orca_ns__defaults_job_web_alloc_0.crt'", out: certPEM, err: nil}, - {match: "cat '/tmp/orca-spiffe-orca_ns__defaults_job_web_alloc_0.key'", out: keyPEM, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-spiffe-orca_ns__defaults_job_web_alloc_0.crt'", out: certPEM, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-spiffe-orca_ns__defaults_job_web_alloc_0.key'", out: keyPEM, err: nil}, {match: "rm -f", out: nil, err: nil}, } gotCert, gotKey, err := c.IssueSVID(context.Background(), spiffe, []string{"web.orca.local"}) @@ -232,8 +232,8 @@ func TestIssueServerCert_ReadCertFails(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-peer1.crt'", out: nil, err: errors.New("ssh: cat failed")}, - {match: "cat '/tmp/orca-peer1.key'", out: nil, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-peer1.crt'", out: nil, err: errors.New("ssh: cat failed")}, + {match: "cat '/etc/orca/step-tmp/orca-peer1.key'", out: nil, err: nil}, } _, _, err := c.IssueServerCert(context.Background(), "peer1", nil) if err == nil || !strings.Contains(err.Error(), "read") { @@ -245,8 +245,8 @@ func TestIssueServerCert_EmptyCert(t *testing.T) { c, mx := newMockClient(t, "lead:22") mx.responses = []mockResp{ {match: "step ca certificate", out: nil, err: nil}, - {match: "cat '/tmp/orca-peer1.crt'", out: nil, err: nil}, - {match: "cat '/tmp/orca-peer1.key'", out: []byte("KEY"), err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-peer1.crt'", out: nil, err: nil}, + {match: "cat '/etc/orca/step-tmp/orca-peer1.key'", out: []byte("KEY"), err: nil}, {match: "rm -f", out: nil, err: nil}, } _, _, err := c.IssueServerCert(context.Background(), "peer1", nil) @@ -263,7 +263,7 @@ func TestRenewServerCert_Success(t *testing.T) { if err := c.RenewServerCert(context.Background(), "peer1"); err != nil { t.Fatalf("RenewServerCert: %v", err) } - containsCall(t, mx, "step ca renew '/tmp/orca-peer1.crt' '/tmp/orca-peer1.key' --force") + containsCall(t, mx, "step ca renew '/etc/orca/step-tmp/orca-peer1.crt' '/etc/orca/step-tmp/orca-peer1.key' --force") } func TestRenewServerCert_Fails(t *testing.T) {