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
2 changes: 1 addition & 1 deletion .github/workflows/pr_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ jobs:
make load-images
- name: Test image
run: |
docker tag ghcr.io/spiffe/spire-controller-manager:devel ghcr.io/spiffe/spire-controller-manager:nightly
docker tag "ghcr.io/${{ github.repository_owner }}/spire-controller-manager:devel" ghcr.io/spiffe/spire-controller-manager:devel
(cd demo; ./test.sh)

success:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release_build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ on:
push:
tags:
- 'v[0-9].[0-9]+.[0-9]+'
- 'v[0-9].[0-9]+.[0-9]+-edera.[0-9]+'
jobs:
build-image:
runs-on: ubuntu-22.04
Expand Down
9 changes: 9 additions & 0 deletions api/v1alpha1/clusterspiffeid_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,15 @@ type ClusterSPIFFEID struct {
Status ClusterSPIFFEIDStatus `json:"status"`
}

// AdditiveAnnotation set to "true" applies the ID without keeping fallback
// ClusterSPIFFEIDs from applying to the same pods. It is an annotation so that
// the stock CRD, which prunes unknown spec fields, keeps it.
const AdditiveAnnotation = "spire-controller-manager.edera.dev/additive"

func (r *ClusterSPIFFEID) IsAdditive() bool {
return r.Annotations[AdditiveAnnotation] == "true"
}

//+kubebuilder:object:root=true

// ClusterSPIFFEIDList contains a list of ClusterSPIFFEID
Expand Down
8 changes: 8 additions & 0 deletions api/v1alpha1/clusterspiffeid_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,14 @@ func (r *ClusterSPIFFEIDCustomValidator) ValidateDelete(context.Context, *Cluste
}

