-
Notifications
You must be signed in to change notification settings - Fork 2
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
Add bundle endpoint client #2
Changes from 1 commit
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 |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package main | ||
|
||
import ( | ||
"context" | ||
"crypto/x509" | ||
"fmt" | ||
"time" | ||
|
||
"github.com/sirupsen/logrus" | ||
"github.com/spiffe/spire/pkg/common/pemutil" | ||
"github.com/spiffe/spire/pkg/server/bundle/client" | ||
|
||
corev1 "k8s.io/api/core/v1" | ||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" | ||
"k8s.io/client-go/kubernetes" | ||
"k8s.io/client-go/rest" | ||
) | ||
|
||
type BundleEndpointClientConfig struct { | ||
TrustDomain string | ||
EndpointAddress string | ||
EndpointSpiffeID string | ||
|
||
Namespace string | ||
ConfigMapName string | ||
ConfigMapKey string | ||
|
||
Log logrus.FieldLogger | ||
} | ||
|
||
type BundleEndpointClient struct { | ||
cfg *BundleEndpointClientConfig | ||
kubeClient *kubernetes.Clientset | ||
} | ||
|
||
func StartBundleEndpointClient(ctx context.Context, cfg *BundleEndpointClientConfig) error { | ||
kubeClient, err := newKubeClient() | ||
if err != nil { | ||
return err | ||
} | ||
|
||
b := &BundleEndpointClient{ | ||
cfg: cfg, | ||
kubeClient: kubeClient, | ||
} | ||
|
||
go b.start(ctx) | ||
|
||
return nil | ||
} | ||
|
||
func (b *BundleEndpointClient) start(ctx context.Context) { | ||
pollInterval := 5 * time.Minute | ||
retryInterval := 5 * time.Second | ||
|
||
var failing bool | ||
ticker := time.NewTicker(pollInterval) | ||
for { | ||
select { | ||
case <-ticker.C: | ||
ok := b.trySync(ctx) | ||
|
||
// Manipulate ticker frequency based on state changes | ||
// between success and failure | ||
if !ok && !failing { | ||
failing = true | ||
ticker = time.NewTicker(retryInterval) | ||
} else if ok && failing { | ||
failing = false | ||
ticker = time.NewTicker(pollInterval) | ||
} | ||
case <-ctx.Done(): | ||
return | ||
} | ||
} | ||
} | ||
|
||
func (b *BundleEndpointClient) trySync(ctx context.Context) bool { | ||
roots, err := b.getEndpointRoots(ctx) | ||
if err != nil { | ||
b.cfg.Log.Errorf("Could not retrieve root CAs to validate bundle endpoint for %v: %v", b.cfg.TrustDomain, err) | ||
return false | ||
} | ||
|
||
currentRoots, err := b.callBundleEndpoint(ctx, roots) | ||
if err != nil { | ||
b.cfg.Log.Errorf("Could not retrieve current root CAs from bundle endpoint for %v: %v", b.cfg.TrustDomain, err) | ||
return false | ||
} | ||
|
||
err = b.updateRoots(ctx, roots, currentRoots) | ||
if err != nil { | ||
b.cfg.Log.Errorf("Could not persist root CA update for %v: %v", b.cfg.TrustDomain, err) | ||
return false | ||
} | ||
|
||
return true | ||
} | ||
|
||
func (b *BundleEndpointClient) getEndpointRoots(ctx context.Context) ([]*x509.Certificate, error) { | ||
configMap, err := b.getConfigMap(ctx, b.cfg.Namespace, b.cfg.ConfigMapName) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
roots, err := pemutil.ParseCertificates([]byte(configMap.Data[b.cfg.ConfigMapKey])) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if len(roots) == 0 { | ||
return nil, fmt.Errorf("no certs found") | ||
} | ||
|
||
return roots, nil | ||
} | ||
|
||
func (b *BundleEndpointClient) callBundleEndpoint(ctx context.Context, roots []*x509.Certificate) ([]*x509.Certificate, error) { | ||
clientConfig := client.ClientConfig{ | ||
TrustDomain: b.cfg.TrustDomain, | ||
EndpointAddress: b.cfg.EndpointAddress, | ||
EndpointSpiffeID: b.cfg.EndpointSpiffeID, | ||
RootCAs: roots, | ||
} | ||
client := client.NewClient(clientConfig) | ||
|
||
bundle, err := client.FetchBundle(ctx) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return bundle.RootCAs(), nil | ||
} | ||
|
||
func (b *BundleEndpointClient) updateRoots(ctx context.Context, roots, currentRoots []*x509.Certificate) error { | ||
// TODO: Check if we need to actually update anything | ||
|
||
configMap, err := b.getConfigMap(ctx, b.cfg.Namespace, b.cfg.ConfigMapName) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
pemBytes := pemutil.EncodeCertificates(currentRoots) | ||
configMap.Data[b.cfg.ConfigMapKey] = string(pemBytes) | ||
|
||
return b.updateConfigMap(ctx, b.cfg.Namespace, configMap) | ||
} | ||
|
||
func (b *BundleEndpointClient) getConfigMap(ctx context.Context, ns, name string) (*corev1.ConfigMap, error) { | ||
return b.kubeClient.CoreV1().ConfigMaps(ns).Get(name, metav1.GetOptions{}) | ||
} | ||
|
||
func (b *BundleEndpointClient) updateConfigMap(ctx context.Context, ns string, configMap *corev1.ConfigMap) error { | ||
_, err := b.kubeClient.CoreV1().ConfigMaps(ns).Update(configMap) | ||
return err | ||
} | ||
|
||
func newKubeClient() (*kubernetes.Clientset, error) { | ||
c, err := rest.InClusterConfig() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return kubernetes.NewForConfig(c) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,14 @@ var ( | |
leafCertPath = flag.String("leafCertPath", "/etc/server/cert-chain.pem", "The leaf certificate to use for serving TLS") | ||
leafKeyPath = flag.String("leafKeyPath", "/etc/server/key.pem", "The private key of the leaf certificate to serve TLS with") | ||
|
||
peerTrustDomainName = flag.String("peerTrustDomain", "spiffe://cluster-2", "The trust domain name to federate with") | ||
peerEndpointAddress = flag.String("peerEndpointAddress", "35.193.205.112", "The address of the remote trust domain's bundle endpoint") | ||
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. Let's use a dedicated IP "240.0.0.10" for now. In the cluster, we create a service entry with a virtual IP for the external IP. We can use DNS name, but that needs to configure CoreDNS, I just don't want to worry about it for now :) |
||
peerSpiffeID = flag.String("peerSpiffeID", "spiffe://cluster-2/spire/server", "The SPIFFE ID of the remote trust domain's bundle endpoint") | ||
|
||
namespace = flag.String("namespace", "istio-system", "The namespace of the config map to keep updated with the peer's CA certificates") | ||
configMapName = flag.String("configMapName", "cluster-2-ca-certs", "The name of the config map to keep updated with the peer's CA certificates") | ||
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. Please use "spiffe-tb-1" for now. |
||
configMapKey = flag.String("configMapKey", "cluster-2-ca-certs", "The key to store the peer's CA certificates under in the configured config map") | ||
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. What about peerTrustDomain? |
||
|
||
logLevel = flag.String("logLevel", "DEBUG", "The level to log at") | ||
) | ||
|
||
|
@@ -38,6 +46,23 @@ func run(ctx context.Context) error { | |
var handler http.Handler = NewHandler(*rootCAPath, log) | ||
handler = logHandler(log, handler) | ||
|
||
clientConfig := &BundleEndpointClientConfig{ | ||
TrustDomain: *peerTrustDomainName, | ||
EndpointAddress: *peerEndpointAddress, | ||
EndpointSpiffeID: *peerSpiffeID, | ||
|
||
Namespace: *namespace, | ||
ConfigMapName: *configMapName, | ||
ConfigMapKey: *configMapKey, | ||
|
||
Log: log, | ||
} | ||
log.Info("Starting SPIFFE bundle endpoint client") | ||
err = StartBundleEndpointClient(ctx, clientConfig) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
log.Info("Starting SPIFFE bundle endpoint server") | ||
return http.ListenAndServeTLS("0.0.0.0:443", *leafCertPath, *leafKeyPath, handler) | ||
} | ||
|
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.
Yes, that would be better, but we don't need it now :)