Files
teleport/lib/kube/proxy/auth.go
T
Zac Bergquist 2ae9d770fc Refactor tctl's dependencies (#22693)
* Move configuration from lib/service to lib/service/servicecfg

The new servicecfg package will hold only configuration for services.
This will allow other packages (like tctl and tsh) to depend on
servicecfg without pulling in all of lib/service (which has a number
of platform-specific details).

This is the first step towards being able to build tctl for Windows.

* Move PAM and BPF config into servicecfg

This breaks a compile-time dependency on BPF/PAM for tctl.
2023-03-09 17:48:36 +00:00

213 lines
8.7 KiB
Go

// Copyright 2021 Gravitational, Inc
//
// 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.
//nolint:goimports // goimports disagree with gci on blank imports
package proxy
import (
"context"
"fmt"
"net"
"net/url"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
authzapi "k8s.io/api/authorization/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
authztypes "k8s.io/client-go/kubernetes/typed/authorization/v1"
// Load kubeconfig auth plugins for gcp and azure.
// Without this, users can't provide a kubeconfig using those.
//
// Note: we don't want to load _all_ plugins. This is a balance between
// support for popular hosting providers and minimizing attack surface.
_ "k8s.io/client-go/plugin/pkg/client/auth/azure"
_ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
"k8s.io/client-go/rest"
"github.com/gravitational/teleport/api/types"
kubeutils "github.com/gravitational/teleport/lib/kube/utils"
"github.com/gravitational/teleport/lib/service/servicecfg"
)
// getKubeDetails fetches the kubernetes API credentials.
//
// There are 2 possible sources of credentials:
// - pod service account credentials: files in hardcoded paths when running
// inside of a k8s pod; this is used when kubeClusterName is set
// - kubeconfig: a file with a set of k8s endpoints and credentials mapped to
// them this is used when kubeconfigPath is set
//
// serviceType changes the loading behavior:
// - LegacyProxyService:
// - if loading from kubeconfig, only "current-context" is returned; the
// returned map key matches tpClusterName
// - if no credentials are loaded, no error is returned
// - permission self-test failures are only logged
//
// - ProxyService:
// - no credentials are loaded and no error is returned
//
// - KubeService:
// - if loading from kubeconfig, all contexts are returned
// - if no credentials are loaded, returns an error
// - permission self-test failures cause an error to be returned
func getKubeDetails(ctx context.Context, log logrus.FieldLogger, tpClusterName, kubeClusterName, kubeconfigPath string, serviceType KubeServiceType, checkImpersonation servicecfg.ImpersonationPermissionsChecker) (map[string]*kubeDetails, error) {
log.
WithField("kubeconfigPath", kubeconfigPath).
WithField("kubeClusterName", kubeClusterName).
WithField("serviceType", serviceType).
Debug("Reading Kubernetes details.")
// Proxy service should never have creds, forwards to kube service
if serviceType == ProxyService {
return map[string]*kubeDetails{}, nil
}
// Load kubeconfig or local pod credentials.
loadAll := serviceType == KubeService
cfg, err := kubeutils.GetKubeConfig(kubeconfigPath, loadAll, kubeClusterName)
if err != nil && !trace.IsNotFound(err) {
return map[string]*kubeDetails{}, trace.Wrap(err)
}
if trace.IsNotFound(err) || len(cfg.Contexts) == 0 {
switch serviceType {
case KubeService:
return nil, trace.BadParameter("no Kubernetes credentials found; Kubernetes_service requires either a valid kubeconfig_file or to run inside of a Kubernetes pod")
case LegacyProxyService:
log.Debugf("Could not load Kubernetes credentials. This proxy will still handle Kubernetes requests for trusted teleport clusters or Kubernetes nodes in this teleport cluster")
}
return map[string]*kubeDetails{}, nil
}
if serviceType == LegacyProxyService {
// Hack for legacy proxy service - register a k8s cluster named after
// the teleport cluster name to route legacy requests.
//
// Also, remove all other contexts. Multiple kubeconfig entries are
// only supported for kubernetes_service.
if currentContext, ok := cfg.Contexts[cfg.CurrentContext]; ok {
cfg.Contexts = map[string]*rest.Config{
tpClusterName: currentContext,
}
} else {
return nil, trace.BadParameter("no Kubernetes current-context found; Kubernetes proxy service requires either a valid kubeconfig_file with a current-context or to run inside of a Kubernetes pod")
}
}
res := make(map[string]*kubeDetails, len(cfg.Contexts))
// Convert kubeconfig contexts into kubeCreds.
for cluster, clientCfg := range cfg.Contexts {
clusterCreds, err := extractKubeCreds(ctx, cluster, clientCfg, log, checkImpersonation)
if err != nil {
log.WithError(err).Warnf("failed to load credentials for cluster %q.", cluster)
continue
}
kubeCluster, err := types.NewKubernetesClusterV3(types.Metadata{
Name: cluster,
}, types.KubernetesClusterSpecV3{})
if err != nil {
log.WithError(err).Warnf("failed to create KubernetesClusterV3 from credentials for cluster %q.", cluster)
continue
}
res[cluster] = &kubeDetails{
kubeCreds: clusterCreds,
// kubeconfig does not allow labels so we don't define static and dynamic labels for the cluster.
// those will be inherited from the service later.
kubeCluster: kubeCluster,
}
}
return res, nil
}
func extractKubeCreds(ctx context.Context, cluster string, clientCfg *rest.Config, log logrus.FieldLogger, checkPermissions servicecfg.ImpersonationPermissionsChecker) (*staticKubeCreds, error) {
log = log.WithField("cluster", cluster)
log.Debug("Checking Kubernetes impersonation permissions.")
client, err := kubernetes.NewForConfig(clientCfg)
if err != nil {
return nil, trace.Wrap(err, "failed to generate Kubernetes client for cluster %q", cluster)
}
// For each loaded cluster, check impersonation permissions. This
// check only logs when permissions are not configured, but does not fail startup.
if err := checkPermissions(ctx, cluster, client.AuthorizationV1().SelfSubjectAccessReviews()); err != nil {
log.WithError(err).Warning("Failed to test the necessary Kubernetes permissions. The target Kubernetes cluster may be down or have misconfigured RBAC. This teleport instance will still handle Kubernetes requests towards this Kubernetes cluster.")
} else {
log.Debug("Have all necessary Kubernetes impersonation permissions.")
}
targetAddr, err := parseKubeHost(clientCfg.Host)
if err != nil {
return nil, trace.Wrap(err)
}
// tlsConfig can be nil and still no error is returned.
// This happens when no `certificate-authority-data` is provided in kubeconfig because one is expected to use
// the the system default CA pool.
tlsConfig, err := rest.TLSConfigFor(clientCfg)
if err != nil {
return nil, trace.Wrap(err, "failed to generate TLS config from kubeconfig: %v", err)
}
transportConfig, err := clientCfg.TransportConfig()
if err != nil {
return nil, trace.Wrap(err, "failed to generate transport config from kubeconfig: %v", err)
}
log.Debug("Initialized Kubernetes credentials")
return &staticKubeCreds{
tlsConfig: tlsConfig,
transportConfig: transportConfig,
targetAddr: targetAddr,
kubeClient: client,
clientRestCfg: clientCfg,
}, nil
}
// parseKubeHost parses and formats kubernetes hostname
// to host:port format, if no port it set,
// it assumes default HTTPS port
func parseKubeHost(host string) (string, error) {
u, err := url.Parse(host)
if err != nil {
return "", trace.Wrap(err, "failed to parse Kubernetes host: %v", err)
}
if _, _, err := net.SplitHostPort(u.Host); err != nil {
// add default HTTPS port
return fmt.Sprintf("%v:443", u.Host), nil
}
return u.Host, nil
}
func checkImpersonationPermissions(ctx context.Context, cluster string, sarClient authztypes.SelfSubjectAccessReviewInterface) error {
for _, resource := range []string{"users", "groups", "serviceaccounts"} {
resp, err := sarClient.Create(ctx, &authzapi.SelfSubjectAccessReview{
Spec: authzapi.SelfSubjectAccessReviewSpec{
ResourceAttributes: &authzapi.ResourceAttributes{
Verb: "impersonate",
Resource: resource,
},
},
}, metav1.CreateOptions{})
if err != nil {
return trace.Wrap(err, "failed to verify impersonation permissions for Kubernetes: %v; this may be due to missing the SelfSubjectAccessReview permission on the ClusterRole used by the proxy; please make sure that proxy has all the necessary permissions: https://goteleport.com/teleport/docs/kubernetes-ssh/#impersonation", err)
}
if !resp.Status.Allowed {
return trace.AccessDenied("proxy can't impersonate Kubernetes %s at the cluster level; please make sure that proxy has all the necessary permissions: https://goteleport.com/teleport/docs/kubernetes-ssh/#impersonation", resource)
}
}
return nil
}