Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/schemas/gcp/gkeautopilotresource.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@
"enable": {
"type": "boolean"
},
"hstsValue": {
"type": "string"
},
"image": {
"type": "string"
},
Expand Down
21 changes: 21 additions & 0 deletions osv-scanner.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# OSV-Scanner ignore list. Mirrors the `not_affected` statements in
# `vex/openvex.json` for the benefit of OpenSSF Scorecard's Vulnerabilities
# check, which queries osv.dev directly and doesn't read VEX. The VEX file
# remains the source-of-truth justification; this file is a re-statement
# in a format the scanner understands.
#
# Add an entry here ONLY after the corresponding `not_affected` statement
# lands in vex/openvex.json (review by code owners under the default
# `*` CODEOWNERS rule). Suppressing a real vulnerability here without VEX
# is a regression — track upstream-fix availability in the VEX
# impact_statement, and remove from BOTH files when fixable.
#
# Reference: https://google.github.io/osv-scanner/configuration/

[[IgnoredVulns]]
id = "GO-2022-0635"
reason = "VEX not_affected: github.com/aws/aws-sdk-go is required transitively by github.com/pulumi/pulumi/pkg/v3/operations; the s3crypto subpackage is never imported by this codebase nor any reachable code path. govulncheck -mode=source confirms 0 reachable vulnerabilities. Full justification in vex/openvex.json."

[[IgnoredVulns]]
id = "GO-2022-0646"
reason = "VEX not_affected: same s3crypto unreachability as GO-2022-0635 — different advisory ID against the same subpackage. govulncheck confirms zero reachable calls to the affected symbols. Full justification in vex/openvex.json."
5 changes: 5 additions & 0 deletions pkg/clouds/k8s/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@ type CaddyConfig struct {
// "Local" preserves the client source IP (required for correct X-Forwarded-For from Cloudflare).
// "Cluster" (default) SNATs source IP to a node IP, losing the direct client IP.
ExternalTrafficPolicy *string `json:"externalTrafficPolicy,omitempty" yaml:"externalTrafficPolicy,omitempty"`
// HSTSValue overrides the Strict-Transport-Security value emitted by
// the embedded `(hsts)` snippet. Applies to every site this Caddy
// aggregator serves; there is no per-stack override. Unset or empty
// uses the snippet's default (`max-age=31536000; includeSubDomains; preload`).
HSTSValue *string `json:"hstsValue,omitempty" yaml:"hstsValue,omitempty"`
}

type DisruptionBudget struct {
Expand Down
32 changes: 5 additions & 27 deletions pkg/clouds/pulumi/docker/build_and_push.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,33 +253,11 @@ func executeSecurityOperations(ctx *sdk.Context, stack api.Stack, dockerImage *d
return fmt.Sprintf("sh %s", shellQuote(path))
}).(sdk.StringOutput),