func (r *ClusterSPIFFEIDCustomValidator) validate(o *ClusterSPIFFEID) (admission.Warnings, error) {
if value, ok := o.Annotations[AdditiveAnnotation]; ok {
if value != "true" && value != "false" {
return nil, fmt.Errorf("%s must be \"true\" or \"false\", not %q", AdditiveAnnotation, value)
}
if value == "true" && o.Spec.Fallback {
return nil, fmt.Errorf("fallback and %s cannot both be set", AdditiveAnnotation)
}
}
_, err := ParseClusterSPIFFEIDSpec(&o.Spec)
return nil, err
}
Expand Down
7 changes: 6 additions & 1 deletion api/v1alpha1/controllermanagerconfig_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,12 @@ type ControllerManagerConfigurationSpec struct {
// +optional
FilterByClassName bool `json:"filterByClassName,omitempty"`

// If specified, uses a different parent id template for linking pods to nodes
// If specified, uses a different parent id template for linking pods to nodes.
// The template can use the pod's fields as well as the node's. The parent
// decides which agent may issue a pod's identity, so use only pod fields
// that cannot change after the pod is admitted, such as the UID,
// namespace, service account, or node selector, and not labels or
// annotations.
// +optional
ParentIDTemplate string `json:"parentIDTemplate,omitempty"`

Expand Down
6 changes: 6 additions & 0 deletions docs/clusterspiffeid-crd.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ The definition can be found [here](../api/v1alpha1/clusterspiffeid_types.go).
| `fallback` | OPTIONAL | Apply this ID only if there are no other matching non fallback ClusterSPIFFEIDs. |
| `className` | OPTIONAL | The class name of the SPIRE controller manager. |

## Annotations

| Annotation | Description |
| ---------- | ----------- |
| `spire-controller-manager.edera.dev/additive` | `"true"` applies this ID without keeping fallback ClusterSPIFFEIDs from applying to the same pods, for an identity that sits beside a workload's own, such as a delegate's. Cannot be combined with `fallback`. |

## ClusterSPIFFEIDStatus

| Field | Description |
Expand Down
5 changes: 2 additions & 3 deletions pkg/spireentry/entries.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func renderPodEntry(spec *spirev1alpha1.ParsedClusterSPIFFEIDSpec, node *corev1.
ClusterDomain: clusterDomain,
NodeMeta: &node.ObjectMeta,
NodeSpec: &node.Spec,
PodMeta: &pod.ObjectMeta,
PodSpec: &pod.Spec,
}

if parentIDTemplate == nil {
Expand All @@ -93,9 +95,6 @@ func renderPodEntry(spec *spirev1alpha1.ParsedClusterSPIFFEIDSpec, node *corev1.
return nil, fmt.Errorf("failed to render parent ID: %w", err)
}

data.PodMeta = &pod.ObjectMeta
data.PodSpec = &pod.Spec

spiffeID, err := renderSPIFFEID(spec.SPIFFEIDTemplate, data, trustDomain)
if err != nil {
return nil, fmt.Errorf("failed to render SPIFFE ID: %w", err)
Expand Down
47 changes: 47 additions & 0 deletions pkg/spireentry/entries_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,50 @@ func TestParentIDTemplateRenderPodEntry(t *testing.T) {

require.Equal(t, entry.ParentID.String(), fmt.Sprintf("spiffe://%s/spire/agent/x509pop/test.example.org", td))
}

func TestParentIDTemplateCanUsePodFields(t *testing.T) {
spec := &spirev1alpha1.ClusterSPIFFEIDSpec{
SPIFFEIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}",
}
node := &corev1.Node{
ObjectMeta: metav1.ObjectMeta{
UID: "node-uid",
Name: "node-a",
},
}
// Pods of a runtime whose agents attest per pod get a parent for each pod,
// and every other pod keeps the node's agent. The branch reads the node
// selector that the runtime class adds at admission, which cannot change
// afterwards.
parentIDTemplate, err := template.New("testParentIDTemplate").Parse(
`{{ if eq (index .PodSpec.NodeSelector "example.org/runtime") "per-pod-agent" }}` +
`spiffe://{{ .TrustDomain }}/spire/agent/per-pod/{{ .NodeMeta.Name }}/pod/{{ .PodMeta.UID }}` +
`{{ else }}spiffe://{{ .TrustDomain }}/spire/agent/k8s_psat/{{ .ClusterName }}/{{ .NodeMeta.UID }}{{ end }}`)
require.NoError(t, err)

parsedSpec, err := spirev1alpha1.ParseClusterSPIFFEIDSpec(spec)
require.NoError(t, err)
td, err := spiffeid.TrustDomainFromString(trustDomain)
require.NoError(t, err)

perPod := &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "namespace",
UID: "pod-uid",
},
Spec: corev1.PodSpec{
ServiceAccountName: "test",
NodeSelector: map[string]string{"example.org/runtime": "per-pod-agent"},
},
}
entry, err := renderPodEntry(parsedSpec, node, perPod, &corev1.EndpointsList{}, td, clusterName, clusterDomain, parentIDTemplate)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("spiffe://%s/spire/agent/per-pod/node-a/pod/pod-uid", td), entry.ParentID.String())

plain := perPod.DeepCopy()
plain.Spec.NodeSelector = nil
entry, err = renderPodEntry(parsedSpec, node, plain, &corev1.EndpointsList{}, td, clusterName, clusterDomain, parentIDTemplate)
require.NoError(t, err)
require.Equal(t, fmt.Sprintf("spiffe://%s/spire/agent/k8s_psat/%s/node-uid", td, clusterName), entry.ParentID.String())
}
104 changes: 104 additions & 0 deletions pkg/spireentry/fallback_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package spireentry

import (
"context"
"strconv"
"testing"

"github.com/spiffe/go-spiffe/v2/spiffeid"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client/fake"

spirev1alpha1 "github.com/spiffe/spire-controller-manager/api/v1alpha1"
)

