github.com/cilium/cilium@v1.16.2/operator/auth/spire/client.go (about)

     1  // SPDX-License-Identifier: Apache-2.0
     2  // Copyright Authors of Cilium
     3  
     4  package spire
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net"
    10  	"strings"
    11  	"time"
    12  
    13  	"github.com/cilium/hive/cell"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/spf13/pflag"
    16  	"github.com/spiffe/go-spiffe/v2/spiffeid"
    17  	"github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig"
    18  	"github.com/spiffe/go-spiffe/v2/workloadapi"
    19  	entryv1 "github.com/spiffe/spire-api-sdk/proto/spire/api/server/entry/v1"
    20  	"github.com/spiffe/spire-api-sdk/proto/spire/api/types"
    21  	"google.golang.org/grpc"
    22  	"google.golang.org/grpc/credentials"
    23  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    24  
    25  	"github.com/cilium/cilium/operator/auth/identity"
    26  	"github.com/cilium/cilium/pkg/backoff"
    27  	k8sClient "github.com/cilium/cilium/pkg/k8s/client"
    28  	"github.com/cilium/cilium/pkg/lock"
    29  	"github.com/cilium/cilium/pkg/logging/logfields"
    30  )
    31  
    32  const (
    33  	notFoundError   = "NotFound"
    34  	defaultParentID = "/cilium-operator"
    35  	pathPrefix      = "/identity"
    36  )
    37  
    38  var defaultSelectors = []*types.Selector{
    39  	{
    40  		Type:  "cilium",
    41  		Value: "mutual-auth",
    42  	},
    43  }
    44  
    45  // Cell is the cell for the SPIRE client.
    46  var Cell = cell.Module(
    47  	"spire-client",
    48  	"Spire Server API Client",
    49  	cell.Config(ClientConfig{}),
    50  	cell.Provide(NewClient),
    51  )
    52  
    53  var FakeCellClient = cell.Module(
    54  	"fake-spire-client",
    55  	"Fake Spire Server API Client",
    56  	cell.Config(ClientConfig{}),
    57  	cell.Provide(NewFakeClient),
    58  )
    59  
    60  // ClientConfig contains the configuration for the SPIRE client.
    61  type ClientConfig struct {
    62  	MutualAuthEnabled            bool          `mapstructure:"mesh-auth-mutual-enabled"`
    63  	SpireAgentSocketPath         string        `mapstructure:"mesh-auth-spire-agent-socket"`
    64  	SpireServerAddress           string        `mapstructure:"mesh-auth-spire-server-address"`
    65  	SpireServerConnectionTimeout time.Duration `mapstructure:"mesh-auth-spire-server-connection-timeout"`
    66  	SpiffeTrustDomain            string        `mapstructure:"mesh-auth-spiffe-trust-domain"`
    67  }
    68  
    69  // Flags adds the flags used by ClientConfig.
    70  func (cfg ClientConfig) Flags(flags *pflag.FlagSet) {
    71  	flags.BoolVar(&cfg.MutualAuthEnabled,
    72  		"mesh-auth-mutual-enabled",
    73  		false,
    74  		"The flag to enable mutual authentication for the SPIRE server (beta).")
    75  	flags.StringVar(&cfg.SpireAgentSocketPath,
    76  		"mesh-auth-spire-agent-socket",
    77  		"/run/spire/sockets/agent/agent.sock",
    78  		"The path for the SPIRE admin agent Unix socket.")
    79  	flags.StringVar(&cfg.SpireServerAddress,
    80  		"mesh-auth-spire-server-address",
    81  		"spire-server.spire.svc:8081",
    82  		"SPIRE server endpoint.")
    83  	flags.DurationVar(&cfg.SpireServerConnectionTimeout,
    84  		"mesh-auth-spire-server-connection-timeout",
    85  		10*time.Second,
    86  		"SPIRE server connection timeout.")
    87  	flags.StringVar(&cfg.SpiffeTrustDomain,
    88  		"mesh-auth-spiffe-trust-domain",
    89  		"spiffe.cilium",
    90  		"The trust domain for the SPIFFE identity.")
    91  }
    92  
    93  type params struct {
    94  	cell.In
    95  
    96  	K8sClient k8sClient.Clientset
    97  }
    98  
    99  type Client struct {
   100  	cfg        ClientConfig
   101  	log        logrus.FieldLogger
   102  	entry      entryv1.EntryClient
   103  	entryMutex lock.RWMutex
   104  	k8sClient  k8sClient.Clientset
   105  }
   106  
   107  // NewClient creates a new SPIRE client.
   108  // If the mutual authentication is not enabled, it returns a noop client.
   109  func NewClient(params params, lc cell.Lifecycle, cfg ClientConfig, log logrus.FieldLogger) identity.Provider {
   110  	if !cfg.MutualAuthEnabled {
   111  		return &noopClient{}
   112  	}
   113  	client := &Client{
   114  		k8sClient: params.K8sClient,
   115  		cfg:       cfg,
   116  		log:       log.WithField(logfields.LogSubsys, "spire-client"),
   117  	}
   118  
   119  	lc.Append(cell.Hook{
   120  		OnStart: client.onStart,
   121  		OnStop:  func(_ cell.HookContext) error { return nil },
   122  	})
   123  	return client
   124  }
   125  
   126  func (c *Client) onStart(_ cell.HookContext) error {
   127  	go func() {
   128  		c.log.Info("Initializing SPIRE client")
   129  		attempts := 0
   130  		backoffTime := backoff.Exponential{Min: 100 * time.Millisecond, Max: 10 * time.Second}
   131  		for {
   132  			attempts++
   133  			conn, err := c.connect(context.Background())
   134  			if err == nil {
   135  				c.entryMutex.Lock()
   136  				c.entry = entryv1.NewEntryClient(conn)
   137  				c.entryMutex.Unlock()
   138  				break
   139  			}
   140  			c.log.WithError(err).Warnf("Unable to connect to SPIRE server, attempt %d", attempts+1)
   141  			time.Sleep(backoffTime.Duration(attempts))
   142  		}
   143  		c.log.Info("Initialized SPIRE client")
   144  	}()
   145  	return nil
   146  }
   147  
   148  func (c *Client) connect(ctx context.Context) (*grpc.ClientConn, error) {
   149  	timeoutCtx, cancelFunc := context.WithTimeout(ctx, c.cfg.SpireServerConnectionTimeout)
   150  	defer cancelFunc()
   151  
   152  	resolvedTarget, err := resolvedK8sService(ctx, c.k8sClient, c.cfg.SpireServerAddress)
   153  	if err != nil {
   154  		c.log.WithError(err).
   155  			WithField(logfields.URL, c.cfg.SpireServerAddress).
   156  			Warning("Unable to resolve SPIRE server address, using original value")
   157  		resolvedTarget = &c.cfg.SpireServerAddress
   158  	}
   159  
   160  	// This is blocking till the cilium-operator is registered in SPIRE.
   161  	source, err := workloadapi.NewX509Source(timeoutCtx,
   162  		workloadapi.WithClientOptions(
   163  			workloadapi.WithAddr(fmt.Sprintf("unix://%s", c.cfg.SpireAgentSocketPath)),
   164  			workloadapi.WithLogger(newSpiffeLogWrapper(c.log)),
   165  		),
   166  	)
   167  	if err != nil {
   168  		return nil, fmt.Errorf("failed to create X509 source: %w", err)
   169  	}
   170  
   171  	trustedDomain, err := spiffeid.TrustDomainFromString(c.cfg.SpiffeTrustDomain)
   172  	if err != nil {
   173  		return nil, fmt.Errorf("failed to parse trust domain: %w", err)
   174  	}
   175  
   176  	tlsConfig := tlsconfig.MTLSClientConfig(source, source, tlsconfig.AuthorizeMemberOf(trustedDomain))
   177  
   178  	c.log.WithFields(logrus.Fields{
   179  		logfields.Address: c.cfg.SpireServerAddress,
   180  		logfields.IPAddr:  resolvedTarget,
   181  	}).Info("Trying to connect to SPIRE server")
   182  	conn, err := grpc.Dial(*resolvedTarget, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
   183  	if err != nil {
   184  		return nil, fmt.Errorf("failed to create connection to SPIRE server: %w", err)
   185  	}
   186  
   187  	c.log.WithFields(logrus.Fields{
   188  		logfields.Address: c.cfg.SpireServerAddress,
   189  		logfields.IPAddr:  resolvedTarget,
   190  	}).Info("Connected to SPIRE server")
   191  	return conn, nil
   192  }
   193  
   194  // Upsert creates or updates the SPIFFE ID for the given ID.
   195  // The SPIFFE ID is in the form of spiffe://<trust-domain>/identity/<id>.
   196  func (c *Client) Upsert(ctx context.Context, id string) error {
   197  	c.entryMutex.RLock()
   198  	defer c.entryMutex.RUnlock()
   199  	if c.entry == nil {
   200  		return fmt.Errorf("unable to connect to SPIRE server %s", c.cfg.SpireServerAddress)
   201  	}
   202  
   203  	entries, err := c.listEntries(ctx, id)
   204  	if err != nil && !strings.Contains(err.Error(), notFoundError) {
   205  		return err
   206  	}
   207  
   208  	desired := []*types.Entry{
   209  		{
   210  			SpiffeId: &types.SPIFFEID{
   211  				TrustDomain: c.cfg.SpiffeTrustDomain,
   212  				Path:        toPath(id),
   213  			},
   214  			ParentId: &types.SPIFFEID{
   215  				TrustDomain: c.cfg.SpiffeTrustDomain,
   216  				Path:        defaultParentID,
   217  			},
   218  			Selectors: defaultSelectors,
   219  		},
   220  	}
   221  
   222  	if entries == nil || len(entries.Entries) == 0 {
   223  		_, err = c.entry.BatchCreateEntry(ctx, &entryv1.BatchCreateEntryRequest{Entries: desired})
   224  		return err
   225  	}
   226  
   227  	_, err = c.entry.BatchUpdateEntry(ctx, &entryv1.BatchUpdateEntryRequest{
   228  		Entries: desired,
   229  	})
   230  	return err
   231  }
   232  
   233  // Delete deletes the SPIFFE ID for the given ID.
   234  // The SPIFFE ID is in the form of spiffe://<trust-domain>/identity/<id>.
   235  func (c *Client) Delete(ctx context.Context, id string) error {
   236  	c.entryMutex.RLock()
   237  	defer c.entryMutex.RUnlock()
   238  	if c.entry == nil {
   239  		return fmt.Errorf("unable to connect to SPIRE server %s", c.cfg.SpireServerAddress)
   240  	}
   241  
   242  	if len(id) == 0 {
   243  		return nil
   244  	}
   245  
   246  	entries, err := c.listEntries(ctx, id)
   247  	if err != nil {
   248  		if strings.Contains(err.Error(), notFoundError) {
   249  			return nil
   250  		}
   251  		return err
   252  	}
   253  	if len(entries.Entries) == 0 {
   254  		return nil
   255  	}
   256  	var ids = make([]string, 0, len(entries.Entries))
   257  	for _, e := range entries.Entries {
   258  		ids = append(ids, e.Id)
   259  	}
   260  
   261  	_, err = c.entry.BatchDeleteEntry(ctx, &entryv1.BatchDeleteEntryRequest{
   262  		Ids: ids,
   263  	})
   264  
   265  	return err
   266  }
   267  
   268  func (c *Client) List(ctx context.Context) ([]string, error) {
   269  	c.entryMutex.RLock()
   270  	defer c.entryMutex.RUnlock()
   271  	entries, err := c.entry.ListEntries(ctx, &entryv1.ListEntriesRequest{
   272  		Filter: &entryv1.ListEntriesRequest_Filter{
   273  			ByParentId: &types.SPIFFEID{
   274  				TrustDomain: c.cfg.SpiffeTrustDomain,
   275  				Path:        defaultParentID,
   276  			},
   277  			BySelectors: &types.SelectorMatch{
   278  				Selectors: defaultSelectors,
   279  				Match:     types.SelectorMatch_MATCH_EXACT,
   280  			},
   281  		},
   282  	})
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	if len(entries.Entries) == 0 {
   287  		return nil, nil
   288  	}
   289  	var ids = make([]string, 0, len(entries.Entries))
   290  	for _, e := range entries.Entries {
   291  		ids = append(ids, e.Id)
   292  	}
   293  	return ids, nil
   294  }
   295  
   296  // listEntries returns the list of entries for the given ID.
   297  // The maximum number of entries returned is 1, so page token can be ignored.
   298  func (c *Client) listEntries(ctx context.Context, id string) (*entryv1.ListEntriesResponse, error) {
   299  	return c.entry.ListEntries(ctx, &entryv1.ListEntriesRequest{
   300  		Filter: &entryv1.ListEntriesRequest_Filter{
   301  			BySpiffeId: &types.SPIFFEID{
   302  				TrustDomain: c.cfg.SpiffeTrustDomain,
   303  				Path:        toPath(id),
   304  			},
   305  			ByParentId: &types.SPIFFEID{
   306  				TrustDomain: c.cfg.SpiffeTrustDomain,
   307  				Path:        defaultParentID,
   308  			},
   309  			BySelectors: &types.SelectorMatch{
   310  				Selectors: defaultSelectors,
   311  				Match:     types.SelectorMatch_MATCH_EXACT,
   312  			},
   313  		},
   314  	})
   315  }
   316  
   317  // resolvedK8sService resolves the given address to the IP address.
   318  // The input must be in the form of <service-name>.<namespace>.svc.*:<port-number>,
   319  // otherwise the original address is returned.
   320  func resolvedK8sService(ctx context.Context, client k8sClient.Clientset, address string) (*string, error) {
   321  	names := strings.Split(address, ".")
   322  	if len(names) < 3 || !strings.HasPrefix(names[2], "svc") {
   323  		return &address, nil
   324  	}
   325  
   326  	// retrieve the service and return its ClusterIP
   327  	svc, err := client.CoreV1().Services(names[1]).Get(ctx, names[0], metav1.GetOptions{})
   328  	if err != nil {
   329  		return nil, err
   330  	}
   331  
   332  	_, port, err := net.SplitHostPort(address)
   333  	if err != nil {
   334  		return nil, err
   335  	}
   336  
   337  	res := net.JoinHostPort(svc.Spec.ClusterIP, port)
   338  	return &res, nil
   339  }
   340  
   341  func toPath(id string) string {
   342  	return fmt.Sprintf("%s/%s", pathPrefix, id)
   343  }