diff --git a/cmd/controller/main.go b/cmd/controller/main.go index 6df7039df79..2279cda444c 100644 --- a/cmd/controller/main.go +++ b/cmd/controller/main.go @@ -60,6 +60,11 @@ func main() { flag.StringVar(&opts.Images.ImageDigestExporterImage, "imagedigest-exporter-image", "", "The container image containing our image digest exporter binary.") flag.StringVar(&opts.Images.WorkingDirInitImage, "workingdirinit-image", "", "The container image containing our working dir init binary.") + flag.StringVar(&opts.SpireConfig.TrustDomain, "spire-trust-domain", "example.org", "Experimental: The SPIRE Trust domain to use.") + flag.StringVar(&opts.SpireConfig.SocketPath, "spire-socket-path", "/spiffe-workload-api/spire-agent.sock", "Experimental: The SPIRE agent socket for SPIFFE workload API.") + flag.StringVar(&opts.SpireConfig.ServerAddr, "spire-server-addr", "spire-server.spire.svc.cluster.local:8081", "Experimental: The SPIRE server address for workload/node registration.") + flag.StringVar(&opts.SpireConfig.NodeAliasPrefix, "spire-node-alias-prefix", "/tekton-node/", "Experimental: The SPIRE node alias prefix to use.") + // This parses flags. cfg := injection.ParseAndGetRESTConfigOrDie() diff --git a/pkg/apis/pipeline/options.go b/pkg/apis/pipeline/options.go index 2e75adca4c1..6c15c86f365 100644 --- a/pkg/apis/pipeline/options.go +++ b/pkg/apis/pipeline/options.go @@ -16,8 +16,13 @@ limitations under the License. package pipeline +import ( + spireconfig "github.com/tektoncd/pipeline/pkg/spire/config" +) + // Options holds options passed to the Tekton Pipeline controllers // typically via command-line flags. type Options struct { - Images Images + Images Images + SpireConfig spireconfig.SpireConfig } diff --git a/pkg/reconciler/taskrun/controller.go b/pkg/reconciler/taskrun/controller.go index 7c58c1caab9..67965f8838e 100644 --- a/pkg/reconciler/taskrun/controller.go +++ b/pkg/reconciler/taskrun/controller.go @@ -62,6 +62,7 @@ func NewController(opts *pipeline.Options, clock clock.Clock) func(context.Conte KubeClientSet: kubeclientset, PipelineClientSet: pipelineclientset, Images: opts.Images, + SpireConfig: opts.SpireConfig, Clock: clock, taskRunLister: taskRunInformer.Lister(), resourceLister: resourceInformer.Lister(), diff --git a/pkg/reconciler/taskrun/taskrun.go b/pkg/reconciler/taskrun/taskrun.go index 038f5392d49..416a5a17767 100644 --- a/pkg/reconciler/taskrun/taskrun.go +++ b/pkg/reconciler/taskrun/taskrun.go @@ -24,17 +24,9 @@ import ( "strings" "go.uber.org/zap" - "google.golang.org/grpc" - "google.golang.org/grpc/codes" - "google.golang.org/grpc/credentials" - - entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" - spiffetypes "github.com/spiffe/spire-api-sdk/proto/spire/api/types" "github.com/ghodss/yaml" "github.com/hashicorp/go-multierror" - "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" - "github.com/spiffe/go-spiffe/v2/workloadapi" "github.com/tektoncd/pipeline/pkg/apis/config" "github.com/tektoncd/pipeline/pkg/apis/pipeline" "github.com/tektoncd/pipeline/pkg/apis/pipeline/pod" @@ -55,6 +47,8 @@ import ( "github.com/tektoncd/pipeline/pkg/reconciler/events/cloudevent" "github.com/tektoncd/pipeline/pkg/reconciler/taskrun/resources" "github.com/tektoncd/pipeline/pkg/reconciler/volumeclaim" + "github.com/tektoncd/pipeline/pkg/spire" + spireconfig "github.com/tektoncd/pipeline/pkg/spire/config" "github.com/tektoncd/pipeline/pkg/taskrunmetrics" _ "github.com/tektoncd/pipeline/pkg/taskrunmetrics/fake" // Make sure the taskrunmetrics are setup "github.com/tektoncd/pipeline/pkg/workspace" @@ -76,6 +70,7 @@ type Reconciler struct { KubeClientSet kubernetes.Interface PipelineClientSet clientset.Interface Images pipeline.Images + SpireConfig spireconfig.SpireConfig Clock clock.Clock // listers index properties about resources @@ -436,8 +431,8 @@ func (c *Reconciler) reconcile(ctx context.Context, tr *v1beta1.TaskRun, rtr *re if podconvert.SidecarsReady(pod.Status) { if config.FromContextOrDefaults(ctx).FeatureFlags.EnableSpire { - logger.Warnf("LUMJJB registering SPIRE entry: %v/%v", pod.Namespace, pod.Name) - spiffeclient, err := NewSpiffeServerApiClient(ctx) + logger.Infof("Registering SPIRE entry: %v/%v", pod.Namespace, pod.Name) + spiffeclient, err := spire.NewSpiffeServerApiClient(ctx, c.SpireConfig) if err != nil { logger.Errorf("Failed to establish client with SPIRE server: %v", err) return err @@ -835,145 +830,3 @@ func willOverwritePodSetAffinity(taskRun *v1beta1.TaskRun) bool { } return taskRun.Annotations[workspace.AnnotationAffinityAssistantName] != "" && podTemplate.Affinity != nil } - -type SpiffeServerApiClient struct { - serverConn *grpc.ClientConn - workloadConn *workloadapi.X509Source - entryClient entryv1.EntryClient -} - -func NewSpiffeServerApiClient(ctx context.Context) (*SpiffeServerApiClient, error) { - // Create X509Source - // TODO(lumjjb) make sock configurable - source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr("unix:///spiffe-workload-api/spire-agent.sock"))) - if err != nil { - return nil, fmt.Errorf("Unable to create X509Source for SPIFFE client: %w", err) - } - - // Create connection - tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeAny()) - conn, err := grpc.DialContext(ctx, "spire-server.spire.svc.cluster.local:8081", grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) - if err != nil { - source.Close() - return nil, fmt.Errorf("Unable to dial SPIRE server: %w", err) - } - - return &SpiffeServerApiClient{ - serverConn: conn, - workloadConn: source, - entryClient: entryv1.NewEntryClient(conn), - }, nil -} - -func (sc *SpiffeServerApiClient) CreateNodeEntry(ctx context.Context, nodeName string) error { - selectors := []*spiffetypes.Selector{ - { - Type: "k8s_psat", - // TODO: set var - Value: "agent_ns:spire", - }, - { - Type: "k8s_psat", - Value: "agent_node_name:" + nodeName, - }, - } - - // TODO(LUMJJB) take in trust domain - entries := []*spiffetypes.Entry{ - { - SpiffeId: &spiffetypes.SPIFFEID{ - TrustDomain: "example.org", - Path: fmt.Sprintf("/tekton-node/%v", nodeName), - }, - ParentId: &spiffetypes.SPIFFEID{ - TrustDomain: "example.org", - Path: "/spire/server", - }, - Selectors: selectors, - }, - } - - req := entryv1.BatchCreateEntryRequest{ - Entries: entries, - } - - resp, err := sc.entryClient.BatchCreateEntry(ctx, &req) - if err != nil { - return err - } - - if len(resp.Results) != 1 { - return fmt.Errorf("Batch create entry failed, malformed response expected 1 result") - } - - res := resp.Results[0] - if codes.Code(res.Status.Code) == codes.AlreadyExists || - codes.Code(res.Status.Code) == codes.OK { - return nil - } - - return fmt.Errorf("Batch create entry failed, code: %v", res.Status.Code) - -} - -func (sc *SpiffeServerApiClient) CreateWorkloadEntry(ctx context.Context, tr *v1beta1.TaskRun, pod *corev1.Pod) error { - // We can potentially add attestation on the container images as well since - // the information is available here. - selectors := []*spiffetypes.Selector{ - { - Type: "k8s", - Value: "pod-uid:" + string(pod.UID), - }, - { - Type: "k8s", - Value: "pod-name:" + pod.Name, - }, - } - - // TODO(LUMJJB) take in trust domain - entries := []*spiffetypes.Entry{ - { - SpiffeId: &spiffetypes.SPIFFEID{ - TrustDomain: "example.org", - Path: fmt.Sprintf("/ns/%v/taskrun/%v", tr.Namespace, tr.Name), - }, - ParentId: &spiffetypes.SPIFFEID{ - TrustDomain: "example.org", - Path: fmt.Sprintf("/tekton-node/%v", pod.Spec.NodeName), - }, - Selectors: selectors, - }, - } - - req := entryv1.BatchCreateEntryRequest{ - Entries: entries, - } - - resp, err := sc.entryClient.BatchCreateEntry(ctx, &req) - if err != nil { - return err - } - - if len(resp.Results) != 1 { - return fmt.Errorf("Batch create entry failed, malformed response expected 1 result") - } - - res := resp.Results[0] - if codes.Code(res.Status.Code) == codes.AlreadyExists || - codes.Code(res.Status.Code) == codes.OK { - return nil - } - - return fmt.Errorf("Batch create entry failed, code: %v", res.Status.Code) -} - -func (sc *SpiffeServerApiClient) Close() { - err := sc.serverConn.Close() - if err != nil { - // Log error - } - err = sc.workloadConn.Close() - if err != nil { - // Log error - } -} diff --git a/pkg/spire/config/config.go b/pkg/spire/config/config.go new file mode 100644 index 00000000000..58e5b1bcced --- /dev/null +++ b/pkg/spire/config/config.go @@ -0,0 +1,64 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package config + +import ( + "fmt" + "sort" + "strings" +) + +// SpireConfig holds the images reference for a number of container images used +// across tektoncd pipelines. +// Example: +// "trustDomain": "example.org", +// "socketPath": "/spiffe-workload-api/spire-agent.sock", +// "serverAddr": "spire-server.spire.svc.cluster.local:8081", +// "nodeAliasPrefix": "/tekton-node/" +type SpireConfig struct { + TrustDomain string + SocketPath string + ServerAddr string + NodeAliasPrefix string +} + +// Validate returns an error if any image is not set. +func (c SpireConfig) Validate() error { + var unset []string + for _, f := range []struct { + v, name string + }{ + {c.TrustDomain, "spire-trust-domain"}, + {c.SocketPath, "spire-socket-path"}, + {c.ServerAddr, "spire-server-addr"}, + {c.NodeAliasPrefix, "spire-node-alias-prefix"}, + } { + if f.v == "" { + unset = append(unset, f.name) + } + } + if len(unset) > 0 { + sort.Strings(unset) + return fmt.Errorf("found unset image flags: %s", unset) + } + + if !strings.HasPrefix(c.NodeAliasPrefix, "/") { + return fmt.Errorf("Spire node alias should start with a /") + } + + return nil +} diff --git a/pkg/spire/spire.go b/pkg/spire/spire.go new file mode 100644 index 00000000000..5c1c932fe30 --- /dev/null +++ b/pkg/spire/spire.go @@ -0,0 +1,174 @@ +/* +Copyright 2019 The Tekton Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package spire + +import ( + "context" + "fmt" + + entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1" + spiffetypes "github.com/spiffe/spire-api-sdk/proto/spire/api/types" + + corev1 "k8s.io/api/core/v1" + + "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" + "github.com/spiffe/go-spiffe/v2/workloadapi" + "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1" + spireconfig "github.com/tektoncd/pipeline/pkg/spire/config" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/credentials" +) + +type SpiffeServerApiClient struct { + serverConn *grpc.ClientConn + workloadConn *workloadapi.X509Source + entryClient entryv1.EntryClient + config spireconfig.SpireConfig +} + +func NewSpiffeServerApiClient(ctx context.Context, c spireconfig.SpireConfig) (*SpiffeServerApiClient, error) { + // Create X509Source + source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClientOptions(workloadapi.WithAddr("unix://"+c.SocketPath))) + if err != nil { + return nil, fmt.Errorf("Unable to create X509Source for SPIFFE client: %w", err) + } + + // Create connection + tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeAny()) + conn, err := grpc.DialContext(ctx, c.ServerAddr, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig))) + if err != nil { + source.Close() + return nil, fmt.Errorf("Unable to dial SPIRE server: %w", err) + } + + return &SpiffeServerApiClient{ + serverConn: conn, + workloadConn: source, + entryClient: entryv1.NewEntryClient(conn), + config: c, + }, nil +} + +func (sc *SpiffeServerApiClient) CreateNodeEntry(ctx context.Context, nodeName string) error { + selectors := []*spiffetypes.Selector{ + { + Type: "k8s_psat", + Value: "agent_ns:spire", + }, + { + Type: "k8s_psat", + Value: "agent_node_name:" + nodeName, + }, + } + + entries := []*spiffetypes.Entry{ + { + SpiffeId: &spiffetypes.SPIFFEID{ + TrustDomain: sc.config.TrustDomain, + Path: fmt.Sprintf("%v%v", sc.config.NodeAliasPrefix, nodeName), + }, + ParentId: &spiffetypes.SPIFFEID{ + TrustDomain: sc.config.TrustDomain, + Path: "/spire/server", + }, + Selectors: selectors, + }, + } + + req := entryv1.BatchCreateEntryRequest{ + Entries: entries, + } + + resp, err := sc.entryClient.BatchCreateEntry(ctx, &req) + if err != nil { + return err + } + + if len(resp.Results) != 1 { + return fmt.Errorf("Batch create entry failed, malformed response expected 1 result") + } + + res := resp.Results[0] + if codes.Code(res.Status.Code) == codes.AlreadyExists || + codes.Code(res.Status.Code) == codes.OK { + return nil + } + + return fmt.Errorf("Batch create entry failed, code: %v", res.Status.Code) +} + +func (sc *SpiffeServerApiClient) CreateWorkloadEntry(ctx context.Context, tr *v1beta1.TaskRun, pod *corev1.Pod) error { + // Note: We can potentially add attestation on the container images as well since + // the information is available here. + selectors := []*spiffetypes.Selector{ + { + Type: "k8s", + Value: "pod-uid:" + string(pod.UID), + }, + { + Type: "k8s", + Value: "pod-name:" + pod.Name, + }, + } + + entries := []*spiffetypes.Entry{ + { + SpiffeId: &spiffetypes.SPIFFEID{ + TrustDomain: sc.config.TrustDomain, + Path: fmt.Sprintf("/ns/%v/taskrun/%v", tr.Namespace, tr.Name), + }, + ParentId: &spiffetypes.SPIFFEID{ + TrustDomain: sc.config.TrustDomain, + Path: fmt.Sprintf("%v%v", sc.config.NodeAliasPrefix, pod.Spec.NodeName), + }, + Selectors: selectors, + }, + } + + req := entryv1.BatchCreateEntryRequest{ + Entries: entries, + } + + resp, err := sc.entryClient.BatchCreateEntry(ctx, &req) + if err != nil { + return err + } + + if len(resp.Results) != 1 { + return fmt.Errorf("Batch create entry failed, malformed response expected 1 result") + } + + res := resp.Results[0] + if codes.Code(res.Status.Code) == codes.AlreadyExists || + codes.Code(res.Status.Code) == codes.OK { + return nil + } + + return fmt.Errorf("Batch create entry failed, code: %v", res.Status.Code) +} + +func (sc *SpiffeServerApiClient) Close() { + err := sc.serverConn.Close() + if err != nil { + // Log error + } + err = sc.workloadConn.Close() + if err != nil { + // Log error + } +}