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 }