// Disable the default behaviour of passing the previous run's
// stdout/stderr back as PULUMI_COMMAND_STDOUT / PULUMI_COMMAND_STDERR
// env vars on Update. pulumi-command's
// `run()` builds envp with:
//
// if in.AddPreviousOutputInEnv == nil || *in.AddPreviousOutputInEnv {
// if out.Stdout != "" {
// cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s",
// util.PulumiCommandStdout, out.Stdout))
// }
// ...
// }
//
// The security-report script writes the full rendered vulnerability
// table to stdout for inline build-log visibility. On a chrome-base-
// derived image that's a 150-200 KB markdown table (thousands of CVE
// rows). pulumi-command captures that into the resource's `Stdout`
// output, which is then echoed back as a SINGLE envp entry on the
// next Update. Kernel ARG_MAX (~128 KB on Linux) covers argv+envp
// combined, so the next deploy fails at `fork/exec /bin/sh: argument
// list too long` — even though our argv is now ~80 bytes thanks to
// tempfile staging.
//
// Our security scripts don't read PULUMI_COMMAND_STDOUT; turning
// this off is purely a no-op for the script's behaviour. The
// stdout is still captured into state for `pulumi stack output` and
// downstream resource references, just not re-injected into envp.
// Suppress pulumi-command's default Stdout-into-envp re-injection on
// Update: the security-report script writes a ~150-200 KB CVE table
// to stdout, which combined with argv exhausts kernel ARG_MAX. Our
// scripts don't read PULUMI_COMMAND_STDOUT, so this is a no-op for
// behaviour; stdout still lands in state for downstream refs.
AddPreviousOutputInEnv: sdk.Bool(false),
}, sdk.DependsOn(reportDeps))
if err != nil {
Expand Down
16 changes: 16 additions & 0 deletions pkg/clouds/pulumi/kubernetes/caddy.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,18 @@ func isPulumiOutputSet(output sdk.StringOutput) bool {
return output.ElementType() != nil
}

// caddyHSTSEnv returns env-var additions overriding the `(hsts)` snippet's
// header. Empty string is treated as unset — Caddy's `os.LookupEnv` would
// otherwise treat a present-but-empty var as "found" and ship a broken
// `Strict-Transport-Security ""`.
func caddyHSTSEnv(cfg *k8s.CaddyConfig) map[string]string {
v := lo.FromPtr(cfg).HSTSValue
if v == nil || *v == "" {
return nil
}
return map[string]string{"HSTS_VALUE": *v}
}

func CaddyResource(ctx *sdk.Context, stack api.Stack, input api.ResourceInput, params pApi.ProvisionParams) (*api.ResourceOutput, error) {
if input.Descriptor.Type != k8s.ResourceTypeCaddy {
return nil, errors.Errorf("unsupported caddy type %q", input.Descriptor.Type)
Expand Down Expand Up @@ -311,6 +323,10 @@ func DeployCaddyService(ctx *sdk.Context, caddy CaddyDeployment, input api.Resou
TextVolumes: caddyVolumes,
}

for k, v := range caddyHSTSEnv(caddy.CaddyConfig) {
deploymentConfig.StackConfig.Env[k] = v
}

// Prepare secret environment variables (e.g., GOOGLE_APPLICATION_CREDENTIALS for GCP)
secretEnvs := make(map[string]string)
if len(caddy.SecretEnvs) > 0 {
Expand Down
62 changes: 62 additions & 0 deletions pkg/clouds/pulumi/kubernetes/caddy_hsts_value_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package kubernetes

import (
"reflect"
"testing"

. "github.com/onsi/gomega"
"github.com/samber/lo"

"github.com/simple-container-com/api/pkg/clouds/k8s"
)

func TestCaddyfileEmbedHSTSPlaceholder(t *testing.T) {
RegisterTestingT(t)
content, err := Caddyconfig.ReadFile("embed/caddy/Caddyfile")
Expect(err).ToNot(HaveOccurred())
Expect(string(content)).To(ContainSubstring(`{$HSTS_VALUE:max-age=31536000; includeSubDomains; preload}`))
}

func TestCaddyConfig_HSTSValue_FieldShape(t *testing.T) {
RegisterTestingT(t)
ft, ok := reflect.TypeOf(k8s.CaddyConfig{}).FieldByName("HSTSValue")
Expect(ok).To(BeTrue())
Expect(ft.Type.String()).To(Equal("*string"))
Expect(ft.Tag.Get("json")).To(Equal("hstsValue,omitempty"))
Expect(ft.Tag.Get("yaml")).To(Equal("hstsValue,omitempty"))
}

func TestCaddyHSTSEnv_WiringContract(t *testing.T) {
RegisterTestingT(t)

tests := []struct {
name string
cfg *k8s.CaddyConfig
want map[string]string
}{
{name: "nil_config", cfg: nil, want: nil},
{name: "unset_field", cfg: &k8s.CaddyConfig{}, want: nil},
{name: "empty_string_treated_as_unset", cfg: &k8s.CaddyConfig{HSTSValue: lo.ToPtr("")}, want: nil},
{
name: "explicit_value_overrides",
cfg: &k8s.CaddyConfig{HSTSValue: lo.ToPtr("max-age=31536000; includeSubDomains")},
want: map[string]string{"HSTS_VALUE": "max-age=31536000; includeSubDomains"},
},
{
name: "max_age_zero_to_disable",
cfg: &k8s.CaddyConfig{HSTSValue: lo.ToPtr("max-age=0")},
want: map[string]string{"HSTS_VALUE": "max-age=0"},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := caddyHSTSEnv(tt.cfg)
if tt.want == nil {
Expect(got).To(BeEmpty())
} else {
Expect(got).To(Equal(tt.want))
}
})
}
}
3 changes: 2 additions & 1 deletion pkg/clouds/pulumi/kubernetes/embed/caddy/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
}

(hsts) {
# Default preserved byte-for-byte; override via CaddyConfig.HSTSValue.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Strict-Transport-Security "{$HSTS_VALUE:max-age=31536000; includeSubDomains; preload}"
}
@httpReq {
header X-Forwarded-Proto http
Expand Down
Loading