summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorIrbe Krumina <irbe@tailscale.com>2024-01-15 19:24:43 +0200
committerIrbe Krumina <irbe@tailscale.com>2024-01-15 19:24:43 +0200
commitbb98a50612b3bed05e1572b2fcc0f79dc0086ed2 (patch)
treef7ea38f76befdeef15ea3e5e927278c119ff8b20
parent7100b6e72162d532efd10021e47b710fc10c9b64 (diff)
downloadtailscale-irbekrm/byocerts.tar.xz
tailscale-irbekrm/byocerts.zip
WIP: BYO TLS certs for Ingressirbekrm/byocerts
Signed-off-by: Irbe Krumina <irbe@tailscale.com>
-rw-r--r--cmd/k8s-operator/deploy/chart/templates/proxy-rbac.yaml22
-rw-r--r--cmd/k8s-operator/ingress.go29
-rw-r--r--ipn/ipn_clone.go1
-rw-r--r--ipn/ipn_view.go5
-rw-r--r--ipn/ipnlocal/serve.go26
-rw-r--r--ipn/serve.go7
-rw-r--r--kube/client.go5
7 files changed, 91 insertions, 4 deletions
diff --git a/cmd/k8s-operator/deploy/chart/templates/proxy-rbac.yaml b/cmd/k8s-operator/deploy/chart/templates/proxy-rbac.yaml
index 31a034aaa..ae33019ed 100644
--- a/cmd/k8s-operator/deploy/chart/templates/proxy-rbac.yaml
+++ b/cmd/k8s-operator/deploy/chart/templates/proxy-rbac.yaml
@@ -30,3 +30,25 @@ roleRef:
kind: Role
name: proxies
apiGroup: rbac.authorization.k8s.io
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRole
+metadata:
+ name: proxies-all-secrets
+rules:
+- apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "list"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: ClusterRoleBinding
+metadata:
+ name: proxies-all-secrets
+subjects:
+- kind: ServiceAccount
+ name: proxies
+ namespace: {{ .Release.Namespace }}
+roleRef:
+ kind: ClusterRole
+ name: proxies-all-secrets
+ apiGroup: rbac.authorization.k8s.io
diff --git a/cmd/k8s-operator/ingress.go b/cmd/k8s-operator/ingress.go
index 0c306fc52..ad19932f8 100644
--- a/cmd/k8s-operator/ingress.go
+++ b/cmd/k8s-operator/ingress.go
@@ -129,9 +129,15 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
a.recorder.Event(ing, corev1.EventTypeWarning, "HTTPSNotEnabled", "HTTPS is not enabled on the tailnet; ingress may not work")
}
+ useProvidedCerts := ing.GetAnnotations()["tailscale.com/use-provided-certs"] == "true"
// magic443 is a fake hostname that we can use to tell containerboot to swap
// out with the real hostname once it's known.
const magic443 = "${TS_CERT_DOMAIN}:443"
+ var proxyHost = magic443
+ if useProvidedCerts {
+ proxyHost = fmt.Sprintf("%s:443", ing.Spec.TLS[0].Hosts[0]) // TODO: validate that host is set
+ }
+
sc := &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
@@ -139,18 +145,33 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
},
},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
- magic443: {
+ ipn.HostPort(proxyHost): {
Handlers: map[string]*ipn.HTTPHandler{},
},
},
}
- if opt.Bool(ing.Annotations[AnnotationFunnel]).EqualBool(true) {
+ if useProvidedCerts {
+ logger.Info("use provided certs")
+ // get the external certs annotation
+ secretName := ing.Spec.TLS[0].SecretName
+ if secretName == "" {
+ return fmt.Errorf("MagicDNS disabled, but Ingress does not specify an alternative TLS certs Secret")
+ }
+ // TODO: here maybe verify that the Secret exists?
+ sc.KubeSecretCertStore = &ipn.KubeSecretCertStore{
+ Name: secretName,
+ Namespace: ing.Namespace, // what if default?
+ }
+ } else {
+ logger.Info("don't use provided certs")
+ }
+ if opt.Bool(ing.Annotations[AnnotationFunnel]).EqualBool(true) && !useProvidedCerts {
sc.AllowFunnel = map[ipn.HostPort]bool{
magic443: true,
}
}
- web := sc.Web[magic443]
+ web := sc.Web[ipn.HostPort(proxyHost)]
addIngressBackend := func(b *networkingv1.IngressBackend, path string) {
if b == nil {
return
@@ -194,7 +215,7 @@ func (a *IngressReconciler) maybeProvision(ctx context.Context, logger *zap.Suga
addIngressBackend(ing.Spec.DefaultBackend, "/")
var tlsHost string // hostname or FQDN or empty
- if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 {
+ if ing.Spec.TLS != nil && len(ing.Spec.TLS) > 0 && len(ing.Spec.TLS[0].Hosts) > 0 && !useProvidedCerts {
tlsHost = ing.Spec.TLS[0].Hosts[0]
}
for _, rule := range ing.Spec.Rules {
diff --git a/ipn/ipn_clone.go b/ipn/ipn_clone.go
index 40cc44296..05bbe47d5 100644
--- a/ipn/ipn_clone.go
+++ b/ipn/ipn_clone.go
@@ -96,6 +96,7 @@ var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
+ KubeSecretCertStore *KubeSecretCertStore
}{})
// Clone makes a deep copy of TCPPortHandler.
diff --git a/ipn/ipn_view.go b/ipn/ipn_view.go
index 18436867d..bb4672776 100644
--- a/ipn/ipn_view.go
+++ b/ipn/ipn_view.go
@@ -185,6 +185,10 @@ func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] {
return views.MapOf(v.ж.AllowFunnel)
}
+func (v ServeConfigView) KubeSecretCert() *KubeSecretCertStore {
+ return v.ж.KubeSecretCertStore
+}
+
func (v ServeConfigView) Foreground() views.MapFn[string, *ServeConfig, ServeConfigView] {
return views.MapFnOf(v.ж.Foreground, func(t *ServeConfig) ServeConfigView {
return t.View()
@@ -199,6 +203,7 @@ var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
+ KubeSecretCertStore *KubeSecretCertStore
}{})
// View returns a readonly view of TCPPortHandler.
diff --git a/ipn/ipnlocal/serve.go b/ipn/ipnlocal/serve.go
index c637a09be..58fa3c8d3 100644
--- a/ipn/ipnlocal/serve.go
+++ b/ipn/ipnlocal/serve.go
@@ -28,6 +28,7 @@ import (
"golang.org/x/net/http2"
"tailscale.com/ipn"
+ "tailscale.com/kube"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netutil"
"tailscale.com/syncs"
@@ -875,6 +876,31 @@ func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHe
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
+ if kubeSecret := b.serveConfig.KubeSecretCert(); kubeSecret != nil {
+ // TODO: initiate kube client once, maybe cache the certs somewhere too
+ c, err := kube.New()
+ if err != nil {
+ return nil, fmt.Errorf("error initalizing kube client: %v", err)
+ }
+ c.SetNS(kubeSecret.Namespace)
+ secret, err := c.GetSecret(ctx, kubeSecret.Name)
+ if err != nil {
+ return nil, fmt.Errorf("error getting certs secret: %v", err)
+ }
+ certBytes, ok := secret.Data["tls.crt"]
+ if !ok {
+ return nil, fmt.Errorf("secret does not contain tls.crt")
+ }
+ keyBytes, ok := secret.Data["tls.key"]
+ if !ok {
+ return nil, fmt.Errorf("secret data does not contain tls.key")
+ }
+ cert, err := tls.X509KeyPair(certBytes, keyBytes)
+ if err != nil {
+ return nil, fmt.Errorf("error creating TLS key pair: %w", err)
+ }
+ return &cert, nil
+ }
pair, err := b.GetCertPEM(ctx, hi.ServerName)
if err != nil {
return nil, err
diff --git a/ipn/serve.go b/ipn/serve.go
index 84db09d1d..94bf06b10 100644
--- a/ipn/serve.go
+++ b/ipn/serve.go
@@ -50,6 +50,13 @@ type ServeConfig struct {
// GetServeConfig request and is translated to an If-Match header
// during a SetServeConfig request.
ETag string `json:"-"`
+ // Reference to a Kubernetes Secret that contains TLS certs for serve
+ KubeSecretCertStore *KubeSecretCertStore `json:",omitempty"`
+}
+
+type KubeSecretCertStore struct {
+ Name string `json:",omitempty"`
+ Namespace string `json:",omitempty"`
}
// HostPort is an SNI name and port number, joined by a colon.
diff --git a/kube/client.go b/kube/client.go
index f4befd1c8..3fbf2a8f2 100644
--- a/kube/client.go
+++ b/kube/client.go
@@ -91,6 +91,11 @@ func (c *Client) SetURL(url string) {
c.url = url
}
+// SetNS sets the ns to use to get Secret. This is a temp fix- TODO: do it differently.
+func (c *Client) SetNS(ns string) {
+ c.ns = ns
+}
+
// SetDialer sets the dialer to use when establishing a connection
// to the Kubernetes API server.
func (c *Client) SetDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {