Skip to content
Open
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/cofide/spiffe-enable

go 1.25.5
go 1.25

require (
github.com/evanphx/json-patch v5.9.11+incompatible
Expand Down
10 changes: 7 additions & 3 deletions internal/const/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@ package constants

// Pod annotations
const (
InjectAnnotation = "spiffe.cofide.io/inject"
DebugAnnotation = "spiffe.cofide.io/debug"
EnvoyLogLevelAnnotation = "spiffe.cofide.io/envoy-log-level"
InjectAnnotation = "spiffe.cofide.io/inject"
DebugAnnotation = "spiffe.cofide.io/debug"
EnvoyLogLevelAnnotation = "spiffe.cofide.io/envoy-log-level"
HelperJWTAudienceAnnotation = "spiffe.io/helper-jwt-audience"
HelperJWTFilenameAnnotation = "spiffe.io/helper-jwt-filename"
HelperJWTExtraAudiencesAnnotation = "spiffe.io/helper-jwt-extra-audiences"
HelperJWTSVIDFileModeAnnotation = "spiffe.io/helper-jwt-svid-file-mode"
)

// Components that can be injected
Expand Down
67 changes: 61 additions & 6 deletions internal/helper/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@ import (
constants "github.com/cofide/spiffe-enable/internal/const"
"github.com/cofide/spiffe-enable/internal/workload"
"github.com/hashicorp/hcl/v2/hclwrite"
"github.com/zclconf/go-cty/cty"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/utils/ptr"

"github.com/hashicorp/hcl/v2/gohcl"
)

// Images
var (
SPIFFEHelperImage = "ghcr.io/spiffe/spiffe-helper:0.10.1"
InitHelperImage = "ghcr.io/cofide/spiffe-enable-init:v0.3.0"
SPIFFEHelperImage = "ghcr.io/spiffe/spiffe-helper:0.10.0"
InitHelperImage = "ghcr.io/cofide/spiffe-enable-init:v0.5.2"
)

// Constants
Expand Down Expand Up @@ -58,13 +60,12 @@ type SPIFFEHelperConfig struct {
SVIDBundleFilename string `hcl:"svid_bundle_file_name"`

// JWT configuration
JWTSVIDs []SPIFFEHelperJWTConfig `hcl:"jwt_svids,block"`
JWTBundleFilename string `hcl:"jwt_bundle_file_name"`
JWTBundleFilename string `hcl:"jwt_bundle_file_name"`
}

type SPIFFEHelperJWTConfig struct {
JWTAudience string `hcl:"jwt_audience"`
JWTExtraAudiences []string `hcl:"jwt_extra_audiences"`
JWTExtraAudiences []string `hcl:"jwt_extra_audiences,optional"`
JWTSVIDFilename string `hcl:"jwt_svid_file_name"`
}

Expand All @@ -79,13 +80,38 @@ type SPIFFEHelperConfigParams struct {
AgentAddress string
CertPath string
IncludeIntermediateBundle bool
JWTConfigs []SPIFFEHelperJWTConfig
JWTSVIDFileMode int
}

func jwtSVIDConfigToCtyValue(jwtConfig SPIFFEHelperJWTConfig) cty.Value {
objMap := map[string]cty.Value{
"jwt_audience": cty.StringVal(jwtConfig.JWTAudience),
"jwt_svid_file_name": cty.StringVal(jwtConfig.JWTSVIDFilename),
}

// Only add jwt_extra_audiences if it has values (to avoid `null` in generated HCL).
if len(jwtConfig.JWTExtraAudiences) > 0 {
extraAuds := make([]cty.Value, len(jwtConfig.JWTExtraAudiences))
for j, aud := range jwtConfig.JWTExtraAudiences {
extraAuds[j] = cty.StringVal(aud)
}
objMap["jwt_extra_audiences"] = cty.ListVal(extraAuds)
}

return cty.ObjectVal(objMap)
}

func NewSPIFFEHelper(params SPIFFEHelperConfigParams) (*SPIFFEHelper, error) {
if params.AgentAddress == "" || params.CertPath == "" {
return nil, fmt.Errorf("missing spiffe-helper configuration parameters")
}

jwtSVIDFileMode := params.JWTSVIDFileMode
if jwtSVIDFileMode == 0 {
jwtSVIDFileMode = defaultJWTSVIDFileMode
}

spiffeHelperCfg := &SPIFFEHelperConfig{
CertDir: params.CertPath,
DaemonMode: BoolPtr(true),
Expand All @@ -95,14 +121,33 @@ func NewSPIFFEHelper(params SPIFFEHelperConfigParams) (*SPIFFEHelper, error) {
SVIDFilename: "tls.crt",
SVIDKeyFilename: "tls.key",
SVIDBundleFilename: "ca.pem",
JWTSVIDFileMode: jwtSVIDFileMode,
HealthCheck: SPIFFEHelperHealthConfig{
ListenerEnabled: true,
BindPort: SPIFFEHelperHealthCheckPort,
LivenessPath: SPIFFEHelperHealthCheckLivenessPath,
ReadinessPath: SPIFFEHelperHealthCheckReadinessPath,
},
}

// Marshal to an HCL-formatted string
// Marshal base config to HCL
hclFile := hclwrite.NewEmptyFile()
gohcl.EncodeIntoBody(spiffeHelperCfg, hclFile.Body())

// Only add JWT SVIDs configuration if present
if len(params.JWTConfigs) > 0 {
body := hclFile.Body()

// Build a list of JWT SVID objects
jwtObjects := make([]cty.Value, len(params.JWTConfigs))
for i, jwtConfig := range params.JWTConfigs {
jwtObjects[i] = jwtSVIDConfigToCtyValue(jwtConfig)
}

// Set jwt_svids as a list attribute
body.SetAttributeValue("jwt_svids", cty.ListVal(jwtObjects))
Comment on lines +142 to +148

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The manual construction of HCL objects using cty.Value and cty.ObjectVal for jwt_svids is effective but verbose and tightly coupled to the SPIFFEHelperJWTConfig struct's fields. If the SPIFFEHelperJWTConfig struct changes (e.g., field names, types), this manual construction logic would need to be updated carefully, increasing maintenance burden. While necessary for the specific HCL output format and avoiding null values, it's a pattern that can become brittle.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good callout. We intentionally build jwt_svids via cty because spiffe-helper expects jwt_svids as a list attribute (not blocks) and gohcl was producing either block syntax or null values in some cases.

To reduce brittleness, I refactored the mapping into a single helper (jwtSVIDConfigToCtyValue) so future changes only need updating in one place. We also keep coverage that asserts the generated config contains no null.

}

hclBytes := hclFile.Bytes()
hclString := string(hclBytes)

Expand Down Expand Up @@ -201,6 +246,16 @@ func (h *SPIFFEHelper) GetInitContainer() corev1.Container {
Name: SPIFFEHelperConfigContentEnvVar,
Value: h.Config,
}},
// Some workloads enforce `runAsNonRoot: true` at the Pod level (e.g. cert-manager).
// Ensure our init container complies; it only writes into EmptyDir volumes and does not need root.
SecurityContext: &corev1.SecurityContext{
AllowPrivilegeEscalation: ptr.To(false),
RunAsUser: ptr.To(int64(65532)),
RunAsGroup: ptr.To(int64(65532)),
RunAsNonRoot: ptr.To(true),
Privileged: ptr.To(false),
Capabilities: &corev1.Capabilities{Drop: []corev1.Capability{"all"}},
Comment on lines +252 to +257

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

The SecurityContext for the init container sets AllowPrivilegeEscalation to ptr.To(false) and Privileged to ptr.To(false). It also drops all capabilities. This is excellent for security, ensuring the init container runs with least privilege. Setting RunAsUser and RunAsGroup to 65532 is a good practice for non-root execution, commonly used for unprivileged users in container images.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks — the intent here is to keep the config-writer init container as locked down as possible while still being compatible with pods enforcing runAsNonRoot (e.g. cert-manager).

},
VolumeMounts: []corev1.VolumeMount{
{
Name: SPIFFEHelperConfigVolumeName, MountPath: filepath.Dir(configFilePath),
Expand Down
21 changes: 21 additions & 0 deletions internal/helper/config_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package helper

import (
"strings"
"testing"

"github.com/hashicorp/hcl/v2/hclsimple"
Expand Down Expand Up @@ -31,10 +32,23 @@ func TestNewSPIFFEHelper(t *testing.T) {
"SVIDFilename": `svid_file_name = "tls.crt"`,
"SVIDKeyFilename": `svid_key_file_name = "tls.key"`,
"SVIDBundleFilename": `svid_bundle_file_name = "ca.pem"`,
"JWTSVIDFileMode": `jwt_svid_file_mode = 600`,
"HealthCheckEnabled": `listener_enabled = true`,
},
expectError: false,
},
{
name: "custom jwt svid file mode",
params: SPIFFEHelperConfigParams{
AgentAddress: "/tmp/agent.sock",
CertPath: "/mnt/certs",
JWTSVIDFileMode: 644,
},
expectedHCLSubstrings: map[string]string{
"JWTSVIDFileMode": `jwt_svid_file_mode = 644`,
},
expectError: false,
},
{
name: "with inc intermediate bundle",
params: SPIFFEHelperConfigParams{
Expand Down Expand Up @@ -79,6 +93,7 @@ func TestNewSPIFFEHelper(t *testing.T) {
require.NoError(t, err)
require.NotNil(t, helper)
require.NotEmpty(t, helper.Config)
require.False(t, strings.Contains(helper.Config, "null"), "generated config must not contain `null`: %s", helper.Config)

// Parse the generated HCL string back into the SPIFFEHelperConfig struct
var decodedCfg SPIFFEHelperConfig
Expand All @@ -100,6 +115,12 @@ func TestNewSPIFFEHelper(t *testing.T) {
assert.Equal(t, "tls.key", decodedCfg.SVIDKeyFilename)
assert.Equal(t, "ca.pem", decodedCfg.SVIDBundleFilename)

if tt.params.JWTSVIDFileMode == 0 {
assert.Equal(t, 600, decodedCfg.JWTSVIDFileMode)
} else {
assert.Equal(t, tt.params.JWTSVIDFileMode, decodedCfg.JWTSVIDFileMode)
}

assert.True(t, decodedCfg.HealthCheck.ListenerEnabled)
})
}
Expand Down
94 changes: 94 additions & 0 deletions internal/helper/jwt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package helper

import (
"fmt"
"strconv"
"strings"

constants "github.com/cofide/spiffe-enable/internal/const"
corev1 "k8s.io/api/core/v1"
)

const defaultJWTSVIDFileMode = 600

// ParseJWTConfigFromAnnotations extracts JWT SVID configuration from pod annotations
func ParseJWTConfigFromAnnotations(annotations map[string]string) []SPIFFEHelperJWTConfig {
var jwtConfigs []SPIFFEHelperJWTConfig

audience, hasAudience := annotations[constants.HelperJWTAudienceAnnotation]
filename, hasFilename := annotations[constants.HelperJWTFilenameAnnotation]

// Only create JWT config if audience is specified
if !hasAudience || audience == "" {
return jwtConfigs
}

// Default filename if not specified
if !hasFilename || filename == "" {
filename = "tokens/token"
}

jwtConfig := SPIFFEHelperJWTConfig{
JWTAudience: audience,
JWTSVIDFilename: filename,
// Keep this non-nil so empty values are consistently treated as an empty list
// (and so future encoding paths don't accidentally emit `null`).
JWTExtraAudiences: []string{},
}

// Parse extra audiences if present (comma-separated)
if extraAudiences, hasExtra := annotations[constants.HelperJWTExtraAudiencesAnnotation]; hasExtra && extraAudiences != "" {
rawAudiences := strings.Split(extraAudiences, ",")
audiences := make([]string, 0, len(rawAudiences))
for _, a := range rawAudiences {
a = strings.TrimSpace(a)
if a == "" {
continue
}
audiences = append(audiences, a)
}
jwtConfig.JWTExtraAudiences = audiences
}

jwtConfigs = append(jwtConfigs, jwtConfig)
return jwtConfigs
}

// ParseJWTSVIDFileModeFromAnnotations extracts a numeric file mode for JWT SVID output.
//
// If not set, returns 600 (owner read/write).
func ParseJWTSVIDFileModeFromAnnotations(annotations map[string]string) (int, error) {
raw, ok := annotations[constants.HelperJWTSVIDFileModeAnnotation]
if !ok || strings.TrimSpace(raw) == "" {
return defaultJWTSVIDFileMode, nil
}

mode, err := strconv.Atoi(strings.TrimSpace(raw))
if err != nil {
return 0, fmt.Errorf("invalid %s %q: must be an integer (e.g. 600)", constants.HelperJWTSVIDFileModeAnnotation, raw)
}
if mode <= 0 {
return 0, fmt.Errorf("invalid %s %q: must be a positive integer (e.g. 600)", constants.HelperJWTSVIDFileModeAnnotation, raw)
}
return mode, nil
}

// EnsureCertVolumeMount adds the cert directory volume mount to the container
// if it doesn't already exist
func EnsureCertVolumeMount(container *corev1.Container, certPath string) bool {
volumeMount := corev1.VolumeMount{
Name: constants.SPIFFEEnableCertVolumeName,
MountPath: certPath,
ReadOnly: true,
}

// Check if this volume mount already exists
for _, vm := range container.VolumeMounts {
if vm.Name == constants.SPIFFEEnableCertVolumeName && vm.MountPath == certPath {
return false // Already exists
}
}

container.VolumeMounts = append(container.VolumeMounts, volumeMount)
return true
}
Loading