func TestAdditiveClusterSPIFFEIDKeepsTheFallback(t *testing.T) {
for name, tc := range map[string]struct {
additive bool
wantFallback bool
}{
"additive": {additive: true, wantFallback: true},
"not additive": {additive: false, wantFallback: false},
} {
t.Run(name, func(t *testing.T) {
declared := declaredSPIFFEIDs(t,
clusterSPIFFEID("fallback", "spiffe://{{ .TrustDomain }}/fallback", true, nil),
clusterSPIFFEID("beside", "spiffe://{{ .TrustDomain }}/beside", false,
map[string]string{spirev1alpha1.AdditiveAnnotation: strconv.FormatBool(tc.additive)}),
)
require.Contains(t, declared, "spiffe://example.org/beside")
if tc.wantFallback {
require.Contains(t, declared, "spiffe://example.org/fallback")
} else {
require.NotContains(t, declared, "spiffe://example.org/fallback")
}
})
}
}

func TestAdditiveAnnotationIsValidated(t *testing.T) {
for name, tc := range map[string]struct {
value string
fallback bool
wantErr string
}{
"additive": {value: "true"},
"not additive": {value: "false", fallback: true},
"additive fallback": {value: "true", fallback: true, wantErr: "fallback and"},
"not a boolean": {value: "yes", wantErr: `not "yes"`},
} {
t.Run(name, func(t *testing.T) {
c := clusterSPIFFEID("c", "spiffe://{{ .TrustDomain }}/c", tc.fallback,
map[string]string{spirev1alpha1.AdditiveAnnotation: tc.value})
_, err := (&spirev1alpha1.ClusterSPIFFEIDCustomValidator{}).ValidateCreate(context.Background(), &c.ClusterSPIFFEID)
if tc.wantErr == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.wantErr)
}
})
}
}

func clusterSPIFFEID(name, template string, fallback bool, annotations map[string]string) *ClusterSPIFFEID {
return &ClusterSPIFFEID{ClusterSPIFFEID: spirev1alpha1.ClusterSPIFFEID{
ObjectMeta: metav1.ObjectMeta{Name: name, Annotations: annotations},
Spec: spirev1alpha1.ClusterSPIFFEIDSpec{SPIFFEIDTemplate: template, Fallback: fallback},
}}
}

// declaredSPIFFEIDs renders the given ClusterSPIFFEIDs for one scheduled pod
// and returns the SPIFFE IDs they declare for it.
func declaredSPIFFEIDs(t *testing.T, clusterSPIFFEIDs ...*ClusterSPIFFEID) []string {
t.Helper()
scheme := runtime.NewScheme()
require.NoError(t, corev1.AddToScheme(scheme))
node := &corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "node-a", UID: "node-uid"}}
k8sClient := fake.NewClientBuilder().WithScheme(scheme).WithObjects(
&corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "default"}},
&corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "pod", Namespace: "default", UID: "pod-uid"},
Spec: corev1.PodSpec{NodeName: node.Name},
},
node,
).Build()

reconciler := &entryReconciler{config: ReconcilerConfig{
TrustDomain: spiffeid.RequireTrustDomainFromString("example.org"),
ClusterName: "cluster",
K8sClient: k8sClient,
}}
state := make(entriesState)
reconciler.addClusterSPIFFEIDEntriesState(context.Background(), state, clusterSPIFFEIDs, map[string]*corev1.Node{node.Name: node})

var declared []string
for _, entry := range state {
for _, d := range entry.Declared {
declared = append(declared, d.Entry.SPIFFEID.String())
}
}
return declared
}
2 changes: 1 addition & 1 deletion pkg/spireentry/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ func (r *entryReconciler) addClusterSPIFFEIDEntriesState(ctx context.Context, st
// renderPodEntry will return a nil entry if requisite k8s
// objects disappeared from underneath.
state.AddDeclared(*entry, clusterSPIFFEID)
if !clusterSPIFFEID.Spec.Fallback {
if !clusterSPIFFEID.Spec.Fallback && !clusterSPIFFEID.IsAdditive() {
podsWithNonFallbackApplied[pods[i].UID] = struct{}{}
}
}
Expand Down
Loading