mirror of
https://github.com/gravitational/teleport.git
synced 2026-11-03 16:14:19 +08:00
This change adds the client side of the auth ceremony for the Oracle join method.
908 lines
32 KiB
Go
908 lines
32 KiB
Go
// Teleport
|
|
// Copyright (C) 2024 Gravitational, Inc.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
package join
|
|
|
|
import (
|
|
"context"
|
|
"crypto"
|
|
"crypto/x509"
|
|
"log/slog"
|
|
"net/http"
|
|
"os"
|
|
"time"
|
|
|
|
"github.com/gravitational/trace"
|
|
"github.com/jonboulle/clockwork"
|
|
"go.opentelemetry.io/otel"
|
|
"golang.org/x/crypto/ssh"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/breaker"
|
|
"github.com/gravitational/teleport/api/client"
|
|
"github.com/gravitational/teleport/api/client/proto"
|
|
"github.com/gravitational/teleport/api/client/webclient"
|
|
"github.com/gravitational/teleport/api/observability/tracing"
|
|
"github.com/gravitational/teleport/api/types"
|
|
"github.com/gravitational/teleport/api/utils/aws"
|
|
"github.com/gravitational/teleport/api/utils/keys"
|
|
"github.com/gravitational/teleport/lib/auth/authclient"
|
|
"github.com/gravitational/teleport/lib/auth/join/iam"
|
|
"github.com/gravitational/teleport/lib/auth/join/oracle"
|
|
"github.com/gravitational/teleport/lib/auth/state"
|
|
"github.com/gravitational/teleport/lib/bitbucket"
|
|
"github.com/gravitational/teleport/lib/circleci"
|
|
proxyinsecureclient "github.com/gravitational/teleport/lib/client/proxy/insecure"
|
|
"github.com/gravitational/teleport/lib/cloud/imds/azure"
|
|
"github.com/gravitational/teleport/lib/cloud/imds/gcp"
|
|
"github.com/gravitational/teleport/lib/cryptosuites"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/githubactions"
|
|
"github.com/gravitational/teleport/lib/gitlab"
|
|
kubetoken "github.com/gravitational/teleport/lib/kube/token"
|
|
"github.com/gravitational/teleport/lib/spacelift"
|
|
"github.com/gravitational/teleport/lib/terraformcloud"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/tpm"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
)
|
|
|
|
var tracer = otel.Tracer("github.com/gravitational/teleport/lib/auth/join")
|
|
|
|
// HostCredentials is an interface for a client that can be used to get host
|
|
// credentials. This interface is needed because lib/client cannot be imported
|
|
// in lib/auth due to circular imports.
|
|
type HostCredentials func(context.Context, string, bool, types.RegisterUsingTokenRequest) (*proto.Certs, error)
|
|
|
|
// AzureParams is the parameters specific to the azure join method.
|
|
type AzureParams struct {
|
|
// ClientID is the client ID of the managed identity for Teleport to assume
|
|
// when authenticating a node.
|
|
ClientID string
|
|
}
|
|
|
|
// RegisterParams specifies parameters
|
|
// for first time register operation with auth server
|
|
type RegisterParams struct {
|
|
// Token is a secure token to join the cluster
|
|
Token string
|
|
// ID is identity ID
|
|
ID state.IdentityID
|
|
// AuthServers is a list of auth servers to dial
|
|
// Ignored if AuthClient is provided.
|
|
AuthServers []utils.NetAddr
|
|
// ProxyServer is a proxy server to dial
|
|
// Ignored if AuthClient is provided.
|
|
ProxyServer utils.NetAddr
|
|
// AdditionalPrincipals is a list of additional principals to dial
|
|
AdditionalPrincipals []string
|
|
// DNSNames is a list of DNS names to add to x509 certificate
|
|
DNSNames []string
|
|
// CipherSuites is a list of cipher suites to use for TLS client connection
|
|
// Ignored if AuthClient is provided.
|
|
CipherSuites []uint16
|
|
// CAPins are the SKPI hashes of the CAs used to verify the Auth Server.
|
|
// Ignored if AuthClient is provided.
|
|
CAPins []string
|
|
// CAPath is the path to the CA file.
|
|
// Ignored if AuthClient is provided.
|
|
CAPath string
|
|
// GetHostCredentials is a client that can be used to register via the
|
|
// proxy web API.
|
|
// Ignored if AuthClient is provided.
|
|
GetHostCredentials HostCredentials
|
|
// Clock specifies the time provider. Will be used to override the time anchor
|
|
// for TLS certificate verification.
|
|
// Defaults to real clock if unspecified
|
|
Clock clockwork.Clock
|
|
// JoinMethod is the joining method used for this register request.
|
|
JoinMethod types.JoinMethod
|
|
// ec2IdentityDocument is used for Simplified Node Joining to prove the
|
|
// identity of a joining EC2 instance.
|
|
ec2IdentityDocument []byte
|
|
// AzureParams is the parameters specific to the azure join method.
|
|
AzureParams AzureParams
|
|
// CircuitBreakerConfig defines how the circuit breaker should behave.
|
|
// Ignored if AuthClient is provided.
|
|
CircuitBreakerConfig breaker.Config
|
|
// FIPS means FedRAMP/FIPS 140-2 compliant configuration was requested.
|
|
// Ignored if AuthClient is provided.
|
|
FIPS bool
|
|
// IDToken is a token retrieved from a workload identity provider for
|
|
// certain join types e.g GitHub, Google.
|
|
IDToken string
|
|
// Expires is an optional field for bots that specifies a time that the
|
|
// certificates that are returned by registering should expire at.
|
|
// It should not be specified for non-bot registrations.
|
|
Expires *time.Time
|
|
// Insecure trusts the certificates from the Auth Server or Proxy during registration without verification.
|
|
// Ignored if AuthClient is provided.
|
|
Insecure bool
|
|
// AuthClient allows an existing client with a connection to the auth
|
|
// server to be used for the registration process. If specified, then the
|
|
// Register method will not attempt to dial, and many other parameters
|
|
// may be ignored.
|
|
AuthClient AuthJoinClient
|
|
// KubernetesReadFileFunc is a function used to read the Kubernetes token
|
|
// from disk. Used in tests, and set to `os.ReadFile` if unset.
|
|
KubernetesReadFileFunc func(name string) ([]byte, error)
|
|
// TerraformCloudAudienceTag is a tag name for the environment variable
|
|
// containing TF Cloud's Workload Identity Token when using Terraform Cloud
|
|
// joining.
|
|
TerraformCloudAudienceTag string
|
|
}
|
|
|
|
func (r *RegisterParams) checkAndSetDefaults() error {
|
|
if r.Clock == nil {
|
|
r.Clock = clockwork.NewRealClock()
|
|
}
|
|
|
|
if r.KubernetesReadFileFunc == nil {
|
|
r.KubernetesReadFileFunc = os.ReadFile
|
|
}
|
|
|
|
if err := r.verifyAuthOrProxyAddress(); err != nil {
|
|
return trace.BadParameter("no auth or proxy servers set")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (r *RegisterParams) verifyAuthOrProxyAddress() error {
|
|
// If AuthClient is provided we do not need addresses to dial with.
|
|
if r.AuthClient != nil {
|
|
return nil
|
|
}
|
|
|
|
haveAuthServers := len(r.AuthServers) > 0
|
|
haveProxyServer := !r.ProxyServer.IsEmpty()
|
|
|
|
if !haveAuthServers && !haveProxyServer {
|
|
return trace.BadParameter("no auth or proxy servers set")
|
|
}
|
|
|
|
if haveAuthServers && haveProxyServer {
|
|
return trace.BadParameter("only one of auth or proxy server should be set")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RegisterResult contains the certificates and the private key generated during
|
|
// the registration process.
|
|
type RegisterResult struct {
|
|
// Certs holds the certificates issued and signed by the Auth server.
|
|
Certs *proto.Certs
|
|
// PrivateKey is the subject key of the certificates in [Certs]. It is
|
|
// generated according to the current signature algorithm suite configured
|
|
// in the cluster.
|
|
PrivateKey crypto.Signer
|
|
}
|
|
|
|
// Register is used to get signed certificates when a node, proxy, or bot is
|
|
// running on a different host than the auth server. This method requires a
|
|
// provision token that will be used to authenticate as an identity that should
|
|
// be allowed to join the cluster.
|
|
func Register(ctx context.Context, params RegisterParams) (result *RegisterResult, err error) {
|
|
ctx, span := tracer.Start(ctx, "Register")
|
|
defer func() { tracing.EndSpan(span, err) }()
|
|
|
|
if err := params.checkAndSetDefaults(); err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
// Read in the token. The token can either be passed in or come from a file
|
|
// on disk.
|
|
token, err := utils.TryReadValueAsFile(params.Token)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// add EC2 Identity Document to params if required for given join method
|
|
switch params.JoinMethod {
|
|
case types.JoinMethodEC2:
|
|
if !aws.IsEC2NodeID(params.ID.HostUUID) {
|
|
return nil, trace.BadParameter(
|
|
`Host ID %q is not valid when using the EC2 join method, `+
|
|
`try removing the "host_uuid" file in your teleport data dir `+
|
|
`(e.g. /var/lib/teleport/host_uuid)`,
|
|
params.ID.HostUUID)
|
|
}
|
|
params.ec2IdentityDocument, err = utils.GetRawEC2IdentityDocument(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodGitHub:
|
|
params.IDToken, err = githubactions.NewIDTokenSource().GetIDToken(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodGitLab:
|
|
params.IDToken, err = gitlab.NewIDTokenSource(os.Getenv).GetIDToken()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodCircleCI:
|
|
params.IDToken, err = circleci.GetIDToken(os.Getenv)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodKubernetes:
|
|
params.IDToken, err = kubetoken.GetIDToken(os.Getenv, params.KubernetesReadFileFunc)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodGCP:
|
|
params.IDToken, err = gcp.GetIDToken(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodSpacelift:
|
|
params.IDToken, err = spacelift.NewIDTokenSource(os.Getenv).GetIDToken()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodTerraformCloud:
|
|
params.IDToken, err = terraformcloud.NewIDTokenSource(params.TerraformCloudAudienceTag, os.Getenv).GetIDToken()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
case types.JoinMethodBitbucket:
|
|
params.IDToken, err = bitbucket.NewIDTokenSource(os.Getenv).GetIDToken()
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
// If an explicit AuthClient has been provided, we want to go straight to
|
|
// using that rather than trying both proxy and auth dialing.
|
|
if params.AuthClient != nil {
|
|
slog.InfoContext(ctx, "Attempting registration with existing auth client.")
|
|
result, err := registerThroughAuthClient(ctx, token, params, params.AuthClient)
|
|
if err != nil {
|
|
slog.ErrorContext(ctx, "Registration with existing auth client failed.", "error", err)
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
slog.InfoContext(ctx, "Successfully registered with existing auth client.")
|
|
return result, nil
|
|
}
|
|
|
|
type registerMethod struct {
|
|
call func(ctx context.Context, token string, params RegisterParams) (*RegisterResult, error)
|
|
desc string
|
|
}
|
|
|
|
registerThroughAuth := registerMethod{registerThroughAuth, "with auth server"}
|
|
registerThroughProxy := registerMethod{registerThroughProxy, "via proxy server"}
|
|
|
|
registerMethods := []registerMethod{registerThroughAuth, registerThroughProxy}
|
|
|
|
if !params.ProxyServer.IsEmpty() {
|
|
slog.DebugContext(ctx, "Registering node to the cluster.", "proxy_server", params.ProxyServer)
|
|
|
|
registerMethods = []registerMethod{registerThroughProxy}
|
|
|
|
if proxyServerIsAuth(params.ProxyServer) {
|
|
slog.DebugContext(ctx, "The specified proxy server appears to be an auth server.")
|
|
}
|
|
} else {
|
|
slog.DebugContext(ctx, "Registering node to the cluster.", "auth_servers", params.AuthServers)
|
|
|
|
if params.GetHostCredentials == nil {
|
|
slog.DebugContext(ctx, "Missing client, it is not possible to register through proxy.")
|
|
registerMethods = []registerMethod{registerThroughAuth}
|
|
} else if authServerIsProxy(params.AuthServers) {
|
|
slog.DebugContext(ctx, "The first specified auth server appears to be a proxy.")
|
|
registerMethods = []registerMethod{registerThroughProxy, registerThroughAuth}
|
|
}
|
|
}
|
|
|
|
var collectedErrs []error
|
|
for _, method := range registerMethods {
|
|
slog.InfoContext(ctx, "Attempting registration.", "method", method.desc)
|
|
result, err := method.call(ctx, token, params)
|
|
if err != nil {
|
|
collectedErrs = append(collectedErrs, err)
|
|
slog.DebugContext(ctx, "Registration failed.", "method", method.desc, "error", err)
|
|
continue
|
|
}
|
|
slog.InfoContext(ctx, "Successfully registered.", "method", method.desc)
|
|
return result, nil
|
|
}
|
|
return nil, trace.NewAggregate(collectedErrs...)
|
|
}
|
|
|
|
// authServerIsProxy returns true if the first specified auth server
|
|
// to register with appears to be a proxy.
|
|
func authServerIsProxy(servers []utils.NetAddr) bool {
|
|
if len(servers) == 0 {
|
|
return false
|
|
}
|
|
port := servers[0].Port(0)
|
|
return port == defaults.HTTPListenPort || port == teleport.StandardHTTPSPort
|
|
}
|
|
|
|
// proxyServerIsAuth returns true if the address given to register with
|
|
// appears to be an auth server.
|
|
func proxyServerIsAuth(server utils.NetAddr) bool {
|
|
port := server.Port(0)
|
|
return port == defaults.AuthListenPort
|
|
}
|
|
|
|
// registerThroughProxy is used to register through the proxy server.
|
|
func registerThroughProxy(
|
|
ctx context.Context,
|
|
token string,
|
|
params RegisterParams,
|
|
) (result *RegisterResult, err error) {
|
|
ctx, span := tracer.Start(ctx, "registerThroughProxy")
|
|
defer func() { tracing.EndSpan(span, err) }()
|
|
|
|
proxyAddr := getHostAddresses(params)[0]
|
|
hostKeys, err := generateHostKeysForProxy(ctx, params.Insecure, proxyAddr)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
var certs *proto.Certs
|
|
switch params.JoinMethod {
|
|
case types.JoinMethodIAM,
|
|
types.JoinMethodAzure,
|
|
types.JoinMethodTPM,
|
|
types.JoinMethodOracle:
|
|
// These join methods require gRPC client
|
|
conn, err := proxyinsecureclient.NewConnection(
|
|
ctx,
|
|
proxyinsecureclient.ConnectionConfig{
|
|
ProxyServer: proxyAddr,
|
|
CipherSuites: params.CipherSuites,
|
|
Clock: params.Clock,
|
|
Insecure: params.Insecure,
|
|
Log: slog.Default(),
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
joinServiceClient := client.NewJoinServiceClient(proto.NewJoinServiceClient(conn))
|
|
switch params.JoinMethod {
|
|
case types.JoinMethodIAM:
|
|
certs, err = registerUsingIAMMethod(ctx, joinServiceClient, token, hostKeys, params)
|
|
case types.JoinMethodAzure:
|
|
certs, err = registerUsingAzureMethod(ctx, joinServiceClient, token, hostKeys, params)
|
|
case types.JoinMethodTPM:
|
|
certs, err = registerUsingTPMMethod(ctx, joinServiceClient, token, hostKeys, params)
|
|
case types.JoinMethodOracle:
|
|
certs, err = registerUsingOracleMethod(ctx, joinServiceClient, token, hostKeys, params)
|
|
default:
|
|
return nil, trace.BadParameter("unhandled join method %q", params.JoinMethod)
|
|
}
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
default:
|
|
// The rest of the join methods use GetHostCredentials function passed
|
|
// through params to call proxy HTTP endpoint.
|
|
var err error
|
|
certs, err = params.GetHostCredentials(ctx,
|
|
proxyAddr,
|
|
params.Insecure,
|
|
*registerUsingTokenRequestForParams(token, hostKeys, params))
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
return &RegisterResult{
|
|
Certs: certs,
|
|
PrivateKey: hostKeys.privateKey,
|
|
}, nil
|
|
}
|
|
|
|
// registerThroughAuth is used to register through the auth server.
|
|
func registerThroughAuth(
|
|
ctx context.Context, token string, params RegisterParams,
|
|
) (result *RegisterResult, err error) {
|
|
ctx, span := tracer.Start(ctx, "registerThroughAuth")
|
|
defer func() { tracing.EndSpan(span, err) }()
|
|
|
|
var client *authclient.Client
|
|
// Build a client for the Auth Server with different certificate validation
|
|
// depending on the configured values for Insecure, CAPins and CAPath.
|
|
switch {
|
|
case params.Insecure:
|
|
slog.WarnContext(ctx, "Insecure mode enabled. Auth Server cert will not be validated and CAPins and CAPath value will be ignored.")
|
|
client, err = insecureRegisterClient(ctx, params)
|
|
case len(params.CAPins) != 0:
|
|
// CAPins takes precedence over CAPath
|
|
client, err = pinRegisterClient(ctx, params)
|
|
case params.CAPath != "":
|
|
client, err = caPathRegisterClient(ctx, params)
|
|
default:
|
|
// We fall back to insecure mode here - this is a little odd but is
|
|
// necessary to preserve the behavior of registration. At a later date,
|
|
// we may consider making this an error asking the user to provide
|
|
// Insecure, CAPins or CAPath.
|
|
client, err = insecureRegisterClient(ctx, params)
|
|
}
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "building auth client")
|
|
}
|
|
defer client.Close()
|
|
|
|
result, err = registerThroughAuthClient(ctx, token, params, client)
|
|
return result, trace.Wrap(err, "registering through auth client")
|
|
}
|
|
|
|
// AuthJoinClient is a client that allows access to the Auth Servers join
|
|
// service and RegisterUsingToken method for the purposes of joining.
|
|
type AuthJoinClient interface {
|
|
joinServiceClient
|
|
RegisterUsingToken(ctx context.Context, req *types.RegisterUsingTokenRequest) (*proto.Certs, error)
|
|
Ping(ctx context.Context) (proto.PingResponse, error)
|
|
}
|
|
|
|
func registerThroughAuthClient(
|
|
ctx context.Context,
|
|
token string,
|
|
params RegisterParams,
|
|
client AuthJoinClient,
|
|
) (result *RegisterResult, err error) {
|
|
hostKeys, err := generateHostKeysForAuth(ctx, client)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "generating host keys")
|
|
}
|
|
|
|
var certs *proto.Certs
|
|
switch params.JoinMethod {
|
|
// IAM and Azure methods use unique gRPC endpoints
|
|
case types.JoinMethodIAM:
|
|
certs, err = registerUsingIAMMethod(ctx, client, token, hostKeys, params)
|
|
case types.JoinMethodAzure:
|
|
certs, err = registerUsingAzureMethod(ctx, client, token, hostKeys, params)
|
|
case types.JoinMethodTPM:
|
|
certs, err = registerUsingTPMMethod(ctx, client, token, hostKeys, params)
|
|
default:
|
|
// non-IAM join methods use HTTP endpoint
|
|
// Get the SSH and X509 certificates for a node.
|
|
certs, err = client.RegisterUsingToken(ctx, registerUsingTokenRequestForParams(token, hostKeys, params))
|
|
}
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "registering with %s method", params.JoinMethod)
|
|
}
|
|
return &RegisterResult{
|
|
Certs: certs,
|
|
PrivateKey: hostKeys.privateKey,
|
|
}, nil
|
|
}
|
|
|
|
func getHostAddresses(params RegisterParams) []string {
|
|
if !params.ProxyServer.IsEmpty() {
|
|
return []string{params.ProxyServer.String()}
|
|
}
|
|
|
|
return utils.NetAddrsToStrings(params.AuthServers)
|
|
}
|
|
|
|
// insecureRegisterClient attempts to connects to the Auth Server using the
|
|
// CA on disk. If no CA is found on disk, Teleport will not verify the Auth
|
|
// Server it is connecting to.
|
|
func insecureRegisterClient(ctx context.Context, params RegisterParams) (*authclient.Client, error) {
|
|
const msg = "Joining cluster without validating the identity of the Auth " +
|
|
"Server. This may open you up to a Man-In-The-Middle (MITM) attack if an " +
|
|
"attacker can gain privileged network access. To remedy this, use the CA pin " +
|
|
"value provided when join token was generated to validate the identity of " +
|
|
"the Auth Server or point to a valid Certificate via the CA Path option."
|
|
slog.WarnContext(ctx, msg)
|
|
|
|
tlsConfig := utils.TLSConfig(params.CipherSuites)
|
|
tlsConfig.Time = params.Clock.Now
|
|
tlsConfig.InsecureSkipVerify = true
|
|
|
|
client, err := authclient.NewClient(client.Config{
|
|
Addrs: getHostAddresses(params),
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: params.CircuitBreakerConfig,
|
|
Context: ctx,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "creating insecure auth client")
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
// pinRegisterClient first connects to the Auth Server using a insecure
|
|
// connection to fetch the root CA. If the root CA matches the provided CA
|
|
// pin, a connection will be re-established and the root CA will be used to
|
|
// validate the certificate presented. If both conditions hold true, then we
|
|
// know we are connecting to the expected Auth Server.
|
|
func pinRegisterClient(
|
|
ctx context.Context, params RegisterParams,
|
|
) (*authclient.Client, error) {
|
|
// Build a insecure client to the Auth Server. This is safe because even if
|
|
// an attacker were to MITM this connection the CA pin will not match below.
|
|
tlsConfig := utils.TLSConfig(params.CipherSuites)
|
|
tlsConfig.InsecureSkipVerify = true
|
|
tlsConfig.Time = params.Clock.Now
|
|
authClient, err := authclient.NewClient(client.Config{
|
|
Addrs: getHostAddresses(params),
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: params.CircuitBreakerConfig,
|
|
Context: ctx,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
defer authClient.Close()
|
|
|
|
// Fetch the root CA from the Auth Server. The NOP role has access to the
|
|
// GetClusterCACert endpoint.
|
|
localCA, err := authClient.GetClusterCACert(ctx)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
certs, err := tlsca.ParseCertificatePEMs(localCA.TLSCA)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// Check that the SPKI pin matches the CA we fetched over a insecure
|
|
// connection. This makes sure the CA fetched over a insecure connection is
|
|
// in-fact the expected CA.
|
|
err = utils.CheckSPKI(params.CAPins, certs)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
for _, cert := range certs {
|
|
// Check that the fetched CA is valid at the current time.
|
|
err = utils.VerifyCertificateExpiry(cert, params.Clock)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
}
|
|
slog.InfoContext(ctx, "Joining remote cluster with CA pin.", "cluster", certs[0].Subject.CommonName)
|
|
|
|
// Create another client, but this time with the CA provided to validate
|
|
// that the Auth Server was issued a certificate by the same CA.
|
|
tlsConfig = utils.TLSConfig(params.CipherSuites)
|
|
tlsConfig.Time = params.Clock.Now
|
|
certPool := x509.NewCertPool()
|
|
for _, cert := range certs {
|
|
certPool.AddCert(cert)
|
|
}
|
|
tlsConfig.RootCAs = certPool
|
|
|
|
authClient, err = authclient.NewClient(client.Config{
|
|
Addrs: getHostAddresses(params),
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: params.CircuitBreakerConfig,
|
|
Context: ctx,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return authClient, nil
|
|
}
|
|
|
|
func caPathRegisterClient(ctx context.Context, params RegisterParams) (*authclient.Client, error) {
|
|
tlsConfig := utils.TLSConfig(params.CipherSuites)
|
|
tlsConfig.Time = params.Clock.Now
|
|
|
|
cert, err := readCA(params.CAPath)
|
|
if err != nil && !trace.IsNotFound(err) {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
// If we're unable to read the file at CAPath, we fall back to insecure
|
|
// registration. This preserves the existing behavior. At a later date,
|
|
// we may wish to consider changing this to return an error - but this is a
|
|
// breaking change.
|
|
if trace.IsNotFound(err) {
|
|
slog.WarnContext(ctx, "Falling back to insecurely joining because a missing or empty CA Path was provided.")
|
|
return insecureRegisterClient(ctx, params)
|
|
}
|
|
|
|
certPool := x509.NewCertPool()
|
|
certPool.AddCert(cert)
|
|
tlsConfig.RootCAs = certPool
|
|
|
|
slog.InfoContext(ctx, "Joining remote cluster, validating connection with certificate on disk.", "cluster", cert.Subject.CommonName)
|
|
|
|
client, err := authclient.NewClient(client.Config{
|
|
Addrs: getHostAddresses(params),
|
|
Credentials: []client.Credentials{
|
|
client.LoadTLS(tlsConfig),
|
|
},
|
|
CircuitBreakerConfig: params.CircuitBreakerConfig,
|
|
Context: ctx,
|
|
})
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
type joinServiceClient interface {
|
|
RegisterUsingIAMMethod(ctx context.Context, challengeResponse client.RegisterIAMChallengeResponseFunc) (*proto.Certs, error)
|
|
RegisterUsingAzureMethod(ctx context.Context, challengeResponse client.RegisterAzureChallengeResponseFunc) (*proto.Certs, error)
|
|
RegisterUsingTPMMethod(
|
|
ctx context.Context,
|
|
initReq *proto.RegisterUsingTPMMethodInitialRequest,
|
|
solveChallenge client.RegisterTPMChallengeResponseFunc,
|
|
) (*proto.Certs, error)
|
|
RegisterUsingOracleMethod(
|
|
ctx context.Context,
|
|
tokenReq *types.RegisterUsingTokenRequest,
|
|
challengeResponse client.RegisterOracleChallengeResponseFunc,
|
|
) (*proto.Certs, error)
|
|
}
|
|
|
|
func registerUsingTokenRequestForParams(token string, hostKeys *newHostKeys, params RegisterParams) *types.RegisterUsingTokenRequest {
|
|
return &types.RegisterUsingTokenRequest{
|
|
Token: token,
|
|
HostID: params.ID.HostUUID,
|
|
NodeName: params.ID.NodeName,
|
|
Role: params.ID.Role,
|
|
AdditionalPrincipals: params.AdditionalPrincipals,
|
|
DNSNames: params.DNSNames,
|
|
PublicTLSKey: hostKeys.tlsPub,
|
|
PublicSSHKey: hostKeys.sshPub,
|
|
EC2IdentityDocument: params.ec2IdentityDocument,
|
|
IDToken: params.IDToken,
|
|
Expires: params.Expires,
|
|
}
|
|
}
|
|
|
|
// registerUsingIAMMethod is used to register using the IAM join method. It is
|
|
// able to register through a proxy or through the auth server directly.
|
|
func registerUsingIAMMethod(
|
|
ctx context.Context, joinServiceClient joinServiceClient, token string, hostKeys *newHostKeys, params RegisterParams,
|
|
) (*proto.Certs, error) {
|
|
slog.InfoContext(ctx, "Attempting to register with IAM method using region STS endpoint.", "role", params.ID.Role)
|
|
// Call RegisterUsingIAMMethod and pass a callback to respond to the challenge with a signed join request.
|
|
certs, err := joinServiceClient.RegisterUsingIAMMethod(ctx, func(challenge string) (*proto.RegisterUsingIAMMethodRequest, error) {
|
|
// create the signed sts:GetCallerIdentity request and include the challenge
|
|
signedRequest, err := iam.CreateSignedSTSIdentityRequest(ctx, challenge,
|
|
iam.WithFIPSEndpoint(params.FIPS),
|
|
)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "creating signed sts:GetCallerIdentity request")
|
|
}
|
|
|
|
// send the register request including the challenge response
|
|
return &proto.RegisterUsingIAMMethodRequest{
|
|
RegisterUsingTokenRequest: registerUsingTokenRequestForParams(token, hostKeys, params),
|
|
StsIdentityRequest: signedRequest,
|
|
}, nil
|
|
})
|
|
if err != nil {
|
|
slog.InfoContext(ctx, "Failed to register using regional STS endpoint", "role", params.ID.Role, "error", err)
|
|
return nil, trace.Wrap(err, "registering via IAM method streaming RPC")
|
|
}
|
|
|
|
slog.InfoContext(ctx, "Successfully registered with IAM method using regional STS endpoint.", "role", params.ID.Role)
|
|
return certs, nil
|
|
}
|
|
|
|
// registerUsingAzureMethod is used to register using the Azure join method. It
|
|
// is able to register through a proxy or through the auth server directly.
|
|
func registerUsingAzureMethod(
|
|
ctx context.Context, client joinServiceClient, token string, hostKeys *newHostKeys, params RegisterParams,
|
|
) (*proto.Certs, error) {
|
|
certs, err := client.RegisterUsingAzureMethod(ctx, func(challenge string) (*proto.RegisterUsingAzureMethodRequest, error) {
|
|
imds := azure.NewInstanceMetadataClient()
|
|
if !imds.IsAvailable(ctx) {
|
|
return nil, trace.AccessDenied("could not reach instance metadata. Is Teleport running on an Azure VM?")
|
|
}
|
|
ad, err := imds.GetAttestedData(ctx, challenge)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
accessToken, err := imds.GetAccessToken(ctx, params.AzureParams.ClientID)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
|
|
return &proto.RegisterUsingAzureMethodRequest{
|
|
RegisterUsingTokenRequest: registerUsingTokenRequestForParams(token, hostKeys, params),
|
|
AttestedData: ad,
|
|
AccessToken: accessToken,
|
|
}, nil
|
|
})
|
|
return certs, trace.Wrap(err)
|
|
}
|
|
|
|
// registerUsingTPMMethod is used to register using the TPM join method. It
|
|
// is able to register through a proxy or through the auth server directly.
|
|
func registerUsingTPMMethod(
|
|
ctx context.Context,
|
|
client joinServiceClient,
|
|
token string,
|
|
hostKeys *newHostKeys,
|
|
params RegisterParams,
|
|
) (*proto.Certs, error) {
|
|
log := slog.Default()
|
|
|
|
initReq := &proto.RegisterUsingTPMMethodInitialRequest{
|
|
JoinRequest: registerUsingTokenRequestForParams(token, hostKeys, params),
|
|
}
|
|
|
|
attestation, close, err := tpm.Attest(ctx, log)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
defer func() {
|
|
if err := close(); err != nil {
|
|
log.WarnContext(ctx, "Failed to close TPM", "error", err)
|
|
}
|
|
}()
|
|
|
|
initReq.AttestationParams = tpm.AttestationParametersToProto(
|
|
attestation.AttestParams,
|
|
)
|
|
// Get the EKKey or EKCert. We want to prefer the EKCert if it is available
|
|
// as this is signed by the manufacturer.
|
|
switch {
|
|
case attestation.Data.EKCert != nil:
|
|
log.DebugContext(
|
|
ctx,
|
|
"Using EKCert for TPM registration",
|
|
"ekcert_serial", attestation.Data.EKCert.SerialNumber,
|
|
)
|
|
initReq.Ek = &proto.RegisterUsingTPMMethodInitialRequest_EkCert{
|
|
EkCert: attestation.Data.EKCert.Raw,
|
|
}
|
|
case attestation.Data.EKPub != nil:
|
|
log.DebugContext(
|
|
ctx,
|
|
"Using EKKey for TPM registration",
|
|
"ekpub_hash", attestation.Data.EKPubHash,
|
|
)
|
|
initReq.Ek = &proto.RegisterUsingTPMMethodInitialRequest_EkKey{
|
|
EkKey: attestation.Data.EKPub,
|
|
}
|
|
default:
|
|
return nil, trace.BadParameter("tpm has neither ekkey or ekcert")
|
|
}
|
|
|
|
// Submit initial request to the Auth Server.
|
|
certs, err := client.RegisterUsingTPMMethod(
|
|
ctx,
|
|
initReq,
|
|
func(
|
|
challenge *proto.TPMEncryptedCredential,
|
|
) (*proto.RegisterUsingTPMMethodChallengeResponse, error) {
|
|
// Solve the encrypted credential with our AK to prove possession
|
|
// and obtain the solution we need to complete the ceremony.
|
|
solution, err := attestation.Solve(tpm.EncryptedCredentialFromProto(
|
|
challenge,
|
|
))
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "activating credential")
|
|
}
|
|
return &proto.RegisterUsingTPMMethodChallengeResponse{
|
|
Solution: solution,
|
|
}, nil
|
|
},
|
|
)
|
|
return certs, trace.Wrap(err)
|
|
}
|
|
|
|
func mapFromHeader(header http.Header) map[string]string {
|
|
out := make(map[string]string, len(header))
|
|
for k := range header {
|
|
out[k] = header.Get(k)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func registerUsingOracleMethod(
|
|
ctx context.Context, client joinServiceClient, token string, hostKeys *newHostKeys, params RegisterParams,
|
|
) (*proto.Certs, error) {
|
|
certs, err := client.RegisterUsingOracleMethod(
|
|
ctx,
|
|
registerUsingTokenRequestForParams(token, hostKeys, params),
|
|
func(challenge string) (*proto.OracleSignedRequest, error) {
|
|
innerHeaders, outerHeaders, err := oracle.CreateSignedRequest(challenge)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return &proto.OracleSignedRequest{
|
|
Headers: mapFromHeader(outerHeaders),
|
|
PayloadHeaders: mapFromHeader(innerHeaders),
|
|
}, nil
|
|
})
|
|
return certs, trace.Wrap(err)
|
|
}
|
|
|
|
// readCA will read in CA that will be used to validate the certificate that
|
|
// the Auth Server presents.
|
|
func readCA(path string) (*x509.Certificate, error) {
|
|
certBytes, err := utils.ReadPath(path)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
cert, err := tlsca.ParseCertificatePEM(certBytes)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err, "failed to parse certificate at %v", path)
|
|
}
|
|
return cert, nil
|
|
}
|
|
|
|
type newHostKeys struct {
|
|
privateKey crypto.Signer
|
|
sshPub []byte
|
|
tlsPub []byte
|
|
}
|
|
|
|
func generateHostKeysForProxy(ctx context.Context, insecure bool, proxyAddr string) (*newHostKeys, error) {
|
|
getSuite := func(ctx context.Context) (types.SignatureAlgorithmSuite, error) {
|
|
pr, err := webclient.Find(&webclient.Config{
|
|
Context: ctx,
|
|
ProxyAddr: proxyAddr,
|
|
Insecure: insecure,
|
|
})
|
|
if err != nil {
|
|
return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED, trace.Wrap(err, "pinging proxy to determine signature algorithm suite")
|
|
}
|
|
return pr.Auth.SignatureAlgorithmSuite, nil
|
|
}
|
|
return generateHostKeys(ctx, getSuite)
|
|
}
|
|
|
|
func generateHostKeysForAuth(ctx context.Context, authClient AuthJoinClient) (*newHostKeys, error) {
|
|
getSuite := func(ctx context.Context) (types.SignatureAlgorithmSuite, error) {
|
|
pr, err := authClient.Ping(ctx)
|
|
if err != nil {
|
|
return types.SignatureAlgorithmSuite_SIGNATURE_ALGORITHM_SUITE_UNSPECIFIED, trace.Wrap(err, "pinging auth to determine signature algorithm suite")
|
|
}
|
|
return pr.SignatureAlgorithmSuite, nil
|
|
}
|
|
return generateHostKeys(ctx, getSuite)
|
|
}
|
|
|
|
func generateHostKeys(ctx context.Context, getSuite cryptosuites.GetSuiteFunc) (*newHostKeys, error) {
|
|
key, err := cryptosuites.GenerateKey(ctx, getSuite, cryptosuites.HostIdentity)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
sshPub, err := ssh.NewPublicKey(key.Public())
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
tlsPub, err := keys.MarshalPublicKey(key.Public())
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
return &newHostKeys{
|
|
privateKey: key,
|
|
sshPub: ssh.MarshalAuthorizedKey(sshPub),
|
|
tlsPub: tlsPub,
|
|
}, nil
|
|
}
|