mirror of
https://github.com/gravitational/teleport.git
synced 2026-10-12 02:15:01 +00:00
Removes any externally visibile notions to end users that a token name and token secret are two different entities. This removes the need to output a token_name and token_secret value from `tctl scoped tokens add` as well as removing the token_secret from teleport.yaml and teleport CLIs. Internally, the name of a scoped_token is still NOT a secret. The secret is a separate field in the scoped_token.status. When the token is presented to a user it is done so as `<token_name>:<base64(token_secret)`. The leading token_name is not encoded to allow users to visually dentify tokens they may have created with a specific name. The `~` separator was chosen because it is neither a valid backend key, scope separator, and does not need any special encoding if included in a URL. The token_secret is base64 encoded so that it appears as opaque text to users and can be included in a URL. Additionally, the tctl scoped ref parsing has been updated such that tctl get/edit/rm honors both `<token_name>` and `<token_name>:<base64(token_secret)>` This does contain a few breaking changes. The token_secret has been removed from the teleport.yaml file config and its equivalent CLI flags have been removed. Since scopes are still under active development and this only impacts scoped_tokens and not traditional tokens the impact of this change should be minimal.
3435 lines
111 KiB
Go
3435 lines
111 KiB
Go
/*
|
|
* Teleport
|
|
* Copyright (C) 2023 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 config provides facilities for configuring Teleport daemons
|
|
// including
|
|
// - parsing YAML configuration
|
|
// - parsing CLI flags
|
|
package config
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"crypto/x509"
|
|
"errors"
|
|
"io"
|
|
"log/slog"
|
|
"maps"
|
|
"math"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode"
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
|
"github.com/gravitational/trace"
|
|
"golang.org/x/net/idna"
|
|
kyaml "k8s.io/apimachinery/pkg/util/yaml"
|
|
|
|
"github.com/gravitational/teleport"
|
|
"github.com/gravitational/teleport/api/client/webclient"
|
|
"github.com/gravitational/teleport/api/constants"
|
|
"github.com/gravitational/teleport/api/types"
|
|
gcputils "github.com/gravitational/teleport/api/utils/gcp"
|
|
"github.com/gravitational/teleport/lib/automaticupgrades"
|
|
"github.com/gravitational/teleport/lib/backend"
|
|
"github.com/gravitational/teleport/lib/backend/lite"
|
|
"github.com/gravitational/teleport/lib/backend/memory"
|
|
"github.com/gravitational/teleport/lib/defaults"
|
|
"github.com/gravitational/teleport/lib/integrations/externalauditstorage/easconfig"
|
|
"github.com/gravitational/teleport/lib/integrations/samlidp/samlidpconfig"
|
|
"github.com/gravitational/teleport/lib/limiter"
|
|
"github.com/gravitational/teleport/lib/multiplexer"
|
|
"github.com/gravitational/teleport/lib/service/servicecfg"
|
|
"github.com/gravitational/teleport/lib/services"
|
|
"github.com/gravitational/teleport/lib/tlsca"
|
|
"github.com/gravitational/teleport/lib/utils"
|
|
awsregion "github.com/gravitational/teleport/lib/utils/aws/region"
|
|
logutils "github.com/gravitational/teleport/lib/utils/log"
|
|
"github.com/gravitational/teleport/lib/utils/parse"
|
|
libslices "github.com/gravitational/teleport/lib/utils/slices"
|
|
)
|
|
|
|
// CommandLineFlags stores command line flag values, it's a much simplified subset
|
|
// of Teleport configuration (which is fully expressed via YAML config file)
|
|
type CommandLineFlags struct {
|
|
// --name flag
|
|
NodeName string
|
|
// --auth-server flag
|
|
AuthServerAddr []string
|
|
// --token flag
|
|
AuthToken string
|
|
// --join-method flag
|
|
JoinMethod string
|
|
// CAPins are the SKPI hashes of the CAs used to verify the Auth Server.
|
|
CAPins []string
|
|
// --listen-ip flag
|
|
ListenIP net.IP
|
|
// --advertise-ip flag
|
|
AdvertiseIP string
|
|
// --config flag
|
|
ConfigFile string
|
|
// --apply-on-startup contains the path of a YAML manifest whose resources should be
|
|
// applied on startup. Unlike the bootstrap flag, the resources are always applied,
|
|
// even if the cluster is already initialized. Existing resources will be updated.
|
|
ApplyOnStartupFile string
|
|
// Bootstrap flag contains a YAML file that defines a set of resources to bootstrap
|
|
// a cluster.
|
|
BootstrapFile string
|
|
// ConfigString is a base64 encoded configuration string
|
|
// set by --config-string or TELEPORT_CONFIG environment variable
|
|
ConfigString string
|
|
// --roles flag
|
|
Roles string
|
|
// -d flag
|
|
Debug bool
|
|
|
|
// --insecure-no-tls flag
|
|
DisableTLS bool
|
|
|
|
// --labels flag
|
|
Labels string
|
|
// --pid-file flag
|
|
PIDFile string
|
|
// DiagnosticAddr is listen address for diagnostic endpoint
|
|
DiagnosticAddr string
|
|
// PermitUserEnvironment enables reading of ~/.tsh/environment
|
|
// when creating a new session.
|
|
PermitUserEnvironment bool
|
|
|
|
// Insecure mode is controlled by --insecure flag and in this mode
|
|
// Teleport won't check certificates when connecting to trusted clusters
|
|
// It's useful for learning Teleport (following quick starts, etc).
|
|
InsecureMode bool
|
|
|
|
// FIPS mode means Teleport starts in a FedRAMP/FIPS 140 compliant
|
|
// configuration.
|
|
FIPS bool
|
|
|
|
// SkipVersionCheck allows Teleport to connect to auth servers that
|
|
// have an earlier major version number.
|
|
SkipVersionCheck bool
|
|
|
|
// AppName is the name of the application to proxy.
|
|
AppName string
|
|
|
|
// AppURI is the internal address of the application to proxy.
|
|
AppURI string
|
|
|
|
// AppCloud is set if application is proxying Cloud API
|
|
AppCloud string
|
|
|
|
// AppPublicAddr is the public address of the application to proxy.
|
|
AppPublicAddr string
|
|
|
|
// MCPDemoServer enables the "Teleport Demo" MCP server.
|
|
MCPDemoServer bool
|
|
|
|
// DatabaseName is the name of the database to proxy.
|
|
DatabaseName string
|
|
// DatabaseDescription is a free-form database description.
|
|
DatabaseDescription string
|
|
// DatabaseProtocol is the type of the proxied database e.g. postgres or mysql.
|
|
DatabaseProtocol string
|
|
// DatabaseURI is the address to connect to the proxied database.
|
|
DatabaseURI string
|
|
// DatabaseCACertFile is the database CA cert path.
|
|
DatabaseCACertFile string
|
|
// DatabaseAWSRegion is an optional database cloud region e.g. when using AWS RDS.
|
|
DatabaseAWSRegion string
|
|
// DatabaseAWSAccountID is an optional AWS account ID e.g. when using Keyspaces.
|
|
DatabaseAWSAccountID string
|
|
// DatabaseAWSAssumeRoleARN is an optional AWS IAM role ARN to assume when accessing the database.
|
|
DatabaseAWSAssumeRoleARN string
|
|
// DatabaseAWSExternalID is an optional AWS external ID used to enable assuming an AWS role across accounts.
|
|
DatabaseAWSExternalID string
|
|
// DatabaseAWSRedshiftClusterID is Redshift cluster identifier.
|
|
DatabaseAWSRedshiftClusterID string
|
|
// DatabaseAWSRDSInstanceID is RDS instance identifier.
|
|
DatabaseAWSRDSInstanceID string
|
|
// DatabaseAWSRDSClusterID is RDS cluster (Aurora) cluster identifier.
|
|
DatabaseAWSRDSClusterID string
|
|
// DatabaseAWSElastiCacheGroupID is the ElastiCache replication group identifier.
|
|
DatabaseAWSElastiCacheGroupID string
|
|
// DatabaseAWSElastiCacheServerlessCacheName is the ElastiCache Serverless cache name.
|
|
DatabaseAWSElastiCacheServerlessCacheName string
|
|
// DatabaseAWSMemoryDBClusterName is the MemoryDB cluster name.
|
|
DatabaseAWSMemoryDBClusterName string
|
|
// DatabaseAWSSessionTags is the AWS STS session tags.
|
|
DatabaseAWSSessionTags string
|
|
// DatabaseGCPProjectID is GCP Cloud SQL project identifier.
|
|
DatabaseGCPProjectID string
|
|
// DatabaseGCPInstanceID is GCP Cloud SQL instance identifier.
|
|
DatabaseGCPInstanceID string
|
|
// DatabaseGCPAlloyDBEndpointType is the AlloyDB database endpoint type to use.
|
|
DatabaseGCPAlloyDBEndpointType string
|
|
// DatabaseADKeytabFile is the path to Kerberos keytab file.
|
|
DatabaseADKeytabFile string
|
|
// DatabaseADKrb5File is the path to krb5.conf file.
|
|
DatabaseADKrb5File string
|
|
// DatabaseADDomain is the Active Directory domain for authentication.
|
|
DatabaseADDomain string
|
|
// DatabaseADSPN is the database Service Principal Name.
|
|
DatabaseADSPN string
|
|
// DatabaseMySQLServerVersion is the MySQL server version reported to a client
|
|
// if the value cannot be obtained from the database.
|
|
DatabaseMySQLServerVersion string
|
|
|
|
// ProxyServer is the url of the proxy server to connect to.
|
|
ProxyServer string
|
|
// OpenSSHConfigPath is the path of the file to write agentless configuration to.
|
|
OpenSSHConfigPath string
|
|
// RestartOpenSSH indicates whether openssh should be restarted or not.
|
|
RestartOpenSSH bool
|
|
// RestartCommand is the command to use when restarting sshd
|
|
RestartCommand string
|
|
// CheckCommand is the command to use when checking sshd config validity
|
|
CheckCommand string
|
|
// Address is the ip address of the OpenSSH node.
|
|
Address string
|
|
// AdditionalPrincipals is a list of additional principals to include in the SSH cert.
|
|
AdditionalPrincipals string
|
|
// Directory to store
|
|
DataDir string
|
|
|
|
// IntegrationConfDeployServiceIAMArguments contains the arguments of
|
|
// `teleport integration configure deployservice-iam` command
|
|
IntegrationConfDeployServiceIAMArguments IntegrationConfDeployServiceIAM
|
|
|
|
// IntegrationConfAWSAppAccessIAMArguments contains the arguments of
|
|
// `teleport integration configure aws-app-access-iam` command
|
|
IntegrationConfAWSAppAccessIAMArguments IntegrationConfAWSAppAccessIAM
|
|
|
|
// IntegrationConfEC2SSMIAMArguments contains the arguments of
|
|
// `teleport integration configure ec2-ssm-iam` command
|
|
IntegrationConfEC2SSMIAMArguments IntegrationConfEC2SSMIAM
|
|
|
|
// IntegrationConfEKSIAMArguments contains the arguments of
|
|
// `teleport integration configure eks-iam` command
|
|
IntegrationConfEKSIAMArguments IntegrationConfEKSIAM
|
|
|
|
// IntegrationConfAWSOIDCIdPArguments contains the arguments of
|
|
// `teleport integration configure awsoidc-idp` command
|
|
IntegrationConfAWSOIDCIdPArguments IntegrationConfAWSOIDCIdP
|
|
|
|
// IntegrationConfListDatabasesIAMArguments contains the arguments of
|
|
// `teleport integration configure listdatabases-iam` command
|
|
IntegrationConfListDatabasesIAMArguments IntegrationConfListDatabasesIAM
|
|
|
|
// IntegrationConfExternalAuditStorageArguments contains the arguments of the
|
|
// `teleport integration configure externalauditstorage` command
|
|
IntegrationConfExternalAuditStorageArguments easconfig.ExternalAuditStorageConfiguration
|
|
|
|
// IntegrationConfAccessGraphAWSSyncArguments contains the arguments of
|
|
// `teleport integration configure access-graph aws-iam` command
|
|
IntegrationConfAccessGraphAWSSyncArguments IntegrationConfAccessGraphAWSSync
|
|
|
|
// IntegrationConfAccessGarphAzureSyncArguments contains the arguments of
|
|
// `teleport integration configure access-graph azure` command
|
|
IntegrationConfAccessGraphAzureSyncArguments IntegrationConfAccessGraphAzureSync
|
|
|
|
// IntegrationConfAzureOIDCArguments contains the arguments of
|
|
// `teleport integration configure azure-oidc` command
|
|
IntegrationConfAzureOIDCArguments IntegrationConfAzureOIDC
|
|
|
|
// IntegrationConfSAMLIdPGCPWorkforceArguments contains the arguments of
|
|
// `teleport integration configure samlidp gcp-workforce` command
|
|
IntegrationConfSAMLIdPGCPWorkforceArguments samlidpconfig.GCPWorkforceAPIParams
|
|
|
|
// IntegrationConfAWSRATrustAnchorArguments contains the arguments of
|
|
// `teleport integration configure awsra-trust-anchor` command
|
|
IntegrationConfAWSRATrustAnchorArguments IntegrationConfAWSRATrustAnchor
|
|
|
|
// IntegrationConfSessionSummariesBedrockArguments contains the arguments of
|
|
// `teleport integration configure session-summaries bedrock` command
|
|
IntegrationConfSessionSummariesBedrockArguments IntegrationConfSessionSummariesBedrock
|
|
|
|
// LogLevel is the new application's log level.
|
|
LogLevel string
|
|
|
|
// Profiles comma-separated list of pprof profiles to be collected.
|
|
Profiles string
|
|
|
|
// ProfileSeconds defines the time the pprof will be collected.
|
|
ProfileSeconds int
|
|
|
|
// DisableDebugService disables the debug service.
|
|
DisableDebugService bool
|
|
|
|
// EnableSELinux enables SELinux support for the SSH service.
|
|
EnableSELinux bool
|
|
|
|
// EnsureSELinuxEnforcing will cause Teleport to exit if the SELinux module
|
|
// is not set to enforcing mode or the global SELinux mode is not set to
|
|
// enforcing.
|
|
EnsureSELinuxEnforcing bool
|
|
|
|
// BackendKey is the backend key to use for various `teleport backend` commands.
|
|
BackendKey string
|
|
// BackendPrefix limits `teleport backend ls` to only output keys matching the prefix.
|
|
BackendPrefix string
|
|
// Format is used to change the format of output.
|
|
Format string
|
|
}
|
|
|
|
// IntegrationConfAccessGraphAWSSync contains the arguments of
|
|
// `teleport integration configure access-graph aws-iam` command.
|
|
type IntegrationConfAccessGraphAWSSync struct {
|
|
// Role is the AWS Role associated with the Integration
|
|
Role string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
// SQSQueueURL is the URL of the SQS queue to use for the Identity Security Activity Center.
|
|
SQSQueueURL string
|
|
// CloudTrailBucketARN is the name of the S3 bucket to use for the Identity Security Activity Center.
|
|
CloudTrailBucketARN string
|
|
// KMSKeyARNs is the ARN of the KMS key to use for decrypting the Identity Security Activity Center data.
|
|
KMSKeyARNs []string
|
|
// EnableEKSAuditLogs enables collection of EKS audit logs from CloudWatch logs.
|
|
EnableEKSAuditLogs bool
|
|
}
|
|
|
|
// IntegrationConfAccessGraphAzureSync contains the arguments of
|
|
// `teleport integration configure access-graph azure` command.
|
|
type IntegrationConfAccessGraphAzureSync struct {
|
|
// ManagedIdentity is the principal performing the discovery
|
|
ManagedIdentity string
|
|
// RoleName is the name of the Azure Role to create and assign to the managed identity
|
|
RoleName string
|
|
// SubscriptionID is the Azure subscription containing resources for sync
|
|
SubscriptionID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfAzureOIDC contains the arguments of
|
|
// `teleport integration configure azure-oidc` command
|
|
type IntegrationConfAzureOIDC struct {
|
|
// ProxyPublicAddr is the publicly-reachable URL of the Teleport Proxy.
|
|
// It is used as the OIDC issuer URL, as well as for SAML URIs.
|
|
ProxyPublicAddr string
|
|
|
|
// AuthConnectorName is the name of the SAML connector that will be created on Teleport side.
|
|
AuthConnectorName string
|
|
|
|
// AccessGraphEnabled is a flag indicating that access graph integration is requested.
|
|
// When this is true, the integration script will produce
|
|
// a cache file necessary for TAG synchronization.
|
|
AccessGraphEnabled bool
|
|
|
|
// SkipOIDCConfiguration is a flag indicating that OIDC configuration should be skipped.
|
|
SkipOIDCConfiguration bool
|
|
}
|
|
|
|
// IntegrationConfDeployServiceIAM contains the arguments of
|
|
// `teleport integration configure deployservice-iam` command
|
|
type IntegrationConfDeployServiceIAM struct {
|
|
// Cluster is the teleport cluster name.
|
|
Cluster string
|
|
// Name is the integration name.
|
|
Name string
|
|
// Region is the AWS Region used to set up the client.
|
|
Region string
|
|
// Role is the AWS Role associated with the Integration
|
|
Role string
|
|
// TaskRole is the AWS Role to be used by the deployed service.
|
|
TaskRole string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfAWSAppAccessIAM contains the arguments of
|
|
// `teleport integration configure aws-app-access-iam` command
|
|
type IntegrationConfAWSAppAccessIAM struct {
|
|
// RoleName is the AWS Role associated with the Integration
|
|
RoleName string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfEC2SSMIAM contains the arguments of
|
|
// `teleport integration configure ec2-ssm-iam` command
|
|
type IntegrationConfEC2SSMIAM struct {
|
|
// RoleName is the AWS Role associated with the Integration
|
|
RoleName string
|
|
// Region is the AWS Region used to set up the client.
|
|
Region string
|
|
// SSMDocumentName is the SSM Document to be created that will run the installer script.
|
|
SSMDocumentName string
|
|
// ProxyPublicURL is Proxy's Public URL.
|
|
// This is used fetch the installer script.
|
|
// No trailing / is expected.
|
|
// Eg https://tenant.teleport.sh
|
|
ProxyPublicURL string
|
|
// ClusterName is the Teleport cluster name.
|
|
// Used for resource tagging.
|
|
ClusterName string
|
|
// IntegrationName is the Teleport AWS OIDC Integration name.
|
|
// Used for resource tagging.
|
|
IntegrationName string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfEKSIAM contains the arguments of
|
|
// `teleport integration configure eks-iam` command
|
|
type IntegrationConfEKSIAM struct {
|
|
// Region is the AWS Region used to set up the client.
|
|
Region string
|
|
// Role is the AWS Role associated with the Integration
|
|
Role string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfAWSOIDCIdP contains the arguments of
|
|
// `teleport integration configure awsoidc-idp` command
|
|
type IntegrationConfAWSOIDCIdP struct {
|
|
// Cluster is the teleport cluster name.
|
|
Cluster string
|
|
// Name is the integration name.
|
|
Name string
|
|
// Role is the AWS Role to associate with the Integration
|
|
Role string
|
|
// ProxyPublicURL is the IdP Issuer URL (Teleport Proxy Public Address).
|
|
// Eg, https://<tenant>.teleport.sh
|
|
ProxyPublicURL string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
// PolicyPreset is the name of a pre-defined policy statement which will be
|
|
// assigned to the AWS Role associate with the OIDC integration.
|
|
PolicyPreset string
|
|
}
|
|
|
|
// IntegrationConfAWSRATrustAnchor contains the arguments of
|
|
// `teleport integration configure awsra-trust-anchor` command
|
|
type IntegrationConfAWSRATrustAnchor struct {
|
|
// Cluster is the teleport cluster name.
|
|
Cluster string
|
|
// Name is the integration name.
|
|
Name string
|
|
// TrustAnchor is the AWS IAM Roles Anywhere Trust Anchor name to create.
|
|
TrustAnchor string
|
|
// TrustAnchorCertBase64 is trust anchor certificate, encoded in base64.
|
|
TrustAnchorCertBase64 string
|
|
// SyncProfile is the AWS IAM Roles Anywhere Profile name to create, that will be used to sync profiles as entry points for AWS Access.
|
|
SyncProfile string
|
|
// SyncRole is the AWS IAM Role name to create, that will be used to sync profiles as entry points for AWS Access.
|
|
SyncRole string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfListDatabasesIAM contains the arguments of
|
|
// `teleport integration configure listdatabases-iam` command
|
|
type IntegrationConfListDatabasesIAM struct {
|
|
// Region is the AWS Region used to set up the client.
|
|
Region string
|
|
// Role is the AWS Role associated with the Integration
|
|
Role string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// IntegrationConfSessionSummariesBedrock contains the arguments of
|
|
// `teleport integration configure session-summaries bedrock` command
|
|
type IntegrationConfSessionSummariesBedrock struct {
|
|
// Role is the AWS Role associated with the Integration
|
|
Role string
|
|
// Resource is the AWS Bedrock resource to grant access to.
|
|
// Can be a full ARN or a model ID (e.g., 'anthropic.claude-v2' or '*' for all models).
|
|
Resource string
|
|
// AccountID is the AWS account ID.
|
|
AccountID string
|
|
// AutoConfirm skips user confirmation of the operation plan if true.
|
|
AutoConfirm bool
|
|
}
|
|
|
|
// ReadConfigFile reads /etc/teleport.yaml (or whatever is passed via --config flag)
|
|
// and overrides values in 'cfg' structure
|
|
func ReadConfigFile(cliConfigPath string) (*FileConfig, error) {
|
|
configFilePath := defaults.ConfigFilePath
|
|
// --config tells us to use a specific conf. file:
|
|
if cliConfigPath != "" {
|
|
configFilePath = cliConfigPath
|
|
if !utils.FileExists(configFilePath) {
|
|
return nil, trace.NotFound("file %s is not found", configFilePath)
|
|
}
|
|
}
|
|
// default config doesn't exist? quietly return:
|
|
if !utils.FileExists(configFilePath) {
|
|
slog.InfoContext(context.Background(), "not using a config file")
|
|
return nil, nil
|
|
}
|
|
slog.DebugContext(context.Background(), "reading config file", "config_file", configFilePath)
|
|
return ReadFromFile(configFilePath)
|
|
}
|
|
|
|
// ReadResources loads a set of resources from a file.
|
|
func ReadResources(filePath string) ([]types.Resource, error) {
|
|
reader, err := utils.OpenFileAllowingUnsafeLinks(filePath)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
defer reader.Close()
|
|
decoder := kyaml.NewYAMLOrJSONDecoder(reader, defaults.LookaheadBufSize)
|
|
var resources []types.Resource
|
|
for {
|
|
var raw services.UnknownResource
|
|
err := decoder.Decode(&raw)
|
|
if err != nil {
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
rsc, err := services.UnmarshalResource(raw.Kind, raw.Raw)
|
|
if err != nil {
|
|
return nil, trace.Wrap(err)
|
|
}
|
|
resources = append(resources, rsc)
|
|
}
|
|
return resources, nil
|
|
}
|
|
|
|
// ApplyFileConfig applies configuration from a YAML file to Teleport
|
|
// runtime config
|
|
//
|
|
// ApplyFileConfig is used by both teleport and tctl binaries.
|
|
func ApplyFileConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
var err error
|
|
|
|
// no config file? no problem
|
|
if fc == nil {
|
|
return nil
|
|
}
|
|
|
|
applyConfigVersion(fc, cfg)
|
|
|
|
// merge file-based config with defaults in 'cfg'
|
|
if fc.Auth.Disabled() {
|
|
cfg.Auth.Enabled = false
|
|
}
|
|
if fc.SSH.Disabled() {
|
|
cfg.SSH.Enabled = false
|
|
}
|
|
if fc.Proxy.Disabled() {
|
|
cfg.Proxy.Enabled = false
|
|
}
|
|
if fc.Kube.Enabled() {
|
|
cfg.Kube.Enabled = true
|
|
}
|
|
if fc.Apps.Disabled() {
|
|
cfg.Apps.Enabled = false
|
|
}
|
|
if fc.Databases.Disabled() {
|
|
cfg.Databases.Enabled = false
|
|
}
|
|
if fc.Metrics.Disabled() {
|
|
cfg.Metrics.Enabled = false
|
|
}
|
|
if fc.WindowsDesktop.Disabled() {
|
|
cfg.WindowsDesktop.Enabled = false
|
|
}
|
|
if fc.Debug.Disabled() {
|
|
cfg.DebugService.Enabled = false
|
|
}
|
|
|
|
if fc.AccessGraph.Enabled {
|
|
cfg.AccessGraph.Enabled = true
|
|
if fc.AccessGraph.Endpoint == "" {
|
|
return trace.BadParameter("access_graph.endpoint is required when access graph integration is enabled")
|
|
}
|
|
cfg.AccessGraph.Addr = fc.AccessGraph.Endpoint
|
|
cfg.AccessGraph.CA = fc.AccessGraph.CA
|
|
// TODO(tigrato): change this behavior when we drop support for plain text connections
|
|
cfg.AccessGraph.Insecure = fc.AccessGraph.Insecure
|
|
cfg.AccessGraph.AuditLog = servicecfg.AuditLogConfig(fc.AccessGraph.AuditLog)
|
|
}
|
|
|
|
applyString(fc.NodeName, &cfg.Hostname)
|
|
|
|
// apply "advertise_ip" setting:
|
|
advertiseIP := fc.AdvertiseIP
|
|
if advertiseIP != "" {
|
|
if _, _, err := utils.ParseAdvertiseAddr(advertiseIP); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.AdvertiseIP = advertiseIP
|
|
}
|
|
cfg.PIDFile = fc.PIDFile
|
|
|
|
if err := applyAuthOrProxyAddress(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if fc.RelayServer != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.RelayServer, -1)
|
|
if err != nil {
|
|
return trace.Wrap(err, "parsing teleport.relay_server")
|
|
}
|
|
|
|
cfg.RelayServer = addr.Addr
|
|
}
|
|
|
|
if err := applyTokenConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if fc.Global.DataDir != "" {
|
|
cfg.DataDir = fc.Global.DataDir
|
|
cfg.Auth.StorageConfig.Params["path"] = cfg.DataDir
|
|
}
|
|
|
|
// If a backend is specified, override the defaults.
|
|
if fc.Storage.Type != "" {
|
|
// If the alternative name "dir" is given, update it to "lite".
|
|
if fc.Storage.Type == lite.AlternativeName {
|
|
fc.Storage.Type = lite.GetName()
|
|
}
|
|
|
|
cfg.Auth.StorageConfig = fc.Storage
|
|
// backend is specified, but no path is set, set a reasonable default
|
|
_, pathSet := cfg.Auth.StorageConfig.Params[defaults.BackendPath]
|
|
if cfg.Auth.StorageConfig.Type == lite.GetName() && !pathSet {
|
|
if cfg.Auth.StorageConfig.Params == nil {
|
|
cfg.Auth.StorageConfig.Params = make(backend.Params)
|
|
}
|
|
cfg.Auth.StorageConfig.Params[defaults.BackendPath] = filepath.Join(cfg.DataDir, defaults.BackendDir)
|
|
}
|
|
} else {
|
|
// Set a reasonable default.
|
|
cfg.Auth.StorageConfig.Params[defaults.BackendPath] = filepath.Join(cfg.DataDir, defaults.BackendDir)
|
|
}
|
|
|
|
// apply logger settings
|
|
err = applyLogConfig(fc.Logger, cfg)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if fc.CachePolicy.TTL != "" {
|
|
slog.WarnContext(ctx, "cache.ttl config option is deprecated and will be ignored, caches no longer attempt to anticipate resource expiration")
|
|
}
|
|
if fc.CachePolicy.Type == memory.GetName() {
|
|
slog.DebugContext(ctx, "cache.type config option is explicitly set to memory")
|
|
} else if fc.CachePolicy.Type != "" {
|
|
slog.WarnContext(ctx, "cache.type config option is deprecated and will be ignored, caches are always in memory in this version")
|
|
}
|
|
|
|
// apply cache policy for node and proxy
|
|
cachePolicy, err := fc.CachePolicy.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.CachePolicy = *cachePolicy
|
|
|
|
authConnectionConfig, err := fc.AuthConnectionConfig.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.AuthConnectionConfig = *authConnectionConfig
|
|
|
|
cfg.ShutdownDelay = time.Duration(fc.ShutdownDelay)
|
|
|
|
// Apply (TLS) cipher suites and (SSH) ciphers, KEX algorithms, and MAC
|
|
// algorithms.
|
|
if len(fc.CipherSuites) > 0 {
|
|
cipherSuites, err := utils.CipherSuiteMapping(fc.CipherSuites)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.CipherSuites = cipherSuites
|
|
}
|
|
if fc.Ciphers != nil {
|
|
cfg.Ciphers = fc.Ciphers
|
|
}
|
|
if fc.KEXAlgorithms != nil {
|
|
cfg.KEXAlgorithms = fc.KEXAlgorithms
|
|
}
|
|
if fc.MACAlgorithms != nil {
|
|
cfg.MACAlgorithms = fc.MACAlgorithms
|
|
}
|
|
if fc.CASignatureAlgorithm != nil {
|
|
slog.WarnContext(ctx, "ca_signing_algo config option is deprecated and will be removed in a future release, Teleport defaults to rsa-sha2-512")
|
|
}
|
|
|
|
// Read in how nodes will validate the CA. A single empty string in the file
|
|
// conf should indicate no pins.
|
|
if err = cfg.ApplyCAPins(fc.CAPin); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Set diagnostic address
|
|
if fc.DiagAddr != "" {
|
|
// Validate address
|
|
parsed, err := utils.ParseAddr(fc.DiagAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.DiagnosticAddr = *parsed
|
|
}
|
|
|
|
// apply connection throttling:
|
|
limiters := []*limiter.Config{
|
|
&cfg.SSH.Limiter,
|
|
&cfg.Auth.Limiter,
|
|
&cfg.Proxy.Limiter,
|
|
&cfg.Databases.Limiter,
|
|
&cfg.Kube.Limiter,
|
|
&cfg.WindowsDesktop.ConnLimiter,
|
|
&cfg.Apps.Limiter,
|
|
}
|
|
for _, l := range limiters {
|
|
if fc.Limits.MaxConnections > 0 {
|
|
l.MaxConnections = fc.Limits.MaxConnections
|
|
}
|
|
|
|
for _, rate := range fc.Limits.Rates {
|
|
l.Rates = append(l.Rates, limiter.Rate{
|
|
Period: rate.Period,
|
|
Average: rate.Average,
|
|
Burst: rate.Burst,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Apply configuration for "auth_service", "proxy_service", "ssh_service",
|
|
// and "app_service" if they are enabled.
|
|
if fc.Auth.Enabled() {
|
|
err = applyAuthConfig(fc, cfg)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Proxy.Enabled() {
|
|
err = applyProxyConfig(fc, cfg)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.SSH.Enabled() {
|
|
err = applySSHConfig(fc, cfg)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Kube.Enabled() {
|
|
if err := applyKubeConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Apps.Enabled() {
|
|
if err := applyAppsConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Databases.Enabled() {
|
|
if err := applyDatabasesConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Metrics.Enabled() {
|
|
if err := applyMetricsConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.WindowsDesktop.Enabled() {
|
|
if err := applyWindowsDesktopConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Tracing.Enabled() {
|
|
if err := applyTracingConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if fc.Discovery.Enabled() {
|
|
if err := applyDiscoveryConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if fc.Okta.Enabled() {
|
|
if err := applyOktaConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if fc.Jamf.Enabled() {
|
|
if err := applyJamfConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if fc.Relay.Enabled {
|
|
if err := applyRelayConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyAuthOrProxyAddress(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
switch cfg.Version {
|
|
// For config versions v1 and v2, the auth_servers field can point to an auth
|
|
// server or a proxy server
|
|
case defaults.TeleportConfigVersionV1, defaults.TeleportConfigVersionV2:
|
|
// config file has auth servers in there?
|
|
if len(fc.AuthServers) > 0 {
|
|
var parsedAddresses []utils.NetAddr
|
|
|
|
for _, as := range fc.AuthServers {
|
|
addr, err := utils.ParseHostPortAddr(as, defaults.AuthListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
parsedAddresses = append(parsedAddresses, *addr)
|
|
}
|
|
|
|
if err := cfg.SetAuthServerAddresses(parsedAddresses); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if fc.AuthServer != "" {
|
|
return trace.BadParameter("auth_server is supported from config version v3 onwards")
|
|
}
|
|
|
|
if fc.ProxyServer != "" {
|
|
return trace.BadParameter("proxy_server is supported from config version v3 onwards")
|
|
}
|
|
|
|
// From v3 onwards, either auth_server or proxy_server should be set
|
|
case defaults.TeleportConfigVersionV3:
|
|
if len(fc.AuthServers) > 0 {
|
|
return trace.BadParameter("config v3 has replaced auth_servers with either auth_server or proxy_server")
|
|
}
|
|
|
|
haveAuthServer := fc.AuthServer != ""
|
|
haveProxyServer := fc.ProxyServer != ""
|
|
|
|
if haveProxyServer && haveAuthServer {
|
|
return trace.BadParameter("only one of auth_server or proxy_server should be set")
|
|
}
|
|
|
|
if haveAuthServer {
|
|
addr, err := utils.ParseHostPortAddr(fc.AuthServer, defaults.AuthListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.SetAuthServerAddress(*addr)
|
|
}
|
|
|
|
if haveProxyServer {
|
|
if fc.Proxy.Enabled() {
|
|
return trace.BadParameter("proxy_server can not be specified when proxy service is enabled")
|
|
}
|
|
|
|
addr, err := utils.ParseHostPortAddr(fc.ProxyServer, defaults.HTTPListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.ProxyServer = *addr
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyLogConfig(loggerConfig Log, cfg *servicecfg.Config) error {
|
|
cfg.LogConfig = logutils.Config{
|
|
Output: loggerConfig.Output,
|
|
Severity: loggerConfig.Severity,
|
|
Format: loggerConfig.Format.Output,
|
|
ExtraFields: loggerConfig.Format.ExtraFields,
|
|
EnableColors: utils.IsTerminal(os.Stderr),
|
|
}
|
|
|
|
var err error
|
|
cfg.Logger, cfg.LoggerLevel, cfg.LogWriter, err = logutils.Initialize(cfg.LogConfig)
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// applyAuthConfig applies file configuration for the "auth_service" section.
|
|
func applyAuthConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
var err error
|
|
|
|
if fc.Auth.KubeconfigFile != "" {
|
|
const warningMessage = "The auth_service no longer needs kubeconfig_file. It has " +
|
|
"been moved to proxy_service section. This setting is ignored."
|
|
slog.WarnContext(context.Background(), warningMessage)
|
|
}
|
|
|
|
cfg.Auth.PROXYProtocolMode = multiplexer.PROXYProtocolUnspecified
|
|
if fc.Auth.ProxyProtocol != "" {
|
|
val := multiplexer.PROXYProtocolMode(fc.Auth.ProxyProtocol)
|
|
if err := validatePROXYProtocolValue(val); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Auth.PROXYProtocolMode = val
|
|
}
|
|
|
|
if fc.Auth.ListenAddress != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Auth.ListenAddress, int(defaults.AuthListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Auth.ListenAddr = *addr
|
|
if len(cfg.AuthServerAddresses()) == 0 {
|
|
cfg.SetAuthServerAddress(*addr)
|
|
}
|
|
}
|
|
for _, t := range fc.Auth.ReverseTunnels {
|
|
tun, err := t.ConvertAndValidate()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.ReverseTunnels = append(cfg.ReverseTunnels, tun)
|
|
}
|
|
if len(fc.Auth.PublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.Auth.PublicAddr, defaults.AuthListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Auth.PublicAddrs = addrs
|
|
}
|
|
// read in cluster name from file configuration and create services.ClusterName
|
|
cfg.Auth.ClusterName, err = fc.Auth.ClusterName.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// read in static tokens from file configuration and create services.StaticTokens
|
|
if fc.Auth.StaticTokens != nil {
|
|
cfg.Auth.StaticTokens, err = fc.Auth.StaticTokens.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
if fc.Auth.StaticScopedTokens != nil {
|
|
cfg.Auth.StaticScopedTokens, err = fc.Auth.StaticScopedTokens.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
// read in and set authentication preferences
|
|
if fc.Auth.Authentication != nil {
|
|
cfg.Auth.Preference, err = fc.Auth.Authentication.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if fc.Auth.MessageOfTheDay != "" {
|
|
cfg.Auth.Preference.SetMessageOfTheDay(fc.Auth.MessageOfTheDay)
|
|
}
|
|
|
|
if fc.Auth.DisconnectExpiredCert != nil {
|
|
cfg.Auth.Preference.SetOrigin(types.OriginConfigFile)
|
|
cfg.Auth.Preference.SetDisconnectExpiredCert(fc.Auth.DisconnectExpiredCert.Value)
|
|
}
|
|
|
|
// Set cluster audit configuration from file configuration.
|
|
auditConfigSpec, err := services.ClusterAuditConfigSpecFromObject(fc.Storage.Params)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
auditConfigSpec.Type = fc.Storage.Type
|
|
cfg.Auth.AuditConfig, err = types.NewClusterAuditConfig(*auditConfigSpec)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// Only override networking configuration if some of its fields are
|
|
// specified in file configuration.
|
|
if fc.Auth.hasCustomNetworkingConfig() {
|
|
cfg.Auth.NetworkingConfig, err = types.NewClusterNetworkingConfigFromConfigFile(types.ClusterNetworkingConfigSpecV2{
|
|
ClientIdleTimeout: fc.Auth.ClientIdleTimeout,
|
|
ClientIdleTimeoutMessage: fc.Auth.ClientIdleTimeoutMessage,
|
|
WebIdleTimeout: fc.Auth.WebIdleTimeout,
|
|
KeepAliveInterval: fc.Auth.KeepAliveInterval,
|
|
KeepAliveCountMax: fc.Auth.KeepAliveCountMax,
|
|
SessionControlTimeout: fc.Auth.SessionControlTimeout,
|
|
ProxyListenerMode: fc.Auth.ProxyListenerMode,
|
|
RoutingStrategy: fc.Auth.RoutingStrategy,
|
|
TunnelStrategy: fc.Auth.TunnelStrategy,
|
|
ProxyPingInterval: fc.Auth.ProxyPingInterval,
|
|
CaseInsensitiveRouting: fc.Auth.CaseInsensitiveRouting,
|
|
SSHDialTimeout: fc.Auth.SSHDialTimeout,
|
|
})
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
// Only override session recording configuration if either field is
|
|
// specified in file configuration.
|
|
if fc.Auth.hasCustomSessionRecording() {
|
|
src := types.SessionRecordingConfigSpecV2{
|
|
Mode: fc.Auth.SessionRecording,
|
|
ProxyChecksHostKeys: fc.Auth.ProxyChecksHostKeys,
|
|
}
|
|
|
|
if fc.Auth.SessionRecordingConfig != nil {
|
|
if src.Mode != "" {
|
|
return trace.BadParameter("cannot set both session_recording and session_recording_config at the same time, prefer session_recording_config.mode")
|
|
}
|
|
|
|
if src.ProxyChecksHostKeys != nil {
|
|
return trace.BadParameter("cannot set both proxy_checks_host_keys and session_recording_config at the same time, prefer session_recording_config.proxy_checks_host_keys")
|
|
}
|
|
|
|
src = fc.Auth.SessionRecordingConfig.toSpec()
|
|
}
|
|
|
|
cfg.Auth.SessionRecordingConfig, err = types.NewSessionRecordingConfigFromConfigFile(src)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if err := applyKeyStoreConfig(fc, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// read in and set the license file path (not used in open-source version)
|
|
switch licenseFile := fc.Auth.LicenseFile; {
|
|
case licenseFile == "":
|
|
cfg.Auth.LicenseFile = filepath.Join(cfg.DataDir, defaults.LicenseFile)
|
|
case filepath.IsAbs(licenseFile):
|
|
cfg.Auth.LicenseFile = licenseFile
|
|
default:
|
|
cfg.Auth.LicenseFile = filepath.Join(cfg.DataDir, licenseFile)
|
|
}
|
|
|
|
cfg.Auth.LoadAllCAs = fc.Auth.LoadAllCAs
|
|
|
|
// Setting this to true at all times to allow self hosting
|
|
// of plugins that were previously cloud only.
|
|
cfg.Auth.HostedPlugins.Enabled = true
|
|
cfg.Auth.HostedPlugins.OAuthProviders, err = fc.Auth.HostedPlugins.OAuthProviders.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if fc.Auth.AccessMonitoring != nil {
|
|
if err := fc.Auth.AccessMonitoring.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err, "failed to validate access monitoring config")
|
|
}
|
|
cfg.Auth.AccessMonitoring = fc.Auth.AccessMonitoring
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyKeyStoreConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
if fc.Auth.CAKeyParams == nil {
|
|
return nil
|
|
}
|
|
if fc.Auth.CAKeyParams.PKCS11 != nil {
|
|
if fc.Auth.CAKeyParams.GoogleCloudKMS != nil {
|
|
return trace.BadParameter("cannot set both pkcs11 and gcp_kms in file config")
|
|
}
|
|
if fc.Auth.CAKeyParams.AWSKMS != nil {
|
|
return trace.BadParameter("cannot set both pkcs11 and aws_kms in file config")
|
|
}
|
|
return trace.Wrap(applyPKCS11Config(fc.Auth.CAKeyParams.PKCS11, cfg))
|
|
}
|
|
if fc.Auth.CAKeyParams.GoogleCloudKMS != nil {
|
|
if fc.Auth.CAKeyParams.AWSKMS != nil {
|
|
return trace.BadParameter("cannot set both gpc_kms and aws_kms in file config")
|
|
}
|
|
return trace.Wrap(applyGoogleCloudKMSConfig(fc.Auth.CAKeyParams.GoogleCloudKMS, cfg))
|
|
}
|
|
if fc.Auth.CAKeyParams.AWSKMS != nil {
|
|
return trace.Wrap(applyAWSKMSConfig(fc.Auth.CAKeyParams.AWSKMS, cfg))
|
|
}
|
|
cfg.Auth.KeyStore.HealthCheck = fc.Auth.CAKeyParams.HealthCheck
|
|
return nil
|
|
}
|
|
|
|
func applyPKCS11Config(pkcs11Config *PKCS11, cfg *servicecfg.Config) error {
|
|
if pkcs11Config.ModulePath != "" {
|
|
fi, err := utils.StatFile(pkcs11Config.ModulePath)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
const worldWritableBits = 0o002
|
|
if fi.Mode().Perm()&worldWritableBits != 0 {
|
|
return trace.Errorf(
|
|
"PKCS11 library (%s) must not be world-writable",
|
|
pkcs11Config.ModulePath,
|
|
)
|
|
}
|
|
|
|
cfg.Auth.KeyStore.PKCS11.Path = pkcs11Config.ModulePath
|
|
}
|
|
|
|
cfg.Auth.KeyStore.PKCS11.TokenLabel = pkcs11Config.TokenLabel
|
|
cfg.Auth.KeyStore.PKCS11.SlotNumber = pkcs11Config.SlotNumber
|
|
cfg.Auth.KeyStore.PKCS11.MaxSessions = pkcs11Config.MaxSessions
|
|
|
|
cfg.Auth.KeyStore.PKCS11.PIN = pkcs11Config.PIN
|
|
if pkcs11Config.PINPath != "" {
|
|
if pkcs11Config.PIN != "" {
|
|
return trace.BadParameter("can not set both pin and pin_path")
|
|
}
|
|
|
|
fi, err := utils.StatFile(pkcs11Config.PINPath)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
const worldReadableBits = 0o004
|
|
if fi.Mode().Perm()&worldReadableBits != 0 {
|
|
return trace.Errorf(
|
|
"HSM pin file (%s) must not be world-readable",
|
|
pkcs11Config.PINPath,
|
|
)
|
|
}
|
|
|
|
pinBytes, err := os.ReadFile(pkcs11Config.PINPath)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
pin := strings.TrimRight(string(pinBytes), "\r\n")
|
|
cfg.Auth.KeyStore.PKCS11.PIN = pin
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyGoogleCloudKMSConfig(kmsConfig *GoogleCloudKMS, cfg *servicecfg.Config) error {
|
|
if kmsConfig.KeyRing == "" {
|
|
return trace.BadParameter("must set keyring in ca_key_params.gcp_kms")
|
|
}
|
|
cfg.Auth.KeyStore.GCPKMS.KeyRing = kmsConfig.KeyRing
|
|
if kmsConfig.ProtectionLevel == "" {
|
|
return trace.BadParameter("must set protection_level in ca_key_params.gcp_kms")
|
|
}
|
|
cfg.Auth.KeyStore.GCPKMS.ProtectionLevel = kmsConfig.ProtectionLevel
|
|
return nil
|
|
}
|
|
|
|
func applyAWSKMSConfig(kmsConfig *AWSKMS, cfg *servicecfg.Config) error {
|
|
if kmsConfig.Account == "" {
|
|
return trace.BadParameter("must set account in ca_key_params.aws_kms")
|
|
}
|
|
if kmsConfig.Region == "" {
|
|
return trace.BadParameter("must set region in ca_key_params.aws_kms")
|
|
}
|
|
cfg.Auth.KeyStore.AWSKMS = &servicecfg.AWSKMSConfig{
|
|
AWSAccount: kmsConfig.Account,
|
|
AWSRegion: kmsConfig.Region,
|
|
Tags: kmsConfig.Tags,
|
|
MultiRegion: kmsConfig.MultiRegion,
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func validatePROXYProtocolValue(p multiplexer.PROXYProtocolMode) error {
|
|
allowedOptions := []multiplexer.PROXYProtocolMode{multiplexer.PROXYProtocolOn, multiplexer.PROXYProtocolOff}
|
|
|
|
if !slices.Contains(allowedOptions, p) {
|
|
return trace.BadParameter("invalid 'proxy_protocol' value %q. Available options are: %v", p, allowedOptions)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
const proxyUntrustedTLSCertErrMsg = `The Proxy Service was unable to validate the certificate chain of the
|
|
configured TLS certificate. The authority that issued this certificate is not
|
|
trusted on this host. Using an untrusted certificate is likely to cause
|
|
connection problems when clients and other Teleport services connect to this
|
|
Proxy Service. To trust a custom certificate authority you may set the
|
|
SSL_CERT_FILE or SSL_CERT_DIR environment variables to a path with your
|
|
authority's certificate chain.`
|
|
|
|
// applyProxyConfig applies file configuration for the "proxy_service" section.
|
|
func applyProxyConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
var err error
|
|
|
|
cfg.Proxy.PROXYProtocolMode = multiplexer.PROXYProtocolUnspecified
|
|
if fc.Proxy.ProxyProtocol != "" {
|
|
val := multiplexer.PROXYProtocolMode(fc.Proxy.ProxyProtocol)
|
|
if err := validatePROXYProtocolValue(val); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Proxy.PROXYProtocolMode = val
|
|
}
|
|
|
|
allowDowngrade := fc.Proxy.ProxyProtocolAllowDowngrade
|
|
cfg.Proxy.PROXYAllowDowngrade = allowDowngrade != nil && allowDowngrade.Value
|
|
|
|
if fc.Proxy.ListenAddress != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.ListenAddress, defaults.SSHProxyListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.SSHAddr = *addr
|
|
}
|
|
if fc.Proxy.WebAddr != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.WebAddr, defaults.HTTPListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.WebAddr = *addr
|
|
}
|
|
if fc.Proxy.TunAddr != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.TunAddr, defaults.SSHProxyTunnelListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.ReverseTunnelListenAddr = *addr
|
|
}
|
|
if fc.Proxy.MySQLAddr != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.MySQLAddr, defaults.MySQLListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.MySQLAddr = *addr
|
|
}
|
|
if fc.Proxy.PostgresAddr != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.PostgresAddr, defaults.PostgresListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.PostgresAddr = *addr
|
|
}
|
|
if fc.Proxy.MongoAddr != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.MongoAddr, defaults.MongoListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.MongoAddr = *addr
|
|
}
|
|
if fc.Proxy.PeerAddr != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.PeerAddr, int(defaults.ProxyPeeringListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.PeerAddress = *addr
|
|
}
|
|
|
|
if fc.Proxy.UI != nil {
|
|
cfg.Proxy.UI = webclient.UIConfig(*fc.Proxy.UI)
|
|
switch cfg.Proxy.UI.ShowResources {
|
|
case constants.ShowResourcesaccessibleOnly,
|
|
constants.ShowResourcesRequestable:
|
|
case "":
|
|
{
|
|
cfg.Proxy.UI.ShowResources = constants.ShowResourcesRequestable
|
|
}
|
|
default:
|
|
return trace.BadParameter("show resources %q not supported", cfg.Proxy.UI.ShowResources)
|
|
}
|
|
}
|
|
|
|
if fc.Proxy.MySQLServerVersion != "" {
|
|
cfg.Proxy.MySQLServerVersion = fc.Proxy.MySQLServerVersion
|
|
}
|
|
|
|
if fc.Proxy.AutomaticUpgradesChannels != nil {
|
|
cfg.Proxy.AutomaticUpgradesChannels = fc.Proxy.AutomaticUpgradesChannels
|
|
} else {
|
|
cfg.Proxy.AutomaticUpgradesChannels = make(automaticupgrades.Channels)
|
|
}
|
|
if err = cfg.Proxy.AutomaticUpgradesChannels.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err, "validating the automatic upgrades configuration")
|
|
}
|
|
|
|
// This is the legacy format. Continue to support it forever, but ideally
|
|
// users now use the list format below.
|
|
if fc.Proxy.KeyFile != "" || fc.Proxy.CertFile != "" {
|
|
cfg.Proxy.KeyPairs = append(cfg.Proxy.KeyPairs, servicecfg.KeyPairPath{
|
|
PrivateKey: fc.Proxy.KeyFile,
|
|
Certificate: fc.Proxy.CertFile,
|
|
})
|
|
}
|
|
for _, p := range fc.Proxy.KeyPairs {
|
|
// Check that the certificate exists on disk. This exists to provide the
|
|
// user a sensible error message.
|
|
if !utils.FileExists(p.PrivateKey) {
|
|
return trace.Errorf("https private key does not exist: %s", p.PrivateKey)
|
|
}
|
|
if !utils.FileExists(p.Certificate) {
|
|
return trace.Errorf("https cert does not exist: %s", p.Certificate)
|
|
}
|
|
|
|
// Read in certificate from disk. If Teleport finds a self-signed
|
|
// certificate chain, log a warning, and then accept whatever certificate
|
|
// was passed. If the certificate is not self-signed, verify the certificate
|
|
// chain from leaf to root with the trust store on the computer so browsers
|
|
// don't complain.
|
|
certificateChain, err := utils.ReadCertificatesFromPath(p.Certificate)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if utils.IsSelfSigned(certificateChain) {
|
|
const warningMessage = "Starting Teleport with a self-signed TLS certificate, this is " +
|
|
"not safe for production clusters. Using a self-signed certificate opens " +
|
|
"Teleport users to Man-in-the-Middle attacks."
|
|
slog.WarnContext(context.Background(), warningMessage)
|
|
} else {
|
|
if err := utils.VerifyCertificateChain(certificateChain); err != nil {
|
|
return trace.BadParameter("unable to verify HTTPS certificate chain in %v:\n\n %s\n\n %s",
|
|
p.Certificate, proxyUntrustedTLSCertErrMsg, err)
|
|
}
|
|
}
|
|
|
|
cfg.Proxy.KeyPairs = append(cfg.Proxy.KeyPairs, servicecfg.KeyPairPath{
|
|
PrivateKey: p.PrivateKey,
|
|
Certificate: p.Certificate,
|
|
})
|
|
}
|
|
cfg.Proxy.KeyPairsReloadInterval = fc.Proxy.KeyPairsReloadInterval
|
|
|
|
// apply kubernetes proxy config, by default kube proxy is disabled
|
|
legacyKube := fc.Proxy.Kube.Configured() && fc.Proxy.Kube.Enabled()
|
|
newKube := fc.Proxy.KubeAddr != "" || len(fc.Proxy.KubePublicAddr) > 0
|
|
switch {
|
|
case legacyKube && !newKube:
|
|
cfg.Proxy.Kube.Enabled = true
|
|
cfg.Proxy.Kube.LegacyKubeProxy = true
|
|
if fc.Proxy.Kube.KubeconfigFile != "" {
|
|
cfg.Proxy.Kube.KubeconfigPath = fc.Proxy.Kube.KubeconfigFile
|
|
}
|
|
if fc.Proxy.Kube.ListenAddress != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.Kube.ListenAddress, int(defaults.KubeListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.Kube.ListenAddr = *addr
|
|
}
|
|
if len(fc.Proxy.Kube.PublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.Kube.PublicAddr, defaults.KubeListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.Kube.PublicAddrs = addrs
|
|
}
|
|
case !legacyKube && newKube:
|
|
// New kubernetes format (kubernetes_service +
|
|
// proxy_service.kube_listen_addr) is only relevant in the config file
|
|
// format. Under the hood, we use the same cfg.Proxy.Kube field to
|
|
// enable it.
|
|
if len(fc.Proxy.KubePublicAddr) > 0 && fc.Proxy.KubeAddr == "" {
|
|
return trace.BadParameter("kube_listen_addr must be set when kube_public_addr is set")
|
|
}
|
|
cfg.Proxy.Kube.Enabled = true
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.KubeAddr, int(defaults.KubeListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.Kube.ListenAddr = *addr
|
|
|
|
publicAddrs, err := utils.AddrsFromStrings(fc.Proxy.KubePublicAddr, defaults.KubeListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.Kube.PublicAddrs = publicAddrs
|
|
case legacyKube && newKube:
|
|
return trace.BadParameter("proxy_service should either set kube_listen_addr/kube_public_addr or kubernetes.enabled, not both; keep kubernetes.enabled if you don't enable kubernetes_service, or keep kube_listen_addr otherwise")
|
|
case !legacyKube && !newKube:
|
|
if fc.Version != "" && fc.Version != defaults.TeleportConfigVersionV1 {
|
|
cfg.Proxy.Kube.Enabled = true
|
|
}
|
|
}
|
|
if len(fc.Proxy.PublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.PublicAddr, cfg.Proxy.WebAddr.Port(defaults.HTTPListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.PublicAddrs = addrs
|
|
}
|
|
if len(fc.Proxy.SSHPublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.SSHPublicAddr, defaults.SSHProxyListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.SSHPublicAddrs = addrs
|
|
}
|
|
if len(fc.Proxy.TunnelPublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.TunnelPublicAddr, defaults.SSHProxyTunnelListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.TunnelPublicAddrs = addrs
|
|
}
|
|
if len(fc.Proxy.PostgresPublicAddr) != 0 {
|
|
defaultPort := getPostgresDefaultPort(cfg)
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.PostgresPublicAddr, defaultPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.PostgresPublicAddrs = addrs
|
|
}
|
|
|
|
if len(fc.Proxy.MySQLPublicAddr) != 0 {
|
|
if fc.Proxy.MySQLAddr == "" {
|
|
return trace.BadParameter("mysql_listen_addr must be set when mysql_public_addr is set")
|
|
}
|
|
// MySQL proxy is listening on a separate port.
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.MySQLPublicAddr, defaults.MySQLListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.MySQLPublicAddrs = addrs
|
|
}
|
|
|
|
if len(fc.Proxy.MongoPublicAddr) != 0 {
|
|
if fc.Proxy.MongoAddr == "" {
|
|
return trace.BadParameter("mongo_listen_addr must be set when mongo_public_addr is set")
|
|
}
|
|
addrs, err := utils.AddrsFromStrings(fc.Proxy.MongoPublicAddr, defaults.MongoListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.MongoPublicAddrs = addrs
|
|
}
|
|
if fc.Proxy.PeerPublicAddr != "" {
|
|
if fc.Proxy.PeerAddr == "" {
|
|
return trace.BadParameter("peer_listen_addr must be set when peer_public_addr is set")
|
|
}
|
|
addr, err := utils.ParseHostPortAddr(fc.Proxy.PeerPublicAddr, int(defaults.ProxyPeeringListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.PeerPublicAddr = *addr
|
|
}
|
|
|
|
cfg.Proxy.IdP.SAMLIdP.Enabled = fc.Proxy.IdP.SAMLIdP.Enabled()
|
|
cfg.Proxy.IdP.SAMLIdP.BaseURL = fc.Proxy.IdP.SAMLIdP.BaseURL
|
|
|
|
acme, err := fc.Proxy.ACME.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Proxy.ACME = *acme
|
|
cfg.Proxy.TrustXForwardedFor = fc.Proxy.TrustXForwardedFor.Value()
|
|
return nil
|
|
}
|
|
|
|
func getPostgresDefaultPort(cfg *servicecfg.Config) int {
|
|
if !cfg.Proxy.PostgresAddr.IsEmpty() {
|
|
// If the proxy.PostgresAddr flag was provided return port
|
|
// from PostgresAddr address or default PostgresListenPort.
|
|
return cfg.Proxy.PostgresAddr.Port(defaults.PostgresListenPort)
|
|
}
|
|
// Postgres proxy is multiplexed on the web proxy port. If the proxy is
|
|
// not specified here explicitly, prefer defaults in the following
|
|
// order, depending on what's set:
|
|
// 1. Web proxy public port
|
|
// 2. Web proxy listen port
|
|
// 3. Web proxy default listen port
|
|
if len(cfg.Proxy.PublicAddrs) != 0 {
|
|
return cfg.Proxy.PublicAddrs[0].Port(defaults.HTTPListenPort)
|
|
}
|
|
return cfg.Proxy.WebAddr.Port(defaults.HTTPListenPort)
|
|
}
|
|
|
|
func applyDefaultProxyListenerAddresses(cfg *servicecfg.Config) {
|
|
// From v2 onwards if an address is not provided don't fall back to the default values.
|
|
if cfg.Version != "" && cfg.Version != defaults.TeleportConfigVersionV1 {
|
|
return
|
|
}
|
|
|
|
// For v1 configuration check if address was set in config file if
|
|
// not fallback to the default listener address.
|
|
if cfg.Proxy.WebAddr.IsEmpty() {
|
|
cfg.Proxy.WebAddr = *defaults.ProxyWebListenAddr()
|
|
}
|
|
if cfg.Proxy.SSHAddr.IsEmpty() {
|
|
cfg.Proxy.SSHAddr = *defaults.ProxyListenAddr()
|
|
}
|
|
if cfg.Proxy.ReverseTunnelListenAddr.IsEmpty() {
|
|
cfg.Proxy.ReverseTunnelListenAddr = *defaults.ReverseTunnelListenAddr()
|
|
}
|
|
if cfg.Proxy.Kube.ListenAddr.IsEmpty() && cfg.Proxy.Kube.Enabled {
|
|
cfg.Proxy.Kube.ListenAddr = *defaults.KubeProxyListenAddr()
|
|
}
|
|
}
|
|
|
|
// applySSHConfig applies file configuration for the "ssh_service" section.
|
|
func applySSHConfig(fc *FileConfig, cfg *servicecfg.Config) (err error) {
|
|
if fc.SSH.ListenAddress != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.SSH.ListenAddress, int(defaults.SSHServerListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.SSH.Addr = *addr
|
|
}
|
|
if fc.SSH.Labels != nil {
|
|
cfg.SSH.Labels = maps.Clone(fc.SSH.Labels)
|
|
}
|
|
if fc.SSH.Commands != nil {
|
|
cfg.SSH.CmdLabels = make(services.CommandLabels)
|
|
for _, cmdLabel := range fc.SSH.Commands {
|
|
cfg.SSH.CmdLabels[cmdLabel.Name] = &types.CommandLabelV2{
|
|
Period: types.NewDuration(cmdLabel.Period),
|
|
Command: cmdLabel.Command,
|
|
Result: "",
|
|
}
|
|
}
|
|
}
|
|
if fc.SSH.Namespace != "" {
|
|
cfg.SSH.Namespace = fc.SSH.Namespace
|
|
}
|
|
if fc.SSH.PermitUserEnvironment {
|
|
cfg.SSH.PermitUserEnvironment = true
|
|
}
|
|
if fc.SSH.DisableCreateHostUser || runtime.GOOS != constants.LinuxOS {
|
|
cfg.SSH.DisableCreateHostUser = true
|
|
if runtime.GOOS != constants.LinuxOS {
|
|
slog.DebugContext(context.Background(), "Disabling host user creation as this feature is only available on Linux")
|
|
}
|
|
}
|
|
if fc.SSH.PAM != nil {
|
|
cfg.SSH.PAM = fc.SSH.PAM.Parse()
|
|
}
|
|
if len(fc.SSH.PublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.SSH.PublicAddr, defaults.SSHServerListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.SSH.PublicAddrs = addrs
|
|
}
|
|
if fc.SSH.BPF != nil {
|
|
cfg.SSH.BPF = fc.SSH.BPF.Parse()
|
|
}
|
|
if fc.SSH.RestrictedSession != nil {
|
|
slog.ErrorContext(context.Background(), "Restricted Sessions for SSH were removed in Teleport 15")
|
|
}
|
|
|
|
cfg.SSH.AllowTCPForwarding = fc.SSH.AllowTCPForwarding()
|
|
|
|
cfg.SSH.X11, err = fc.SSH.X11ServerConfig()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.SSH.AllowFileCopying = fc.SSH.SSHFileCopy()
|
|
|
|
cfg.SSH.ForceListen = fc.SSH.ForceListen
|
|
|
|
return nil
|
|
}
|
|
|
|
// getInstallerProxyAddr determines the address of the proxy for discovered
|
|
// nodes to connect to.
|
|
func getInstallerProxyAddr(fc *FileConfig) string {
|
|
// Proxy address from config.
|
|
if fc.ProxyServer != "" {
|
|
return fc.ProxyServer
|
|
}
|
|
if fc.Proxy.Enabled() && len(fc.Proxy.PublicAddr) > 0 {
|
|
return fc.Proxy.PublicAddr[0]
|
|
}
|
|
// Possible proxy address for v1/v2 config.
|
|
if len(fc.AuthServers) > 0 {
|
|
return fc.AuthServers[0]
|
|
}
|
|
// Probably not a proxy address, but we have nothing better.
|
|
return fc.AuthServer
|
|
}
|
|
|
|
func applyDiscoveryConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
cfg.Discovery.Enabled = fc.Discovery.Enabled()
|
|
cfg.Discovery.DiscoveryGroup = fc.Discovery.DiscoveryGroup
|
|
cfg.Discovery.PollInterval = fc.Discovery.PollInterval
|
|
|
|
defaultProxyAddr := getInstallerProxyAddr(fc)
|
|
|
|
for _, matcher := range fc.Discovery.AWSMatchers {
|
|
var err error
|
|
var installParams *types.InstallerParams
|
|
if matcher.InstallParams != nil {
|
|
installParams, err = matcher.InstallParams.parse(defaultProxyAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
var assumeRole *types.AssumeRole
|
|
if matcher.AssumeRoleARN != "" || matcher.ExternalID != "" || matcher.AssumeRoleName != "" {
|
|
assumeRole = &types.AssumeRole{
|
|
RoleARN: matcher.AssumeRoleARN,
|
|
RoleName: matcher.AssumeRoleName,
|
|
ExternalID: matcher.ExternalID,
|
|
}
|
|
}
|
|
|
|
for _, region := range matcher.Regions {
|
|
if region == types.Wildcard {
|
|
continue
|
|
}
|
|
|
|
if !awsregion.IsKnownRegion(region) {
|
|
const message = "AWS matcher uses unknown region" +
|
|
"This is either a typo or a new AWS region that is unknown to the AWS SDK used to compile this binary. "
|
|
slog.WarnContext(context.Background(), message,
|
|
"region", region,
|
|
"known_regions", awsregion.GetKnownRegions(),
|
|
)
|
|
}
|
|
}
|
|
|
|
var organizationMatcher *types.AWSOrganizationMatcher
|
|
if matcher.Organization != nil {
|
|
organizationMatcher = &types.AWSOrganizationMatcher{
|
|
OrganizationID: matcher.Organization.OrganizationID,
|
|
OrganizationalUnits: &types.AWSOrganizationUnitsMatcher{
|
|
Include: matcher.Organization.OrganizationalUnits.Include,
|
|
Exclude: matcher.Organization.OrganizationalUnits.Exclude,
|
|
},
|
|
}
|
|
}
|
|
|
|
var ssm *types.AWSSSM
|
|
if matcher.SSM.DocumentName != "" {
|
|
ssm = &types.AWSSSM{DocumentName: matcher.SSM.DocumentName}
|
|
}
|
|
|
|
serviceMatcher := types.AWSMatcher{
|
|
Types: matcher.Types,
|
|
Regions: matcher.Regions,
|
|
AssumeRole: assumeRole,
|
|
Tags: matcher.Tags,
|
|
Params: installParams,
|
|
SSM: ssm,
|
|
Integration: matcher.Integration,
|
|
KubeAppDiscovery: matcher.KubeAppDiscovery,
|
|
SetupAccessForARN: matcher.SetupAccessForARN,
|
|
Organization: organizationMatcher,
|
|
}
|
|
if err := serviceMatcher.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Discovery.AWSMatchers = append(cfg.Discovery.AWSMatchers, serviceMatcher)
|
|
}
|
|
|
|
for _, matcher := range fc.Discovery.AzureMatchers {
|
|
var installParams *types.InstallerParams
|
|
var err error
|
|
if slices.Contains(matcher.Types, types.AzureMatcherVM) {
|
|
// Backwards compatibility for Azure VM matcher:
|
|
// If install_teleport param is not set, default to false.
|
|
if matcher.InstallParams == nil {
|
|
installParams = &types.InstallerParams{
|
|
PublicProxyAddr: defaultProxyAddr,
|
|
}
|
|
}
|
|
|
|
if matcher.InstallParams != nil {
|
|
installParams, err = matcher.InstallParams.parse(defaultProxyAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matcher.InstallParams.InstallTeleport == "" {
|
|
installParams.InstallTeleport = false
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
serviceMatcher := types.AzureMatcher{
|
|
Subscriptions: matcher.Subscriptions,
|
|
ResourceGroups: matcher.ResourceGroups,
|
|
Types: matcher.Types,
|
|
Regions: matcher.Regions,
|
|
ResourceTags: matcher.ResourceTags,
|
|
Integration: matcher.Integration,
|
|
Params: installParams,
|
|
}
|
|
if err := serviceMatcher.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Discovery.AzureMatchers = append(cfg.Discovery.AzureMatchers, serviceMatcher)
|
|
}
|
|
|
|
for _, matcher := range fc.Discovery.GCPMatchers {
|
|
var installParams *types.InstallerParams
|
|
var err error
|
|
if slices.Contains(matcher.Types, types.GCPMatcherCompute) { // Backwards compatibility for GCP VM matcher:
|
|
// If install_teleport param is not set, default to false.
|
|
if matcher.InstallParams == nil {
|
|
installParams = &types.InstallerParams{
|
|
PublicProxyAddr: defaultProxyAddr,
|
|
}
|
|
}
|
|
|
|
if matcher.InstallParams != nil {
|
|
installParams, err = matcher.InstallParams.parse(defaultProxyAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if matcher.InstallParams.InstallTeleport == "" {
|
|
installParams.InstallTeleport = false
|
|
}
|
|
}
|
|
}
|
|
|
|
serviceMatcher := types.GCPMatcher{
|
|
Types: matcher.Types,
|
|
Locations: matcher.Locations,
|
|
Labels: matcher.Labels,
|
|
Tags: matcher.Tags,
|
|
ProjectIDs: matcher.ProjectIDs,
|
|
ServiceAccounts: matcher.ServiceAccounts,
|
|
Params: installParams,
|
|
}
|
|
if err := serviceMatcher.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Discovery.GCPMatchers = append(cfg.Discovery.GCPMatchers, serviceMatcher)
|
|
}
|
|
|
|
if len(fc.Discovery.KubernetesMatchers) > 0 {
|
|
if fc.Discovery.DiscoveryGroup == "" {
|
|
// TODO(anton): add link to documentation when it's available
|
|
return trace.BadParameter(`parameter 'discovery_group' should be defined for discovery service if
|
|
kubernetes matchers are present`)
|
|
}
|
|
}
|
|
for _, matcher := range fc.Discovery.KubernetesMatchers {
|
|
serviceMatcher := types.KubernetesMatcher{
|
|
Types: matcher.Types,
|
|
Namespaces: matcher.Namespaces,
|
|
Labels: matcher.Labels,
|
|
}
|
|
if err := serviceMatcher.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Discovery.KubernetesMatchers = append(cfg.Discovery.KubernetesMatchers, serviceMatcher)
|
|
}
|
|
|
|
if fc.Discovery.AccessGraph != nil {
|
|
var tMatcher types.AccessGraphSync
|
|
for _, awsMatcher := range fc.Discovery.AccessGraph.AWS {
|
|
regions := awsMatcher.Regions
|
|
if len(regions) == 0 {
|
|
return trace.BadParameter("missing regions in access_graph")
|
|
}
|
|
var assumeRole *types.AssumeRole
|
|
if awsMatcher.AssumeRoleARN != "" || awsMatcher.ExternalID != "" {
|
|
assumeRole = &types.AssumeRole{
|
|
RoleARN: awsMatcher.AssumeRoleARN,
|
|
ExternalID: awsMatcher.ExternalID,
|
|
}
|
|
}
|
|
var cloudTrailLogs *types.AccessGraphAWSSyncCloudTrailLogs
|
|
if awsMatcher.CloudTrailLogs != nil {
|
|
cloudTrailLogs = &types.AccessGraphAWSSyncCloudTrailLogs{
|
|
SQSQueue: awsMatcher.CloudTrailLogs.QueueURL,
|
|
Region: awsMatcher.CloudTrailLogs.QueueRegion,
|
|
}
|
|
}
|
|
var eksAuditLogs *types.AccessGraphAWSSyncEKSAuditLogs
|
|
if awsMatcher.EKSAuditLogs != nil {
|
|
eksAuditLogs = &types.AccessGraphAWSSyncEKSAuditLogs{
|
|
Tags: awsMatcher.EKSAuditLogs.Tags,
|
|
}
|
|
}
|
|
|
|
tMatcher.AWS = append(tMatcher.AWS, &types.AccessGraphAWSSync{
|
|
Regions: regions,
|
|
AssumeRole: assumeRole,
|
|
CloudTrailLogs: cloudTrailLogs,
|
|
EksAuditLogs: eksAuditLogs,
|
|
})
|
|
}
|
|
for _, azureMatcher := range fc.Discovery.AccessGraph.Azure {
|
|
subscriptionID := azureMatcher.SubscriptionID
|
|
tMatcher.Azure = append(tMatcher.Azure, &types.AccessGraphAzureSync{
|
|
SubscriptionID: subscriptionID,
|
|
})
|
|
}
|
|
if fc.Discovery.AccessGraph.PollInterval > 0 {
|
|
tMatcher.PollInterval = fc.Discovery.AccessGraph.PollInterval
|
|
}
|
|
cfg.Discovery.AccessGraph = &tMatcher
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyKubeConfig applies file configuration for the "kubernetes_service" section.
|
|
func applyKubeConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
if fc.Kube.ListenAddress != "" {
|
|
addr, err := utils.ParseHostPortAddr(fc.Kube.ListenAddress, int(defaults.SSHProxyListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Kube.ListenAddr = addr
|
|
}
|
|
if len(fc.Kube.PublicAddr) != 0 {
|
|
addrs, err := utils.AddrsFromStrings(fc.Kube.PublicAddr, defaults.KubeListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Kube.PublicAddrs = addrs
|
|
}
|
|
|
|
for _, matcher := range fc.Kube.ResourceMatchers {
|
|
cfg.Kube.ResourceMatchers = append(cfg.Kube.ResourceMatchers,
|
|
services.ResourceMatcher{
|
|
Labels: matcher.Labels,
|
|
AWS: services.ResourceMatcherAWS{
|
|
AssumeRoleARN: matcher.AWS.AssumeRoleARN,
|
|
ExternalID: matcher.AWS.ExternalID,
|
|
},
|
|
})
|
|
}
|
|
|
|
if fc.Kube.KubeconfigFile != "" {
|
|
cfg.Kube.KubeconfigPath = fc.Kube.KubeconfigFile
|
|
}
|
|
if fc.Kube.KubeClusterName != "" {
|
|
cfg.Kube.KubeClusterName = fc.Kube.KubeClusterName
|
|
}
|
|
if fc.Kube.StaticLabels != nil {
|
|
cfg.Kube.StaticLabels = maps.Clone(fc.Kube.StaticLabels)
|
|
}
|
|
if fc.Kube.DynamicLabels != nil {
|
|
cfg.Kube.DynamicLabels = make(services.CommandLabels)
|
|
for _, cmdLabel := range fc.Kube.DynamicLabels {
|
|
cfg.Kube.DynamicLabels[cmdLabel.Name] = &types.CommandLabelV2{
|
|
Period: types.NewDuration(cmdLabel.Period),
|
|
Command: cmdLabel.Command,
|
|
Result: "",
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyDatabasesConfig applies file configuration for the "db_service" section.
|
|
func applyDatabasesConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
cfg.Databases.Enabled = true
|
|
for _, matcher := range fc.Databases.ResourceMatchers {
|
|
cfg.Databases.ResourceMatchers = append(cfg.Databases.ResourceMatchers,
|
|
services.ResourceMatcher{
|
|
Labels: matcher.Labels,
|
|
AWS: services.ResourceMatcherAWS{
|
|
AssumeRoleARN: matcher.AWS.AssumeRoleARN,
|
|
ExternalID: matcher.AWS.ExternalID,
|
|
},
|
|
})
|
|
}
|
|
for _, matcher := range fc.Databases.AWSMatchers {
|
|
cfg.Databases.AWSMatchers = append(cfg.Databases.AWSMatchers,
|
|
types.AWSMatcher{
|
|
Types: matcher.Types,
|
|
Regions: matcher.Regions,
|
|
Tags: matcher.Tags,
|
|
AssumeRole: &types.AssumeRole{
|
|
RoleARN: matcher.AssumeRoleARN,
|
|
ExternalID: matcher.ExternalID,
|
|
},
|
|
Integration: matcher.Integration,
|
|
})
|
|
}
|
|
for _, matcher := range fc.Databases.AzureMatchers {
|
|
cfg.Databases.AzureMatchers = append(cfg.Databases.AzureMatchers,
|
|
types.AzureMatcher{
|
|
Subscriptions: matcher.Subscriptions,
|
|
ResourceGroups: matcher.ResourceGroups,
|
|
Types: matcher.Types,
|
|
Regions: matcher.Regions,
|
|
ResourceTags: matcher.ResourceTags,
|
|
Integration: matcher.Integration,
|
|
})
|
|
}
|
|
for _, database := range fc.Databases.Databases {
|
|
staticLabels := make(map[string]string)
|
|
if database.StaticLabels != nil {
|
|
staticLabels = database.StaticLabels
|
|
}
|
|
dynamicLabels := make(services.CommandLabels)
|
|
if database.DynamicLabels != nil {
|
|
for _, v := range database.DynamicLabels {
|
|
dynamicLabels[v.Name] = &types.CommandLabelV2{
|
|
Period: types.NewDuration(v.Period),
|
|
Command: v.Command,
|
|
Result: "",
|
|
}
|
|
}
|
|
}
|
|
|
|
caBytes, err := readCACert(database)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if err = gcputils.ValidateAlloyDBEndpointType(database.GCP.AlloyDB.EndpointType); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
db := servicecfg.Database{
|
|
Name: database.Name,
|
|
Description: database.Description,
|
|
Protocol: database.Protocol,
|
|
URI: database.URI,
|
|
StaticLabels: staticLabels,
|
|
DynamicLabels: dynamicLabels,
|
|
MySQL: servicecfg.MySQLOptions{
|
|
ServerVersion: database.MySQL.ServerVersion,
|
|
},
|
|
TLS: servicecfg.DatabaseTLS{
|
|
CACert: caBytes,
|
|
ServerName: database.TLS.ServerName,
|
|
Mode: servicecfg.TLSMode(database.TLS.Mode),
|
|
TrustSystemCertPool: database.TLS.TrustSystemCertPool,
|
|
},
|
|
AdminUser: servicecfg.DatabaseAdminUser{
|
|
Name: database.AdminUser.Name,
|
|
DefaultDatabase: database.AdminUser.DefaultDatabase,
|
|
},
|
|
Oracle: convOracleOptions(database.Oracle),
|
|
AWS: servicecfg.DatabaseAWS{
|
|
AccountID: database.AWS.AccountID,
|
|
AssumeRoleARN: database.AWS.AssumeRoleARN,
|
|
ExternalID: database.AWS.ExternalID,
|
|
Region: database.AWS.Region,
|
|
SessionTags: database.AWS.SessionTags,
|
|
Redshift: servicecfg.DatabaseAWSRedshift{
|
|
ClusterID: database.AWS.Redshift.ClusterID,
|
|
},
|
|
RedshiftServerless: servicecfg.DatabaseAWSRedshiftServerless{
|
|
WorkgroupName: database.AWS.RedshiftServerless.WorkgroupName,
|
|
EndpointName: database.AWS.RedshiftServerless.EndpointName,
|
|
},
|
|
RDS: servicecfg.DatabaseAWSRDS{
|
|
InstanceID: database.AWS.RDS.InstanceID,
|
|
ClusterID: database.AWS.RDS.ClusterID,
|
|
},
|
|
ElastiCache: servicecfg.DatabaseAWSElastiCache{
|
|
ReplicationGroupID: database.AWS.ElastiCache.ReplicationGroupID,
|
|
},
|
|
ElastiCacheServerless: servicecfg.DatabaseAWSElastiCacheServerless{
|
|
CacheName: database.AWS.ElastiCacheServerless.CacheName,
|
|
},
|
|
MemoryDB: servicecfg.DatabaseAWSMemoryDB{
|
|
ClusterName: database.AWS.MemoryDB.ClusterName,
|
|
},
|
|
SecretStore: servicecfg.DatabaseAWSSecretStore{
|
|
KeyPrefix: database.AWS.SecretStore.KeyPrefix,
|
|
KMSKeyID: database.AWS.SecretStore.KMSKeyID,
|
|
},
|
|
},
|
|
GCP: servicecfg.DatabaseGCP{
|
|
ProjectID: database.GCP.ProjectID,
|
|
InstanceID: database.GCP.InstanceID,
|
|
AlloyDB: servicecfg.DatabaseGCPAlloyDB{
|
|
EndpointType: database.GCP.AlloyDB.EndpointType,
|
|
EndpointOverride: database.GCP.AlloyDB.EndpointOverride,
|
|
},
|
|
},
|
|
AD: servicecfg.DatabaseAD{
|
|
KeytabFile: database.AD.KeytabFile,
|
|
Krb5File: database.AD.Krb5File,
|
|
Domain: database.AD.Domain,
|
|
SPN: database.AD.SPN,
|
|
LDAPCert: database.AD.LDAPCert,
|
|
KDCHostName: database.AD.KDCHostName,
|
|
LDAPServiceAccountName: database.AD.LDAPServiceAccountName,
|
|
LDAPServiceAccountSID: database.AD.LDAPServiceAccountSID,
|
|
},
|
|
Azure: servicecfg.DatabaseAzure{
|
|
ResourceID: database.Azure.ResourceID,
|
|
IsFlexiServer: database.Azure.IsFlexiServer,
|
|
},
|
|
}
|
|
if err := db.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Databases.Databases = append(cfg.Databases.Databases, db)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convOracleOptions(o DatabaseOracle) servicecfg.OracleOptions {
|
|
return servicecfg.OracleOptions{
|
|
AuditUser: o.AuditUser,
|
|
RetryCount: o.RetryCount,
|
|
ShuffleHostnames: o.ShuffleHostnames,
|
|
}
|
|
}
|
|
|
|
// readCACert reads database CA certificate from the config file.
|
|
// First 'tls.ca_cert_file` is being read, then deprecated 'ca_cert_file' if
|
|
// the first one is not set.
|
|
func readCACert(database *Database) ([]byte, error) {
|
|
var (
|
|
caBytes []byte
|
|
err error
|
|
)
|
|
if database.TLS.CACertFile != "" {
|
|
caBytes, err = os.ReadFile(database.TLS.CACertFile)
|
|
if err != nil {
|
|
return nil, trace.ConvertSystemError(err)
|
|
}
|
|
}
|
|
|
|
// ca_cert_file is deprecated, but we still support it.
|
|
// Print a warning if the old field is still being used.
|
|
if database.CACertFile != "" {
|
|
if database.TLS.CACertFile != "" {
|
|
// New and old fields are set. Ignore the old field.
|
|
slog.WarnContext(context.Background(), "Ignoring deprecated ca_cert_file database in configuration; using tls.ca_cert_file", "database", database.Name)
|
|
} else {
|
|
// Only old field is set, inform about deprecation.
|
|
slog.WarnContext(context.Background(), "ca_cert_file is deprecated, please use tls.ca_cert_file instead for databases", "database", database.Name)
|
|
|
|
caBytes, err = os.ReadFile(database.CACertFile)
|
|
if err != nil {
|
|
return nil, trace.ConvertSystemError(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return caBytes, nil
|
|
}
|
|
|
|
// applyAppsConfig applies file configuration for the "app_service" section.
|
|
func applyAppsConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
// Apps are enabled.
|
|
cfg.Apps.Enabled = true
|
|
|
|
// Log when proxy_service is enabled in the same config but has no
|
|
// public_addr. The proxy falls back to the cluster name for the
|
|
// auth redirect. Per-app public_addr controls the app's FQDN but
|
|
// does not replace this.
|
|
if fc.Proxy.Enabled() && len(fc.Proxy.PublicAddr) == 0 {
|
|
slog.InfoContext(context.Background(),
|
|
"proxy_service.public_addr not set; using cluster name for app auth redirects",
|
|
"nodename", cfg.Hostname)
|
|
}
|
|
|
|
// Enable debugging application if requested.
|
|
cfg.Apps.DebugApp = fc.Apps.DebugApp
|
|
|
|
// Enable the "Teleport Demo" MCP server if requested.
|
|
cfg.Apps.MCPDemoServer = fc.Apps.MCPDemoServer
|
|
|
|
// Configure resource watcher selectors if present.
|
|
for _, matcher := range fc.Apps.ResourceMatchers {
|
|
if matcher.AWS.AssumeRoleARN != "" {
|
|
return trace.NotImplemented("assume_role_arn is not supported for app resource matchers")
|
|
}
|
|
cfg.Apps.ResourceMatchers = append(cfg.Apps.ResourceMatchers,
|
|
services.ResourceMatcher{
|
|
Labels: matcher.Labels,
|
|
})
|
|
}
|
|
|
|
// Loop over all apps and load app configuration.
|
|
for _, application := range fc.Apps.Apps {
|
|
// Parse the static labels of the application.
|
|
staticLabels := make(map[string]string)
|
|
if application.StaticLabels != nil {
|
|
staticLabels = application.StaticLabels
|
|
}
|
|
|
|
// Parse the dynamic labels of the application.
|
|
dynamicLabels := make(services.CommandLabels)
|
|
if application.DynamicLabels != nil {
|
|
for _, v := range application.DynamicLabels {
|
|
dynamicLabels[v.Name] = &types.CommandLabelV2{
|
|
Period: types.NewDuration(v.Period),
|
|
Command: v.Command,
|
|
}
|
|
}
|
|
}
|
|
|
|
// Add the application to the list of proxied applications.
|
|
app := servicecfg.App{
|
|
Name: application.Name,
|
|
Description: application.Description,
|
|
URI: application.URI,
|
|
PublicAddr: application.PublicAddr,
|
|
StaticLabels: staticLabels,
|
|
DynamicLabels: dynamicLabels,
|
|
InsecureSkipVerify: application.InsecureSkipVerify,
|
|
Cloud: application.Cloud,
|
|
RequiredAppNames: application.RequiredApps,
|
|
UseAnyProxyPublicAddr: application.UseAnyProxyPublicAddr,
|
|
}
|
|
|
|
if application.CORS != nil {
|
|
app.CORS = &servicecfg.CORS{
|
|
AllowedOrigins: application.CORS.AllowedOrigins,
|
|
AllowedMethods: application.CORS.AllowedMethods,
|
|
AllowedHeaders: application.CORS.AllowedHeaders,
|
|
ExposedHeaders: application.CORS.ExposedHeaders,
|
|
AllowCredentials: application.CORS.AllowCredentials,
|
|
MaxAge: application.CORS.MaxAge,
|
|
}
|
|
}
|
|
|
|
if application.Rewrite != nil {
|
|
// Parse http rewrite headers if there are any.
|
|
headers, err := servicecfg.ParseHeaders(application.Rewrite.Headers)
|
|
if err != nil {
|
|
return trace.Wrap(err, "failed to parse headers rewrite configuration for app %q",
|
|
application.Name)
|
|
}
|
|
app.Rewrite = &servicecfg.Rewrite{
|
|
Redirect: application.Rewrite.Redirect,
|
|
Headers: headers,
|
|
JWTClaims: application.Rewrite.JWTClaims,
|
|
}
|
|
}
|
|
if application.AWS != nil {
|
|
app.AWS = &servicecfg.AppAWS{
|
|
ExternalID: application.AWS.ExternalID,
|
|
}
|
|
}
|
|
|
|
if len(application.TCPPorts) != 0 {
|
|
app.TCPPorts = make([]servicecfg.PortRange, 0, len(application.TCPPorts))
|
|
for _, portRange := range application.TCPPorts {
|
|
app.TCPPorts = append(app.TCPPorts, servicecfg.PortRange{
|
|
Port: portRange.Port,
|
|
EndPort: portRange.EndPort,
|
|
})
|
|
}
|
|
}
|
|
|
|
if application.MCP != nil {
|
|
app.MCP = &types.MCP{
|
|
Command: application.MCP.Command,
|
|
Args: application.MCP.Args,
|
|
RunAsHostUser: application.MCP.RunAsHostUser,
|
|
}
|
|
}
|
|
|
|
if application.LLM != nil {
|
|
app.LLM = &types.LLM{
|
|
Format: application.LLM.Format,
|
|
Provider: application.LLM.Provider,
|
|
FallbackModel: application.LLM.FallbackModel,
|
|
}
|
|
app.LLM.Models = make([]*types.LLM_Model, 0, len(application.LLM.Models))
|
|
for _, model := range application.LLM.Models {
|
|
app.LLM.Models = append(app.LLM.Models, &types.LLM_Model{
|
|
Name: model.Name,
|
|
ProviderName: model.ProviderName,
|
|
})
|
|
}
|
|
}
|
|
|
|
if application.TLS != nil {
|
|
app.TLS = &types.AppTLS{
|
|
Mode: application.TLS.Mode,
|
|
ServerName: application.TLS.ServerName,
|
|
ServerSpiffeId: application.TLS.ServerSpiffeId,
|
|
AllowedCas: application.TLS.AllowedCas,
|
|
ClientCertMode: application.TLS.ClientCertMode,
|
|
}
|
|
for _, caCertPath := range application.TLS.AllowedCasFiles {
|
|
caCertContents, err := os.ReadFile(caCertPath)
|
|
if err != nil {
|
|
return trace.ConvertSystemError(err)
|
|
}
|
|
app.TLS.AllowedCas = append(app.TLS.AllowedCas, string(caCertContents))
|
|
}
|
|
}
|
|
|
|
if err := app.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Apps.Apps = append(cfg.Apps.Apps, app)
|
|
}
|
|
|
|
// Reject literal duplicates: at registration two entries with the
|
|
// same name would write to the same backend key (host_id + name)
|
|
// and the second would silently overwrite the first.
|
|
seenNames := make(map[string]struct{}, len(cfg.Apps.Apps))
|
|
for _, app := range cfg.Apps.Apps {
|
|
if _, ok := seenNames[app.Name]; ok {
|
|
return trace.BadParameter("duplicate application name %q in static config", app.Name)
|
|
}
|
|
seenNames[app.Name] = struct{}{}
|
|
}
|
|
|
|
// Reject apps with the same effective routing FQDN; otherwise
|
|
// duplicates route non-deterministically. Mirror FindPublicAddr's
|
|
// fallback: use the proxy public_addr host as-is (IPs included),
|
|
// fall back to cluster_name when unset.
|
|
seenProxyHosts := map[string]struct{}{}
|
|
proxyHosts := make([]string, 0, len(cfg.Proxy.PublicAddrs))
|
|
clusterNameFallback := ""
|
|
if fc.Proxy.Enabled() {
|
|
for _, addr := range cfg.Proxy.PublicAddrs {
|
|
host, err := normalizeFQDN(addr.Host())
|
|
if err != nil {
|
|
// A malformed proxy public_addr that no app
|
|
// depends on should not block app_service startup.
|
|
slog.WarnContext(context.Background(), "skipping malformed proxy public_addr in app FQDN dedupe",
|
|
"proxy_public_addr", addr.String(),
|
|
"error", err,
|
|
)
|
|
continue
|
|
}
|
|
if host == "" {
|
|
continue
|
|
}
|
|
if _, ok := seenProxyHosts[host]; ok {
|
|
continue
|
|
}
|
|
seenProxyHosts[host] = struct{}{}
|
|
proxyHosts = append(proxyHosts, host)
|
|
}
|
|
if cfg.Auth.ClusterName != nil {
|
|
host, err := normalizeFQDN(cfg.Auth.ClusterName.GetClusterName())
|
|
if err != nil {
|
|
// A malformed cluster_name that no app falls back
|
|
// to should not block app_service startup.
|
|
slog.WarnContext(context.Background(), "skipping malformed cluster_name in app FQDN dedupe",
|
|
"cluster_name", cfg.Auth.ClusterName.GetClusterName(),
|
|
"error", err,
|
|
)
|
|
} else {
|
|
clusterNameFallback = host
|
|
}
|
|
}
|
|
}
|
|
// CheckAndSetDefaults validates app.Name and app.PublicAddr as
|
|
// DNS-1123 forms, so they are already lowercase ASCII without a
|
|
// trailing dot; only proxy public_addr and cluster_name need
|
|
// normalizeFQDN.
|
|
seenFQDNs := map[string]string{}
|
|
for _, app := range cfg.Apps.Apps {
|
|
// Dedupe per-app: one app contributing both public_addr and
|
|
// <name>.<proxy> is not a self-conflict.
|
|
appFQDNs := map[string]struct{}{}
|
|
if app.PublicAddr != "" {
|
|
appFQDNs[app.PublicAddr] = struct{}{}
|
|
}
|
|
if app.PublicAddr == "" || app.UseAnyProxyPublicAddr {
|
|
switch {
|
|
case len(proxyHosts) > 0:
|
|
for _, host := range proxyHosts {
|
|
appFQDNs[utils.DefaultAppFQDN(app.Name, host, "")] = struct{}{}
|
|
}
|
|
case clusterNameFallback != "":
|
|
appFQDNs[utils.DefaultAppFQDN(app.Name, "", clusterNameFallback)] = struct{}{}
|
|
}
|
|
}
|
|
// Map order is random; sort for a stable error message.
|
|
for _, fqdn := range slices.Sorted(maps.Keys(appFQDNs)) {
|
|
if seenAppName, ok := seenFQDNs[fqdn]; ok {
|
|
return trace.BadParameter("apps %q and %q route to the same FQDN %q in static config", seenAppName, app.Name, fqdn)
|
|
}
|
|
seenFQDNs[fqdn] = app.Name
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// normalizeFQDN returns host as ASCII, lowercase, with any trailing
|
|
// dot stripped, so IDN, case, and trailing-dot variants collapse to
|
|
// one dedupe key.
|
|
func normalizeFQDN(host string) (string, error) {
|
|
asciiHost, err := idna.ToASCII(strings.TrimRight(host, "."))
|
|
if err != nil {
|
|
return "", trace.Wrap(err)
|
|
}
|
|
return strings.ToLower(asciiHost), nil
|
|
}
|
|
|
|
// applyMetricsConfig applies file configuration for the "metrics_service" section.
|
|
func applyMetricsConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
// Metrics is enabled.
|
|
cfg.Metrics.Enabled = true
|
|
|
|
addr, err := utils.ParseHostPortAddr(fc.Metrics.ListenAddress, int(defaults.MetricsListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Metrics.ListenAddr = addr
|
|
|
|
cfg.Metrics.GRPCServerLatency = fc.Metrics.GRPCServerLatency
|
|
cfg.Metrics.GRPCClientLatency = fc.Metrics.GRPCClientLatency
|
|
|
|
if !fc.Metrics.MTLSEnabled() {
|
|
return nil
|
|
}
|
|
|
|
cfg.Metrics.MTLS = true
|
|
|
|
if len(fc.Metrics.KeyPairs) == 0 {
|
|
return trace.BadParameter("at least one keypair should be provided when mtls is enabled in the metrics config")
|
|
}
|
|
|
|
if len(fc.Metrics.CACerts) == 0 {
|
|
return trace.BadParameter("at least one CA cert should be provided when mtls is enabled in the metrics config")
|
|
}
|
|
|
|
for _, p := range fc.Metrics.KeyPairs {
|
|
// Check that the certificate exists on disk. This exists to provide the
|
|
// user a sensible error message.
|
|
if !utils.FileExists(p.PrivateKey) {
|
|
return trace.NotFound("metrics service private key does not exist: %s", p.PrivateKey)
|
|
}
|
|
if !utils.FileExists(p.Certificate) {
|
|
return trace.NotFound("metrics service cert does not exist: %s", p.Certificate)
|
|
}
|
|
|
|
certificateChain, err := utils.ReadCertificatesFromPath(p.Certificate)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
if !utils.IsSelfSigned(certificateChain) {
|
|
if err := utils.VerifyCertificateChain(certificateChain); err != nil {
|
|
return trace.BadParameter("unable to verify the metrics service certificate chain in %v: %s",
|
|
p.Certificate, utils.UserMessageFromError(err))
|
|
}
|
|
}
|
|
|
|
cfg.Metrics.KeyPairs = append(cfg.Metrics.KeyPairs, servicecfg.KeyPairPath{
|
|
PrivateKey: p.PrivateKey,
|
|
Certificate: p.Certificate,
|
|
})
|
|
}
|
|
|
|
for _, caCert := range fc.Metrics.CACerts {
|
|
// Check that the certificate exists on disk. This exists to provide the
|
|
// user a sensible error message.
|
|
if !utils.FileExists(caCert) {
|
|
return trace.NotFound("metrics service ca cert does not exist: %s", caCert)
|
|
}
|
|
|
|
cfg.Metrics.CACerts = append(cfg.Metrics.CACerts, caCert)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyWindowsDesktopConfig applies file configuration for the "windows_desktop_service" section.
|
|
func applyWindowsDesktopConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
if err := fc.WindowsDesktop.Check(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.WindowsDesktop.Enabled = true
|
|
|
|
if fc.WindowsDesktop.ListenAddress != "" {
|
|
listenAddr, err := utils.ParseHostPortAddr(fc.WindowsDesktop.ListenAddress, int(defaults.WindowsDesktopListenPort))
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.WindowsDesktop.ListenAddr = *listenAddr
|
|
}
|
|
|
|
for _, attributeName := range fc.WindowsDesktop.Discovery.LabelAttributes {
|
|
if !types.IsValidLabelKey(attributeName) {
|
|
return trace.BadParameter("WindowsDesktopService specifies label_attribute %q which is not a valid label key", attributeName)
|
|
}
|
|
}
|
|
|
|
for _, filter := range fc.WindowsDesktop.Discovery.Filters {
|
|
if _, err := ldap.CompileFilter(filter); err != nil {
|
|
return trace.BadParameter("WindowsDesktopService specifies invalid LDAP filter %q", filter)
|
|
}
|
|
}
|
|
|
|
if fc.WindowsDesktop.Discovery.BaseDN != "" && len(fc.WindowsDesktop.DiscoveryConfigs) > 0 {
|
|
return trace.BadParameter("WindowsDesktopService specifies both discovery and discovery_configs: move the discovery section to discovery_configs to continue")
|
|
}
|
|
|
|
for _, discoveryConfig := range fc.WindowsDesktop.DiscoveryConfigs {
|
|
for _, filter := range discoveryConfig.Filters {
|
|
if _, err := ldap.CompileFilter(filter); err != nil {
|
|
return trace.BadParameter("WindowsDesktopService specifies invalid LDAP filter %q", filter)
|
|
}
|
|
}
|
|
for k := range discoveryConfig.Labels {
|
|
if !types.IsValidLabelKey(k) {
|
|
return trace.BadParameter("WindowsDesktopService specifies label %q which is not a valid label key", k)
|
|
}
|
|
}
|
|
for _, attributeName := range discoveryConfig.LabelAttributes {
|
|
if !types.IsValidLabelKey(attributeName) {
|
|
return trace.BadParameter("WindowsDesktopService specifies label_attribute %q which is not a valid label key", attributeName)
|
|
}
|
|
}
|
|
if p := discoveryConfig.RDPPort; p < 0 || p > 65535 {
|
|
return trace.BadParameter("WindowsDesktopService specifies invalid RDP port %d", p)
|
|
}
|
|
}
|
|
|
|
// append the old (singular) discovery config to the new format that supports multiple configs
|
|
if fc.WindowsDesktop.Discovery.BaseDN != "" {
|
|
fc.WindowsDesktop.DiscoveryConfigs = append(fc.WindowsDesktop.DiscoveryConfigs, fc.WindowsDesktop.Discovery)
|
|
}
|
|
|
|
cfg.WindowsDesktop.Discovery = make([]servicecfg.LDAPDiscoveryConfig, 0, len(fc.WindowsDesktop.DiscoveryConfigs))
|
|
for _, dc := range fc.WindowsDesktop.DiscoveryConfigs {
|
|
if dc.BaseDN == "" {
|
|
return trace.BadParameter("WindowsDesktopService discovery_config is missing required base_dn")
|
|
}
|
|
cfg.WindowsDesktop.Discovery = append(cfg.WindowsDesktop.Discovery,
|
|
servicecfg.LDAPDiscoveryConfig{
|
|
BaseDN: dc.BaseDN,
|
|
Filters: dc.Filters,
|
|
Labels: dc.Labels,
|
|
LabelAttributes: dc.LabelAttributes,
|
|
RDPPort: cmp.Or(dc.RDPPort, int(defaults.RDPListenPort)),
|
|
},
|
|
)
|
|
}
|
|
|
|
cfg.WindowsDesktop.DiscoveryInterval = fc.WindowsDesktop.DiscoveryInterval
|
|
if cfg.WindowsDesktop.DiscoveryInterval < 0 {
|
|
return trace.BadParameter("desktop discovery interval must not be negative (%v)", fc.WindowsDesktop.DiscoveryInterval.String())
|
|
}
|
|
|
|
cfg.WindowsDesktop.PublishCRLInterval = fc.WindowsDesktop.PublishCRLInterval
|
|
if cfg.WindowsDesktop.PublishCRLInterval < 0 {
|
|
return trace.BadParameter("publish CRL interval must not be negative (%v)", fc.WindowsDesktop.PublishCRLInterval.String())
|
|
}
|
|
|
|
var err error
|
|
cfg.WindowsDesktop.PublicAddrs, err = utils.AddrsFromStrings(fc.WindowsDesktop.PublicAddr, defaults.WindowsDesktopListenPort)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.WindowsDesktop.ShowDesktopWallpaper = fc.WindowsDesktop.ShowDesktopWallpaper
|
|
if len(fc.WindowsDesktop.ADHosts) > 0 {
|
|
slog.WarnContext(context.Background(), "hosts field is deprecated, prefer static_hosts instead")
|
|
}
|
|
if len(fc.WindowsDesktop.NonADHosts) > 0 {
|
|
slog.WarnContext(context.Background(), "non_ad_hosts field is deprecated, prefer static_hosts instead")
|
|
}
|
|
cfg.WindowsDesktop.StaticHosts, err = staticHostsWithAddress(fc.WindowsDesktop)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if fc.WindowsDesktop.LDAP.DEREncodedCAFile != "" && fc.WindowsDesktop.LDAP.PEMEncodedCACerts != "" {
|
|
return trace.BadParameter("WindowsDesktopService can not use both der_ca_file and ldap_ca_cert")
|
|
}
|
|
|
|
var certs []*x509.Certificate
|
|
if fc.WindowsDesktop.LDAP.DEREncodedCAFile != "" {
|
|
rawCert, err := os.ReadFile(fc.WindowsDesktop.LDAP.DEREncodedCAFile)
|
|
if err != nil {
|
|
return trace.WrapWithMessage(err, "loading the LDAP CA from file %v", fc.WindowsDesktop.LDAP.DEREncodedCAFile)
|
|
}
|
|
|
|
derCert, err := x509.ParseCertificate(rawCert)
|
|
if err != nil {
|
|
return trace.WrapWithMessage(err, "parsing the LDAP root CA file %v", fc.WindowsDesktop.LDAP.DEREncodedCAFile)
|
|
}
|
|
certs = []*x509.Certificate{derCert}
|
|
}
|
|
|
|
if fc.WindowsDesktop.LDAP.PEMEncodedCACerts != "" {
|
|
pemCerts, err := tlsca.ParseCertificatePEMs([]byte(fc.WindowsDesktop.LDAP.PEMEncodedCACerts))
|
|
if err != nil {
|
|
return trace.WrapWithMessage(err, "parsing the LDAP root CA PEM cert(s)")
|
|
}
|
|
if len(pemCerts) == 0 {
|
|
return trace.BadParameter("ldap_ca_cert is set, but no certificates were parsed")
|
|
}
|
|
certs = pemCerts
|
|
}
|
|
|
|
locateServer := servicecfg.LocateServer{
|
|
Enabled: fc.WindowsDesktop.LDAP.LocateServer.Enabled,
|
|
Site: fc.WindowsDesktop.LDAP.LocateServer.Site,
|
|
}
|
|
|
|
cfg.WindowsDesktop.LDAP = servicecfg.LDAPConfig{
|
|
Addr: fc.WindowsDesktop.LDAP.Addr,
|
|
Username: fc.WindowsDesktop.LDAP.Username,
|
|
SID: fc.WindowsDesktop.LDAP.SID,
|
|
Domain: fc.WindowsDesktop.LDAP.Domain,
|
|
InsecureSkipVerify: fc.WindowsDesktop.LDAP.InsecureSkipVerify,
|
|
ServerName: fc.WindowsDesktop.LDAP.ServerName,
|
|
CAs: certs,
|
|
LocateServer: locateServer,
|
|
}
|
|
|
|
cfg.WindowsDesktop.PKIDomain = fc.WindowsDesktop.PKIDomain
|
|
|
|
cfg.WindowsDesktop.KDCAddr = fc.WindowsDesktop.KDCAddress
|
|
|
|
var hlrs []servicecfg.HostLabelRule
|
|
for _, rule := range fc.WindowsDesktop.HostLabels {
|
|
r, err := regexp.Compile(rule.Match)
|
|
if err != nil {
|
|
return trace.BadParameter("WindowsDesktopService specifies invalid regexp %q", rule.Match)
|
|
}
|
|
|
|
if len(rule.Labels) == 0 {
|
|
return trace.BadParameter("WindowsDesktopService host regex %q has no labels", rule.Match)
|
|
}
|
|
|
|
for k := range rule.Labels {
|
|
if !types.IsValidLabelKey(k) {
|
|
return trace.BadParameter("WindowsDesktopService specifies invalid label %q", k)
|
|
}
|
|
}
|
|
|
|
hlrs = append(hlrs, servicecfg.HostLabelRule{
|
|
Regexp: r,
|
|
Labels: rule.Labels,
|
|
})
|
|
}
|
|
cfg.WindowsDesktop.HostLabels = servicecfg.NewHostLabelRules(hlrs...)
|
|
|
|
for _, matcher := range fc.WindowsDesktop.ResourceMatchers {
|
|
cfg.WindowsDesktop.ResourceMatchers = append(cfg.WindowsDesktop.ResourceMatchers, services.ResourceMatcher{
|
|
Labels: matcher.Labels,
|
|
})
|
|
}
|
|
|
|
if fc.WindowsDesktop.Labels != nil {
|
|
cfg.WindowsDesktop.Labels = maps.Clone(fc.WindowsDesktop.Labels)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func staticHostsWithAddress(ws WindowsDesktopService) ([]servicecfg.WindowsHost, error) {
|
|
var hostsWithAddress []servicecfg.WindowsHost
|
|
var cfgHosts []WindowsHost
|
|
cfgHosts = append(cfgHosts, ws.StaticHosts...)
|
|
for _, host := range ws.NonADHosts {
|
|
cfgHosts = append(cfgHosts, WindowsHost{
|
|
Address: host,
|
|
AD: false,
|
|
})
|
|
}
|
|
for _, host := range ws.ADHosts {
|
|
cfgHosts = append(cfgHosts, WindowsHost{
|
|
Address: host,
|
|
AD: true,
|
|
})
|
|
}
|
|
for _, host := range cfgHosts {
|
|
addr, err := utils.ParseHostPortAddr(host.Address, defaults.RDPListenPort)
|
|
if err != nil {
|
|
return nil, trace.BadParameter("invalid addr %q", host.Address)
|
|
}
|
|
hostsWithAddress = append(hostsWithAddress, servicecfg.WindowsHost{
|
|
Name: host.Name,
|
|
Address: *addr,
|
|
Labels: host.Labels,
|
|
AD: host.AD,
|
|
})
|
|
}
|
|
return hostsWithAddress, nil
|
|
}
|
|
|
|
// applyTracingConfig applies file configuration for the "tracing_service" section.
|
|
func applyTracingConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
// Tracing is enabled.
|
|
cfg.Tracing.Enabled = true
|
|
|
|
if fc.Tracing.ExporterURL == "" {
|
|
return trace.BadParameter("tracing_service is enabled but no exporter_url is specified")
|
|
}
|
|
|
|
cfg.Tracing.ExporterURL = fc.Tracing.ExporterURL
|
|
cfg.Tracing.SamplingRate = float64(fc.Tracing.SamplingRatePerMillion) / 1_000_000.0
|
|
|
|
for _, p := range fc.Tracing.KeyPairs {
|
|
// Check that the certificate exists on disk. This exists to provide the
|
|
// user a sensible error message.
|
|
if !utils.FileExists(p.PrivateKey) {
|
|
return trace.NotFound("tracing_service private key does not exist: %s", p.PrivateKey)
|
|
}
|
|
if !utils.FileExists(p.Certificate) {
|
|
return trace.NotFound("tracing_service cert does not exist: %s", p.Certificate)
|
|
}
|
|
|
|
cfg.Tracing.KeyPairs = append(cfg.Tracing.KeyPairs, servicecfg.KeyPairPath{
|
|
PrivateKey: p.PrivateKey,
|
|
Certificate: p.Certificate,
|
|
})
|
|
}
|
|
|
|
for _, caCert := range fc.Tracing.CACerts {
|
|
// Check that the certificate exists on disk. This exists to provide the
|
|
// user a sensible error message.
|
|
if !utils.FileExists(caCert) {
|
|
return trace.NotFound("tracing_service ca cert does not exist: %s", caCert)
|
|
}
|
|
|
|
cfg.Tracing.CACerts = append(cfg.Tracing.CACerts, caCert)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// applyString takes 'src' and overwrites target with it, unless 'src' is empty
|
|
// returns 'True' if 'src' was not empty
|
|
func applyString(src string, target *string) bool {
|
|
if src != "" {
|
|
*target = src
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// applyConfigVersion applies config version from parsed file. If config version is not
|
|
// present the v1 version will be used as default.
|
|
func applyConfigVersion(fc *FileConfig, cfg *servicecfg.Config) {
|
|
cfg.Version = defaults.TeleportConfigVersionV1
|
|
if fc.Version != "" {
|
|
cfg.Version = fc.Version
|
|
}
|
|
}
|
|
|
|
// Configure merges command line arguments with what's in a configuration file
|
|
// with CLI commands taking precedence
|
|
func Configure(clf *CommandLineFlags, cfg *servicecfg.Config, legacyAppFlags bool) error {
|
|
// load /etc/teleport.yaml and apply its values:
|
|
fileConf, err := ReadConfigFile(clf.ConfigFile)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
// if configuration is passed as an environment variable,
|
|
// try to decode it and override the config file
|
|
if clf.ConfigString != "" {
|
|
fileConf, err = ReadFromString(clf.ConfigString)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if clf.BootstrapFile != "" {
|
|
resources, err := ReadResources(clf.BootstrapFile)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if len(resources) < 1 {
|
|
return trace.BadParameter("no resources found: %q", clf.BootstrapFile)
|
|
}
|
|
cfg.Auth.BootstrapResources = resources
|
|
}
|
|
|
|
if clf.ApplyOnStartupFile != "" {
|
|
resources, err := ReadResources(clf.ApplyOnStartupFile)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
if len(resources) < 1 {
|
|
return trace.BadParameter("no resources found: %q", clf.ApplyOnStartupFile)
|
|
}
|
|
cfg.Auth.ApplyOnStartupResources = resources
|
|
}
|
|
|
|
// Apply command line --debug flag to override logger severity.
|
|
if clf.Debug {
|
|
// If debug logging is requested and no file configuration exists, set the
|
|
// log level right away. Otherwise allow the command line flag to override
|
|
// logger severity in file configuration.
|
|
if fileConf == nil {
|
|
cfg.SetLogLevel(slog.LevelDebug)
|
|
} else {
|
|
if strings.ToLower(fileConf.Logger.Severity) != "trace" {
|
|
fileConf.Logger.Severity = teleport.DebugLevel
|
|
}
|
|
}
|
|
}
|
|
|
|
// If this process is trying to join a cluster as an application service,
|
|
// make sure application name and URI are provided.
|
|
if slices.Contains(splitRoles(clf.Roles), defaults.RoleApp) {
|
|
if (clf.AppName == "") && (clf.AppURI == "" && clf.AppCloud == "" && !clf.MCPDemoServer) {
|
|
// TODO: remove legacyAppFlags once `teleport start --app-name` is removed.
|
|
if legacyAppFlags {
|
|
return trace.BadParameter("application name (--app-name) and URI (--app-uri) flags are both required to join application proxy to the cluster")
|
|
}
|
|
return trace.BadParameter("to join application proxy to the cluster provide application name (--name) and either URI (--uri) or Cloud type (--cloud)")
|
|
}
|
|
|
|
if clf.AppName == "" && !clf.MCPDemoServer {
|
|
if legacyAppFlags {
|
|
return trace.BadParameter("application name (--app-name) is required to join application proxy to the cluster")
|
|
}
|
|
return trace.BadParameter("to join application proxy to the cluster provide application name (--name)")
|
|
}
|
|
|
|
if clf.AppName != "" && clf.AppURI == "" && clf.AppCloud == "" {
|
|
if legacyAppFlags {
|
|
return trace.BadParameter("URI (--app-uri) flag is required to join application proxy to the cluster")
|
|
}
|
|
return trace.BadParameter("to join application proxy to the cluster provide URI (--uri) or Cloud type (--cloud)")
|
|
}
|
|
}
|
|
|
|
// Enable the "Teleport Demo" MCP server if requested. Make sure application
|
|
// service is enabled for proxying MCP servers.
|
|
if clf.MCPDemoServer {
|
|
cfg.Apps.MCPDemoServer = true
|
|
cfg.Apps.Enabled = true
|
|
}
|
|
|
|
// If application name was specified on command line, add to file
|
|
// configuration where it will be validated.
|
|
if clf.AppName != "" {
|
|
cfg.Apps.Enabled = true
|
|
|
|
// Parse static and dynamic labels.
|
|
static, dynamic, err := parseLabels(clf.Labels)
|
|
if err != nil {
|
|
return trace.BadParameter("labels invalid: %v", err)
|
|
}
|
|
|
|
// Create and validate application. If valid, add to list of applications.
|
|
app := servicecfg.App{
|
|
Name: clf.AppName,
|
|
URI: clf.AppURI,
|
|
Cloud: clf.AppCloud,
|
|
PublicAddr: clf.AppPublicAddr,
|
|
StaticLabels: static,
|
|
DynamicLabels: dynamic,
|
|
}
|
|
if err := app.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Apps.Apps = append(cfg.Apps.Apps, app)
|
|
}
|
|
|
|
// If database name was specified on the command line, add to configuration.
|
|
if clf.DatabaseName != "" {
|
|
cfg.Databases.Enabled = true
|
|
staticLabels, dynamicLabels, err := parseLabels(clf.Labels)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
var caBytes []byte
|
|
if clf.DatabaseCACertFile != "" {
|
|
caBytes, err = os.ReadFile(clf.DatabaseCACertFile)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
var sessionTags map[string]string
|
|
if clf.DatabaseAWSSessionTags != "" {
|
|
var err error
|
|
sessionTags, err = parse.LabelSelectorSpec(clf.DatabaseAWSSessionTags)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
if err = gcputils.ValidateAlloyDBEndpointType(clf.DatabaseGCPAlloyDBEndpointType); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
db := servicecfg.Database{
|
|
Name: clf.DatabaseName,
|
|
Description: clf.DatabaseDescription,
|
|
Protocol: clf.DatabaseProtocol,
|
|
URI: clf.DatabaseURI,
|
|
StaticLabels: staticLabels,
|
|
MySQL: servicecfg.MySQLOptions{
|
|
ServerVersion: clf.DatabaseMySQLServerVersion,
|
|
},
|
|
DynamicLabels: dynamicLabels,
|
|
TLS: servicecfg.DatabaseTLS{
|
|
CACert: caBytes,
|
|
},
|
|
AWS: servicecfg.DatabaseAWS{
|
|
Region: clf.DatabaseAWSRegion,
|
|
AccountID: clf.DatabaseAWSAccountID,
|
|
AssumeRoleARN: clf.DatabaseAWSAssumeRoleARN,
|
|
ExternalID: clf.DatabaseAWSExternalID,
|
|
SessionTags: sessionTags,
|
|
Redshift: servicecfg.DatabaseAWSRedshift{
|
|
ClusterID: clf.DatabaseAWSRedshiftClusterID,
|
|
},
|
|
RDS: servicecfg.DatabaseAWSRDS{
|
|
InstanceID: clf.DatabaseAWSRDSInstanceID,
|
|
ClusterID: clf.DatabaseAWSRDSClusterID,
|
|
},
|
|
ElastiCache: servicecfg.DatabaseAWSElastiCache{
|
|
ReplicationGroupID: clf.DatabaseAWSElastiCacheGroupID,
|
|
},
|
|
ElastiCacheServerless: servicecfg.DatabaseAWSElastiCacheServerless{
|
|
CacheName: clf.DatabaseAWSElastiCacheServerlessCacheName,
|
|
},
|
|
MemoryDB: servicecfg.DatabaseAWSMemoryDB{
|
|
ClusterName: clf.DatabaseAWSMemoryDBClusterName,
|
|
},
|
|
},
|
|
GCP: servicecfg.DatabaseGCP{
|
|
ProjectID: clf.DatabaseGCPProjectID,
|
|
InstanceID: clf.DatabaseGCPInstanceID,
|
|
AlloyDB: servicecfg.DatabaseGCPAlloyDB{
|
|
EndpointType: clf.DatabaseGCPAlloyDBEndpointType,
|
|
},
|
|
},
|
|
AD: servicecfg.DatabaseAD{
|
|
KeytabFile: clf.DatabaseADKeytabFile,
|
|
Krb5File: clf.DatabaseADKrb5File,
|
|
Domain: clf.DatabaseADDomain,
|
|
SPN: clf.DatabaseADSPN,
|
|
},
|
|
}
|
|
if err := db.CheckAndSetDefaults(); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.Databases.Databases = append(cfg.Databases.Databases, db)
|
|
}
|
|
|
|
if err = ApplyFileConfig(fileConf, cfg); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// If FIPS mode is specified, validate Teleport uses a FIPS-validated module
|
|
if clf.FIPS {
|
|
// Make sure all cryptographic primitives are FIPS compliant.
|
|
//
|
|
err = libslices.ContainsAll(defaults.FIPSCipherSuites, cfg.CipherSuites)
|
|
if err != nil {
|
|
return trace.BadParameter("non-FIPS compliant TLS cipher suite selected: %v", err)
|
|
}
|
|
err = libslices.ContainsAll(defaults.FIPSCiphers, cfg.Ciphers)
|
|
if err != nil {
|
|
return trace.BadParameter("non-FIPS compliant SSH cipher selected: %v", err)
|
|
}
|
|
err = libslices.ContainsAll(defaults.FIPSKEXAlgorithms, cfg.KEXAlgorithms)
|
|
if err != nil {
|
|
return trace.BadParameter("non-FIPS compliant SSH kex algorithm selected: %v", err)
|
|
}
|
|
err = libslices.ContainsAll(defaults.FIPSMACAlgorithms, cfg.MACAlgorithms)
|
|
if err != nil {
|
|
return trace.BadParameter("non-FIPS compliant SSH mac algorithm selected: %v", err)
|
|
}
|
|
|
|
// Make sure cluster settings are also FedRAMP/FIPS compliant.
|
|
if cfg.Auth.Enabled {
|
|
// Only SSO based authentication is supported. The SSO provider is where
|
|
// any FedRAMP/FIPS compliance (like password complexity) should be
|
|
// enforced.
|
|
if cfg.Auth.Preference.GetAllowLocalAuth() {
|
|
return trace.BadParameter("non-FIPS compliant authentication setting: \"local_auth\" must be false")
|
|
}
|
|
|
|
// If sessions are being recorded at the proxy host key checking must be
|
|
// enabled. This make sure the host certificate key algorithm is FIPS
|
|
// compliant.
|
|
if services.IsRecordAtProxy(cfg.Auth.SessionRecordingConfig.GetMode()) &&
|
|
!cfg.Auth.SessionRecordingConfig.GetProxyChecksHostKeys() {
|
|
return trace.BadParameter("non-FIPS compliant proxy settings: \"proxy_checks_host_keys\" must be true")
|
|
}
|
|
|
|
if err := services.ValidateSessionRecordingConfig(cfg.Auth.SessionRecordingConfig, clf.FIPS, cfg.Modules.Features().Cloud); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if cfg.Auth.Preference.Origin() != types.OriginDefaults {
|
|
// Only check the signature algorithm suite if the auth preference was
|
|
// actually set in the config file. If it wasn't set and the default is
|
|
// used, the algorithm suite will be overwritten in initializeAuthPreference
|
|
// based on the FIPS and HSM settings, and any already persisted auth preference.
|
|
if err := cfg.Auth.Preference.CheckSignatureAlgorithmSuite(types.SignatureAlgorithmSuiteParams{
|
|
FIPS: clf.FIPS,
|
|
UsingHSMOrKMS: cfg.Auth.KeyStore != servicecfg.KeystoreConfig{},
|
|
Cloud: cfg.Modules.Features().Cloud,
|
|
}); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
// apply --skip-version-check flag.
|
|
if clf.SkipVersionCheck {
|
|
cfg.SkipVersionCheck = clf.SkipVersionCheck
|
|
}
|
|
|
|
// Apply diagnostic address flag.
|
|
if clf.DiagnosticAddr != "" {
|
|
addr, err := utils.ParseAddr(clf.DiagnosticAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err, "failed to parse diag-addr")
|
|
}
|
|
cfg.DiagnosticAddr = *addr
|
|
}
|
|
|
|
// apply --insecure-no-tls flag:
|
|
if clf.DisableTLS {
|
|
cfg.Proxy.DisableTLS = clf.DisableTLS
|
|
}
|
|
|
|
// apply --debug flag to config:
|
|
if clf.Debug {
|
|
cfg.Debug = clf.Debug
|
|
}
|
|
|
|
// apply --roles flag:
|
|
if clf.Roles != "" {
|
|
if err := validateRoles(clf.Roles); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.SSH.Enabled = strings.Contains(clf.Roles, defaults.RoleNode)
|
|
cfg.Auth.Enabled = strings.Contains(clf.Roles, defaults.RoleAuthService)
|
|
cfg.Proxy.Enabled = strings.Contains(clf.Roles, defaults.RoleProxy)
|
|
cfg.Apps.Enabled = strings.Contains(clf.Roles, defaults.RoleApp)
|
|
cfg.Databases.Enabled = strings.Contains(clf.Roles, defaults.RoleDatabase)
|
|
}
|
|
|
|
// apply --auth-server flag:
|
|
if len(clf.AuthServerAddr) > 0 {
|
|
if cfg.Auth.Enabled {
|
|
slog.WarnContext(context.Background(), "not starting the local auth service. --auth-server flag tells to connect to another auth server")
|
|
cfg.Auth.Enabled = false
|
|
}
|
|
|
|
authServerAddresses := make([]utils.NetAddr, 0, len(clf.AuthServerAddr))
|
|
for _, as := range clf.AuthServerAddr {
|
|
addr, err := utils.ParseHostPortAddr(as, defaults.AuthListenPort)
|
|
if err != nil {
|
|
return trace.BadParameter("cannot parse auth server address: '%v'", as)
|
|
}
|
|
authServerAddresses = append(authServerAddresses, *addr)
|
|
}
|
|
|
|
if err := cfg.SetAuthServerAddresses(authServerAddresses); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
}
|
|
|
|
// apply --name flag:
|
|
if clf.NodeName != "" {
|
|
cfg.Hostname = clf.NodeName
|
|
}
|
|
|
|
// apply --pid-file flag
|
|
if clf.PIDFile != "" {
|
|
cfg.PIDFile = clf.PIDFile
|
|
}
|
|
|
|
if clf.AuthToken != "" {
|
|
// store the value of the --token flag:
|
|
cfg.SetToken(clf.AuthToken)
|
|
}
|
|
|
|
// Apply flags used for the node to validate the Auth Server.
|
|
if err = cfg.ApplyCAPins(clf.CAPins); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// apply --listen-ip flag:
|
|
if clf.ListenIP != nil {
|
|
applyListenIP(clf.ListenIP, cfg)
|
|
}
|
|
|
|
// --advertise-ip flag
|
|
if clf.AdvertiseIP != "" {
|
|
if _, _, err := utils.ParseAdvertiseAddr(clf.AdvertiseIP); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.AdvertiseIP = clf.AdvertiseIP
|
|
}
|
|
|
|
// apply --labels flag
|
|
if err := parseLabelsApply(clf.Labels, &cfg.SSH); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// --pid-file:
|
|
if clf.PIDFile != "" {
|
|
cfg.PIDFile = clf.PIDFile
|
|
}
|
|
|
|
// auth_servers not configured, but the 'auth' is enabled (auth is on localhost)?
|
|
if len(cfg.AuthServerAddresses()) == 0 && cfg.Auth.Enabled {
|
|
cfg.SetAuthServerAddress(cfg.Auth.ListenAddr)
|
|
}
|
|
|
|
// add data_dir to the backend config:
|
|
if cfg.Auth.StorageConfig.Params == nil {
|
|
cfg.Auth.StorageConfig.Params = backend.Params{}
|
|
}
|
|
cfg.Auth.StorageConfig.Params["data_dir"] = cfg.DataDir
|
|
// command line flag takes precedence over file config
|
|
if clf.PermitUserEnvironment {
|
|
cfg.SSH.PermitUserEnvironment = true
|
|
}
|
|
|
|
// set the default proxy listener addresses for config v1, if not already set
|
|
applyDefaultProxyListenerAddresses(cfg)
|
|
|
|
// not publicly documented or supported for now (thus the
|
|
// "TELEPORT_UNSTABLE_" prefix); the group with an empty ID is a valid
|
|
// group, and generation zero is a valid generation for any group
|
|
cfg.Proxy.ProxyGroupID = os.Getenv("TELEPORT_UNSTABLE_PROXYGROUP_ID")
|
|
if proxyGroupGeneration := os.Getenv("TELEPORT_UNSTABLE_PROXYGROUP_GEN"); proxyGroupGeneration != "" {
|
|
cfg.Proxy.ProxyGroupGeneration, err = strconv.ParseUint(proxyGroupGeneration, 10, 64)
|
|
if err != nil {
|
|
return trace.Wrap(err, "invalid proxygroup generation %q: %v", proxyGroupGeneration, err)
|
|
}
|
|
}
|
|
|
|
if clf.DisableDebugService {
|
|
cfg.DebugService.Enabled = false
|
|
}
|
|
|
|
if clf.EnableSELinux {
|
|
cfg.SSH.EnableSELinux = true
|
|
}
|
|
|
|
if clf.EnsureSELinuxEnforcing {
|
|
cfg.SSH.EnsureSELinuxEnforcing = true
|
|
}
|
|
|
|
if os.Getenv("TELEPORT_UNSTABLE_QUIC_PROXY_PEERING") == "yes" {
|
|
cfg.Proxy.QUICProxyPeering = true
|
|
}
|
|
|
|
if rawPeriod := os.Getenv("TELEPORT_UNSTABLE_AGENT_ROLLOUT_SYNC_PERIOD"); rawPeriod != "" {
|
|
period, err := time.ParseDuration(rawPeriod)
|
|
if err != nil {
|
|
return trace.Wrap(err, "invalid agent rollout period %q", rawPeriod)
|
|
}
|
|
cfg.Auth.AgentRolloutControllerSyncPeriod = period
|
|
}
|
|
|
|
// pass the value of --insecure flag to the runtime
|
|
if clf.InsecureMode {
|
|
cfg.InsecureMode = true
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ConfigureOpenSSH initializes a config from the commandline flags passed
|
|
func ConfigureOpenSSH(clf *CommandLineFlags, cfg *servicecfg.Config) error {
|
|
// Apply command line --debug flag to override logger severity.
|
|
level := slog.LevelError
|
|
if clf.Debug {
|
|
cfg.SetLogLevel(slog.LevelDebug)
|
|
level = slog.LevelDebug
|
|
cfg.Debug = clf.Debug
|
|
}
|
|
|
|
// Ensure that the logging level is respected by the logger.
|
|
utils.InitLogger(utils.LoggingForDaemon, level)
|
|
|
|
if clf.AuthToken != "" {
|
|
// store the value of the --token flag:
|
|
cfg.SetToken(clf.AuthToken)
|
|
}
|
|
|
|
// apply --skip-version-check flag.
|
|
if clf.SkipVersionCheck {
|
|
cfg.SkipVersionCheck = clf.SkipVersionCheck
|
|
}
|
|
|
|
slog.DebugContext(context.Background(), "Disabling all services, only the Teleport OpenSSH service can run during the `teleport join openssh` command")
|
|
servicecfg.DisableLongRunningServices(cfg)
|
|
|
|
cfg.DataDir = clf.DataDir
|
|
cfg.Version = defaults.TeleportConfigVersionV3
|
|
cfg.OpenSSH.SSHDConfigPath = clf.OpenSSHConfigPath
|
|
cfg.OpenSSH.RestartSSHD = clf.RestartOpenSSH
|
|
cfg.OpenSSH.RestartCommand = clf.RestartCommand
|
|
cfg.OpenSSH.CheckCommand = clf.CheckCommand
|
|
cfg.JoinMethod = types.JoinMethod(clf.JoinMethod)
|
|
|
|
hostname, err := os.Hostname()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Hostname = hostname
|
|
cfg.OpenSSH.InstanceAddr = clf.Address
|
|
cfg.OpenSSH.AdditionalPrincipals = []string{hostname, clf.Address}
|
|
for principal := range strings.SplitSeq(clf.AdditionalPrincipals, ",") {
|
|
if principal == "" {
|
|
continue
|
|
}
|
|
cfg.OpenSSH.AdditionalPrincipals = append(cfg.OpenSSH.AdditionalPrincipals, principal)
|
|
}
|
|
cfg.OpenSSH.Labels, err = parse.LabelSelectorSpec(clf.Labels)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
proxyServer, err := utils.ParseAddr(clf.ProxyServer)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
cfg.SetAuthServerAddresses(nil)
|
|
cfg.ProxyServer = *proxyServer
|
|
|
|
// pass the value of --insecure flag to the runtime
|
|
if clf.InsecureMode {
|
|
cfg.InsecureMode = true
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseLabels parses the labels command line flag and returns static and
|
|
// dynamic labels.
|
|
func parseLabels(spec string) (map[string]string, services.CommandLabels, error) {
|
|
// Base syntax parsing, the spec must be in the form of 'key=value,more="better"'.
|
|
lmap, err := parse.LabelSelectorSpec(spec)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
|
|
static := make(map[string]string)
|
|
dynamic := make(services.CommandLabels)
|
|
|
|
if len(lmap) == 0 {
|
|
return static, dynamic, nil
|
|
}
|
|
|
|
// Loop over all parsed labels and set either static or dynamic labels.
|
|
for key, value := range lmap {
|
|
dynamicLabel, err := isCmdLabelSpec(value)
|
|
if err != nil {
|
|
return nil, nil, trace.Wrap(err)
|
|
}
|
|
if dynamicLabel != nil {
|
|
dynamic[key] = dynamicLabel
|
|
} else {
|
|
static[key] = value
|
|
}
|
|
}
|
|
|
|
return static, dynamic, nil
|
|
}
|
|
|
|
// parseLabelsApply reads in the labels command line flag and tries to
|
|
// correctly populate static and dynamic labels for the SSH service.
|
|
func parseLabelsApply(spec string, sshConf *servicecfg.SSHConfig) error {
|
|
if spec == "" {
|
|
return nil
|
|
}
|
|
|
|
var err error
|
|
sshConf.Labels, sshConf.CmdLabels, err = parseLabels(spec)
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// isCmdLabelSpec tries to interpret a given string as a "command label" spec.
|
|
// A command label spec looks like [time_duration:command param1 param2 ...] where
|
|
// time_duration is in "1h2m1s" form.
|
|
//
|
|
// Example of a valid spec: "[1h:/bin/uname -m]"
|
|
func isCmdLabelSpec(spec string) (types.CommandLabel, error) {
|
|
// command spec? (surrounded by brackets?)
|
|
if len(spec) > 5 && spec[0] == '[' && spec[len(spec)-1] == ']' {
|
|
invalidSpecError := trace.BadParameter(
|
|
"invalid command label spec: '%s'", spec)
|
|
spec = strings.Trim(spec, "[]")
|
|
idx := strings.IndexRune(spec, ':')
|
|
if idx < 0 {
|
|
return nil, trace.Wrap(invalidSpecError)
|
|
}
|
|
periodSpec := spec[:idx]
|
|
period, err := time.ParseDuration(periodSpec)
|
|
if err != nil {
|
|
return nil, trace.Wrap(invalidSpecError)
|
|
}
|
|
cmdSpec := spec[idx+1:]
|
|
if len(cmdSpec) < 1 {
|
|
return nil, trace.Wrap(invalidSpecError)
|
|
}
|
|
openQuote := false
|
|
return &types.CommandLabelV2{
|
|
Period: types.NewDuration(period),
|
|
Command: strings.FieldsFunc(cmdSpec, func(c rune) bool {
|
|
if c == '"' {
|
|
openQuote = !openQuote
|
|
}
|
|
return unicode.IsSpace(c) && !openQuote
|
|
}),
|
|
}, nil
|
|
}
|
|
// not a valid spec
|
|
return nil, nil
|
|
}
|
|
|
|
// applyListenIP replaces all 'listen addr' settings for all services with
|
|
// a given IP
|
|
func applyListenIP(ip net.IP, cfg *servicecfg.Config) {
|
|
listeningAddresses := []*utils.NetAddr{
|
|
&cfg.Auth.ListenAddr,
|
|
&cfg.Proxy.SSHAddr,
|
|
&cfg.Proxy.WebAddr,
|
|
&cfg.SSH.Addr,
|
|
&cfg.Proxy.ReverseTunnelListenAddr,
|
|
}
|
|
for _, addr := range listeningAddresses {
|
|
if !addr.IsEmpty() {
|
|
replaceHost(addr, ip.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
// replaceHost takes utils.NetAddr and replaces the hostname in it, preserving
|
|
// the original port
|
|
func replaceHost(addr *utils.NetAddr, newHost string) {
|
|
_, port, err := net.SplitHostPort(addr.Addr)
|
|
if err != nil {
|
|
slog.ErrorContext(context.Background(), "failed parsing address", "address", addr.Addr, "error", err)
|
|
}
|
|
addr.Addr = net.JoinHostPort(newHost, port)
|
|
}
|
|
|
|
// validateRoles makes sure that value passed to the --roles flag is valid
|
|
func validateRoles(roles string) error {
|
|
for _, role := range splitRoles(roles) {
|
|
switch role {
|
|
case defaults.RoleAuthService,
|
|
defaults.RoleNode,
|
|
defaults.RoleProxy,
|
|
defaults.RoleApp,
|
|
defaults.RoleDatabase,
|
|
defaults.RoleWindowsDesktop,
|
|
defaults.RoleDiscovery:
|
|
default:
|
|
return trace.Errorf("unknown role: '%s'", role)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// splitRoles splits in the format roles expects.
|
|
func splitRoles(roles string) []string {
|
|
return strings.Split(roles, ",")
|
|
}
|
|
|
|
// applyTokenConfig applies the auth_token and join_params to the config
|
|
func applyTokenConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
if fc.AuthToken != "" {
|
|
if fc.JoinParams != (JoinParams{}) {
|
|
return trace.BadParameter("only one of auth_token or join_params should be set")
|
|
}
|
|
|
|
cfg.JoinMethod = types.JoinMethodToken
|
|
cfg.SetToken(fc.AuthToken)
|
|
|
|
return nil
|
|
}
|
|
|
|
if fc.JoinParams != (JoinParams{}) {
|
|
cfg.SetToken(fc.JoinParams.TokenName)
|
|
|
|
if err := types.ValidateJoinMethod(fc.JoinParams.Method); err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.JoinMethod = fc.JoinParams.Method
|
|
|
|
if fc.JoinParams.Azure != (AzureJoinParams{}) {
|
|
cfg.JoinParams = servicecfg.JoinParams{
|
|
Azure: servicecfg.AzureJoinParams{
|
|
ClientID: fc.JoinParams.Azure.ClientID,
|
|
},
|
|
}
|
|
}
|
|
|
|
if fc.JoinParams.BoundKeypair != (BoundKeypairParams{}) {
|
|
cfg.JoinParams = servicecfg.JoinParams{
|
|
BoundKeypair: servicecfg.BoundKeypairParams{
|
|
RegistrationSecretValue: fc.JoinParams.BoundKeypair.RegistrationSecretValue,
|
|
RegistrationSecretPath: fc.JoinParams.BoundKeypair.RegistrationSecretPath,
|
|
StaticPrivateKeyPath: fc.JoinParams.BoundKeypair.StaticPrivateKeyPath,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func applyOktaConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
if fc.Okta.APIEndpoint == "" {
|
|
return trace.BadParameter("okta_service is enabled but no api_endpoint is specified")
|
|
}
|
|
if fc.Okta.APITokenPath == "" {
|
|
return trace.BadParameter("okta_service is enabled but no api_token_path is specified")
|
|
}
|
|
|
|
// Make sure the URL is valid
|
|
url, err := url.Parse(fc.Okta.APIEndpoint)
|
|
if err != nil {
|
|
return trace.NewAggregate(trace.BadParameter("malformed URL %s", fc.Okta.APIEndpoint), err)
|
|
}
|
|
|
|
if url.Host == "" {
|
|
return trace.BadParameter("api_endpoint has no host")
|
|
} else if url.Scheme == "" {
|
|
return trace.BadParameter("api_endpoint has no scheme")
|
|
}
|
|
|
|
// Make sure the API token exists.
|
|
if _, err := utils.StatFile(fc.Okta.APITokenPath); err != nil {
|
|
return trace.NewAggregate(trace.BadParameter("error trying to find file %s", fc.Okta.APITokenPath), err)
|
|
}
|
|
|
|
syncSettings, err := fc.Okta.Sync.Parse()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
// For backwards compatibility, if SyncPeriod is specified, use that in the sync settings.
|
|
if syncSettings.AppGroupSyncPeriod == 0 {
|
|
syncSettings.AppGroupSyncPeriod = fc.Okta.SyncPeriod
|
|
}
|
|
|
|
cfg.Okta.Enabled = fc.Okta.Enabled()
|
|
cfg.Okta.APIEndpoint = fc.Okta.APIEndpoint
|
|
cfg.Okta.APITokenPath = fc.Okta.APITokenPath
|
|
cfg.Okta.SyncPeriod = fc.Okta.SyncPeriod
|
|
cfg.Okta.SyncSettings = *syncSettings
|
|
return nil
|
|
}
|
|
|
|
func applyJamfConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
// Ignore empty configs, validate and transform anything else.
|
|
if reflect.DeepEqual(fc.Jamf, JamfService{}) {
|
|
return nil
|
|
}
|
|
|
|
creds, err := fc.Jamf.readJamfCredentials()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
jamfSpec, err := fc.Jamf.toJamfSpecV1()
|
|
if err != nil {
|
|
return trace.Wrap(err)
|
|
}
|
|
|
|
cfg.Jamf = servicecfg.JamfConfig{
|
|
Spec: jamfSpec,
|
|
ExitOnSync: fc.Jamf.ExitOnSync,
|
|
Credentials: creds,
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func applyRelayConfig(fc *FileConfig, cfg *servicecfg.Config) error {
|
|
// TODO(espadolini): potential compatibility checks here like requiring
|
|
// config v3+ and proxy_server
|
|
|
|
// we're here because fc.Relay.Enabled is true
|
|
cfg.Relay.Enabled = true
|
|
|
|
if fc.Relay.RelayGroup == "" {
|
|
return trace.BadParameter("missing relay_service.relay_group")
|
|
}
|
|
cfg.Relay.RelayGroup = fc.Relay.RelayGroup
|
|
|
|
if fc.Relay.TargetConnectionCount < 1 || fc.Relay.TargetConnectionCount > math.MaxInt32 {
|
|
return trace.BadParameter("missing or invalid relay_service.target_connection_count")
|
|
}
|
|
cfg.Relay.TargetConnectionCount = int32(fc.Relay.TargetConnectionCount)
|
|
|
|
if len(fc.Relay.PublicHostnames) < 1 {
|
|
return trace.BadParameter("missing relay_service.public_hostnames")
|
|
}
|
|
if slices.Contains(fc.Relay.PublicHostnames, "") {
|
|
return trace.BadParameter("empty string in relay_service.public_hostnames")
|
|
}
|
|
cfg.Relay.PublicHostnames = slices.Clone(fc.Relay.PublicHostnames)
|
|
|
|
if fc.Relay.TransportListenAddr == "" {
|
|
return trace.BadParameter("missing relay_service.transport_listen_addr")
|
|
}
|
|
transportListenAddr, err := netip.ParseAddrPort(fc.Relay.TransportListenAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err, "parsing relay_service.transport_listen_addr")
|
|
}
|
|
cfg.Relay.TransportListenAddr = transportListenAddr
|
|
cfg.Relay.TransportPROXYProtocol = fc.Relay.TransportPROXYProtocol
|
|
|
|
if fc.Relay.PeerListenAddr == "" {
|
|
return trace.BadParameter("missing relay_service.peer_listen_addr")
|
|
}
|
|
peerListenAddr, err := netip.ParseAddrPort(fc.Relay.PeerListenAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err, "parsing relay_service.peer_listen_addr")
|
|
}
|
|
cfg.Relay.PeerListenAddr = peerListenAddr
|
|
|
|
if fc.Relay.PeerPublicAddr != "" {
|
|
_, _, err := net.SplitHostPort(fc.Relay.PeerPublicAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err, "parsing relay_service.peer_public_addr")
|
|
}
|
|
cfg.Relay.PeerPublicAddr = fc.Relay.PeerPublicAddr
|
|
}
|
|
|
|
if fc.Relay.TunnelListenAddr == "" {
|
|
return trace.BadParameter("missing relay_service.tunnel_listen_addr")
|
|
}
|
|
tunnelListenAddr, err := netip.ParseAddrPort(fc.Relay.TunnelListenAddr)
|
|
if err != nil {
|
|
return trace.Wrap(err, "parsing relay_service.tunnel_listen_addr")
|
|
}
|
|
cfg.Relay.TunnelListenAddr = tunnelListenAddr
|
|
cfg.Relay.TunnelPROXYProtocol = fc.Relay.TunnelPROXYProtocol
|
|
|
|
return nil
|
|
}
|