-
Notifications
You must be signed in to change notification settings - Fork 3
Add support for spiffe.io/helper-jwt-audience and spiffe.io/helper-jwt-filename annotations #84
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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"` | ||
| } | ||
|
|
||
|
|
@@ -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), | ||
|
|
@@ -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)) | ||
| } | ||
|
|
||
| hclBytes := hclFile.Bytes() | ||
| hclString := string(hclBytes) | ||
|
|
||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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), | ||
|
|
||
| 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 | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The manual construction of HCL objects using
cty.Valueandcty.ObjectValforjwt_svidsis effective but verbose and tightly coupled to theSPIFFEHelperJWTConfigstruct's fields. If theSPIFFEHelperJWTConfigstruct 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 avoidingnullvalues, it's a pattern that can become brittle.There was a problem hiding this comment.
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.