diff --git a/lib/tbot/config/config.go b/lib/tbot/config/config.go index 84992f5ea88df..f21f885520dd8 100644 --- a/lib/tbot/config/config.go +++ b/lib/tbot/config/config.go @@ -565,22 +565,7 @@ func ReadConfig(reader io.ReadSeeker, manualMigration bool) (*BotConfig, error) switch version.Version { case V1, "": - if !manualMigration { - log.WarnContext( - context.TODO(), "Deprecated config version (V1) detected. Attempting to perform an on-the-fly in-memory migration to latest version. Please persist the config migration using `tbot migrate`.") - } - config := &configV1{} - if err := decoder.Decode(config); err != nil { - return nil, trace.BadParameter("failed parsing config file: %s", strings.ReplaceAll(err.Error(), "\n", "")) - } - latestConfig, err := config.migrate() - if err != nil { - return nil, trace.WithUserMessage( - trace.Wrap(err, "migrating v1 config"), - "Failed to migrate. Please contact Teleport support or use https://goteleport.com/docs/reference/machine-id/configuration/ to manually migrate your configuration.", - ) - } - return latestConfig, nil + return nil, trace.BadParameter("configuration version v1 is no longer supported") case V2: if manualMigration { return nil, trace.BadParameter("configuration already the latest version. nothing to migrate.") diff --git a/lib/tbot/config/migrate.go b/lib/tbot/config/migrate.go deleted file mode 100644 index 925bfeff37ed6..0000000000000 --- a/lib/tbot/config/migrate.go +++ /dev/null @@ -1,426 +0,0 @@ -/* - * 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 . - */ - -package config - -import ( - "slices" - "time" - - "github.com/gravitational/trace" - "gopkg.in/yaml.v3" - - "github.com/gravitational/teleport/lib/tbot/bot" - "github.com/gravitational/teleport/lib/tbot/bot/destination" - "github.com/gravitational/teleport/lib/tbot/bot/onboarding" - "github.com/gravitational/teleport/lib/tbot/services/application" - "github.com/gravitational/teleport/lib/tbot/services/database" - "github.com/gravitational/teleport/lib/tbot/services/identity" - "github.com/gravitational/teleport/lib/tbot/services/k8s" - "github.com/gravitational/teleport/lib/tbot/services/ssh" -) - -type destinationMixinV1 struct { - Directory *destination.Directory `yaml:"directory"` - Memory *destination.Memory `yaml:"memory"` -} - -func (c *destinationMixinV1) migrate() (destination.Destination, error) { - switch { - case c.Memory != nil && c.Directory != nil: - return nil, trace.BadParameter("both 'memory' and 'directory' cannot be specified") - case c.Memory != nil: - return c.Memory, nil - case c.Directory != nil: - return c.Directory, nil - default: - return nil, trace.BadParameter("at least one of `memory' and 'directory' must be specified") - } -} - -type storageConfigV1 struct { - Mixin destinationMixinV1 `yaml:",inline"` -} - -func (c *storageConfigV1) migrate() (*StorageConfig, error) { - dest, err := c.Mixin.migrate() - if err != nil { - return nil, trace.Wrap(err, "migrating destination mixin") - } - - return &StorageConfig{ - Destination: dest, - }, nil -} - -type configV1Database struct { - Service string `yaml:"service"` - Database string `yaml:"database"` - Username string `yaml:"username"` -} - -type configV1DestinationConfigHostCert struct { - Principals []string `yaml:"principals"` -} - -type configV1DestinationConfig struct { - SSHClient map[string]any `yaml:"ssh_client"` - Identity map[string]any `yaml:"identity"` - TLS map[string]any `yaml:"tls"` - TLSCAs map[string]any `yaml:"tls_cas"` - Mongo map[string]any `yaml:"mongo"` - Cockroach map[string]any `yaml:"cockroach"` - Kubernetes map[string]any `yaml:"kubernetes"` - SSHHostCert *configV1DestinationConfigHostCert `yaml:"ssh_host_cert"` -} - -func (c *configV1DestinationConfig) UnmarshalYAML(node *yaml.Node) error { - var simpleTemplate string - if err := node.Decode(&simpleTemplate); err == nil { - switch simpleTemplate { - case TemplateSSHClientName: - c.SSHClient = map[string]any{} - case TemplateIdentityName: - c.Identity = map[string]any{} - case TemplateTLSName: - c.TLS = map[string]any{} - case TemplateTLSCAsName: - c.TLSCAs = map[string]any{} - case TemplateMongoName: - c.Mongo = map[string]any{} - case TemplateCockroachName: - c.Cockroach = map[string]any{} - case TemplateKubernetesName: - c.Kubernetes = map[string]any{} - case TemplateSSHHostCertName: - c.SSHHostCert = &configV1DestinationConfigHostCert{} - default: - return trace.BadParameter("unrecognized config template %q", simpleTemplate) - } - return nil - } - - // Fall back to the full struct; alias it to get standard unmarshal - // behavior and avoid recursion - type rawTemplate configV1DestinationConfig - return trace.Wrap(node.Decode((*rawTemplate)(c))) -} - -type configV1Destination struct { - Mixin destinationMixinV1 `yaml:",inline"` - - Roles []string `yaml:"roles"` - Configs []configV1DestinationConfig `yaml:"configs"` - - Database *configV1Database `yaml:"database"` - KubernetesCluster string `yaml:"kubernetes_cluster"` - App string `yaml:"app"` - Cluster string `yaml:"cluster"` -} - -func validateTemplates(configs []configV1DestinationConfig, allowedTypes []string, requiredTypes []string) error { - var allConfiguredTypes []string - - configUnsupportedErr := func(typeName string) error { - return trace.BadParameter("configuration options are not supported by migration for %s config template", typeName) - } - - for _, templateConfig := range configs { - var configuredTypes []string - if templateConfig.SSHClient != nil { - if len(templateConfig.SSHClient) > 0 { - return configUnsupportedErr(TemplateSSHClientName) - } - configuredTypes = append(configuredTypes, TemplateSSHClientName) - } - if templateConfig.Identity != nil { - if len(templateConfig.Identity) > 0 { - return configUnsupportedErr(TemplateIdentityName) - } - configuredTypes = append(configuredTypes, TemplateIdentityName) - } - if templateConfig.TLS != nil { - if len(templateConfig.TLS) > 0 { - return configUnsupportedErr(TemplateTLSName) - } - configuredTypes = append(configuredTypes, TemplateTLSName) - } - if templateConfig.TLSCAs != nil { - if len(templateConfig.TLSCAs) > 0 { - return configUnsupportedErr(TemplateTLSCAsName) - } - configuredTypes = append(configuredTypes, TemplateTLSCAsName) - } - if templateConfig.Mongo != nil { - if len(templateConfig.Mongo) > 0 { - return configUnsupportedErr(TemplateMongoName) - } - configuredTypes = append(configuredTypes, TemplateMongoName) - } - if templateConfig.Cockroach != nil { - if len(templateConfig.Cockroach) > 0 { - return configUnsupportedErr(TemplateCockroachName) - } - configuredTypes = append(configuredTypes, TemplateCockroachName) - } - if templateConfig.Kubernetes != nil { - if len(templateConfig.Kubernetes) > 0 { - return configUnsupportedErr(TemplateKubernetesName) - } - configuredTypes = append(configuredTypes, TemplateKubernetesName) - } - if templateConfig.SSHHostCert != nil { - if len(templateConfig.SSHHostCert.Principals) == 0 { - return trace.BadParameter("no principals specified for %s config template", TemplateSSHHostCertName) - } - configuredTypes = append(configuredTypes, TemplateSSHHostCertName) - } - - if len(configuredTypes) == 0 { - return trace.BadParameter("config template must not be empty") - } - if len(configuredTypes) > 1 { - return trace.BadParameter("config template must have exactly one configuration") - } - - allConfiguredTypes = append(allConfiguredTypes, configuredTypes...) - } - - // Ensure all types are allowed by the new output type - for _, typeName := range allConfiguredTypes { - if !slices.Contains(allowedTypes, typeName) { - return trace.BadParameter("config template %q unsupported by new output type", typeName) - } - } - - // Ensure the required types are specified for the new output type - for _, typeName := range requiredTypes { - if !slices.Contains(allConfiguredTypes, typeName) { - return trace.BadParameter("old config templates missing required template %s", typeName) - } - } - - // Check for any weird duplicates we can't handle correctly - typeCounts := map[string]int{} - for _, typeName := range allConfiguredTypes { - typeCounts[typeName]++ - } - for typeName, count := range typeCounts { - if count > 1 { - return trace.BadParameter("multiple config template entries found for %q", typeName) - } - } - - return nil -} - -func (c *configV1Destination) migrate() (ServiceConfig, error) { - dest, err := c.Mixin.migrate() - if err != nil { - return nil, trace.Wrap(err, "migrating destination") - } - - appConfigured := c.App != "" - databaseConfigured := c.Database != nil - kubernetesConfigured := c.KubernetesCluster != "" - hostCertConfigured := false - for _, templateConfig := range c.Configs { - if templateConfig.SSHHostCert != nil { - hostCertConfigured = true - } - } - outputTypesCount := 0 - for _, val := range []bool{appConfigured, databaseConfigured, kubernetesConfigured, hostCertConfigured} { - if val { - outputTypesCount++ - } - } - if outputTypesCount > 1 { - return nil, trace.BadParameter("multiple potential output types detected, cannot determine correct type") - } - - switch { - case appConfigured: - if err := validateTemplates( - c.Configs, - []string{TemplateTLSCAsName, TemplateTLSName, TemplateIdentityName}, - []string{}, - ); err != nil { - return nil, trace.Wrap(err, "validating template configs") - } - specificTLSExtensions := false - for _, templateConfig := range c.Configs { - if templateConfig.TLS != nil { - specificTLSExtensions = true - break - } - } - return &application.OutputConfig{ - Destination: dest, - Roles: c.Roles, - AppName: c.App, - SpecificTLSExtensions: specificTLSExtensions, - }, nil - case databaseConfigured: - if err := validateTemplates( - c.Configs, - []string{TemplateTLSCAsName, TemplateIdentityName, TemplateMongoName, TemplateCockroachName, TemplateTLSName}, - []string{}, - ); err != nil { - return nil, trace.Wrap(err, "validating template configs") - } - format := database.UnspecifiedDatabaseFormat - for _, templateConfig := range c.Configs { - if templateConfig.Mongo != nil { - if format != database.UnspecifiedDatabaseFormat { - return nil, trace.BadParameter("multiple candidate formats for database output") - } - format = database.MongoDatabaseFormat - } - if templateConfig.Cockroach != nil { - if format != database.UnspecifiedDatabaseFormat { - return nil, trace.BadParameter("multiple candidate formats for database output") - } - format = database.CockroachDatabaseFormat - } - if templateConfig.TLS != nil { - if format != database.UnspecifiedDatabaseFormat { - return nil, trace.BadParameter("multiple candidate formats for database output") - } - format = database.TLSDatabaseFormat - } - } - return &database.OutputConfig{ - Destination: dest, - Roles: c.Roles, - Format: format, - Database: c.Database.Database, - Service: c.Database.Service, - Username: c.Database.Username, - }, nil - case kubernetesConfigured: - if err := validateTemplates( - c.Configs, - []string{TemplateTLSCAsName, TemplateIdentityName, TemplateKubernetesName}, - []string{}, - ); err != nil { - return nil, trace.Wrap(err, "validating template configs") - } - return &k8s.OutputV1Config{ - Destination: dest, - Roles: c.Roles, - KubernetesCluster: c.KubernetesCluster, - }, nil - case hostCertConfigured: - if err := validateTemplates( - c.Configs, - []string{TemplateSSHHostCertName}, - []string{TemplateSSHHostCertName}, - ); err != nil { - return nil, trace.Wrap(err, "validating template configs") - } - - // Extract principals from template config - principals := []string{} - for _, c := range c.Configs { - if c.SSHHostCert != nil { - principals = c.SSHHostCert.Principals - break - } - } - return &ssh.HostOutputConfig{ - Destination: dest, - Roles: c.Roles, - Principals: principals, - }, nil - default: - if err := validateTemplates( - c.Configs, - []string{TemplateTLSCAsName, TemplateIdentityName, TemplateSSHClientName}, - []string{}, - ); err != nil { - return nil, trace.Wrap(err, "validating template configs") - } - return &identity.OutputConfig{ - Destination: dest, - Roles: c.Roles, - Cluster: c.Cluster, - }, nil - } -} - -type configV1 struct { - Onboarding onboarding.Config `yaml:"onboarding"` - Debug bool `yaml:"debug"` - AuthServer string `yaml:"auth_server"` - CertificateTTL time.Duration `yaml:"certificate_ttl"` - RenewalInterval time.Duration `yaml:"renewal_interval"` - Oneshot bool `yaml:"oneshot"` - FIPS bool `yaml:"fips"` - DiagAddr string `yaml:"diag_addr"` - - Destinations []configV1Destination `yaml:"destinations"` - StorageConfig *storageConfigV1 `yaml:"storage"` - - // This field doesn't exist in V1, but, it exists here so we can detect - // a scenario where for some reason we're trying to migrate a V2 config - // that's missing the version header. - Outputs []any `yaml:"outputs"` -} - -func (c *configV1) migrate() (*BotConfig, error) { - if len(c.Outputs) > 0 { - return nil, trace.BadParameter("config has been detected as potentially v1, but includes the v2 outputs field") - } - - var storage *StorageConfig - var err error - if c.StorageConfig != nil { - storage, err = c.StorageConfig.migrate() - if err != nil { - return nil, trace.Wrap(err, "migrating storage config") - } - } - - var outputs []ServiceConfig - for _, d := range c.Destinations { - o, err := d.migrate() - if err != nil { - return nil, trace.Wrap(err, "migrating output") - } - outputs = append(outputs, o) - } - - return &BotConfig{ - Version: V2, - - Onboarding: c.Onboarding, - Debug: c.Debug, - AuthServer: c.AuthServer, - CredentialLifetime: bot.CredentialLifetime{ - TTL: c.CertificateTTL, - RenewalInterval: c.RenewalInterval, - }, - Oneshot: c.Oneshot, - FIPS: c.FIPS, - DiagAddr: c.DiagAddr, - - Storage: storage, - Services: outputs, - }, nil -} diff --git a/lib/tbot/config/migrate_test.go b/lib/tbot/config/migrate_test.go deleted file mode 100644 index a7d272f3f3f74..0000000000000 --- a/lib/tbot/config/migrate_test.go +++ /dev/null @@ -1,1159 +0,0 @@ -/* - * 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 . - */ - -package config - -import ( - "bytes" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/gravitational/teleport/api/types" - "github.com/gravitational/teleport/lib/tbot/bot" - "github.com/gravitational/teleport/lib/tbot/bot/destination" - "github.com/gravitational/teleport/lib/tbot/bot/onboarding" - "github.com/gravitational/teleport/lib/tbot/botfs" - "github.com/gravitational/teleport/lib/tbot/services/application" - "github.com/gravitational/teleport/lib/tbot/services/database" - "github.com/gravitational/teleport/lib/tbot/services/identity" - "github.com/gravitational/teleport/lib/tbot/services/k8s" - "github.com/gravitational/teleport/lib/tbot/services/ssh" -) - -func TestMigrate(t *testing.T) { - tests := []struct { - name string - input string - wantError string - wantOutput *BotConfig - }{ - { - name: "very full config", - input: ` -auth_server: example.teleport.sh:443 -oneshot: true -debug: true -certificate_ttl: 30m -diag_addr: 127.0.0.1:621 -renewal_interval: 10m -onboarding: - join_method: "token" - token: "my-token" - ca_pins: - - "sha256:my-pin" -storage: - directory: - path: /path/storage - acls: required - symlinks: secure -destinations: -- directory: - path: /path/destination - roles: ["foo"] - configs: - - identity: {} - - ssh_client - - tls_cas -- memory: true - app: my-app - configs: - - identity: {} - - tls_cas: {} - - tls: {} -- memory: true - app: my-app -- memory: {} - kubernetes_cluster: my-kubernetes-cluster - configs: - - identity - - tls_cas - - kubernetes -- memory: {} - database: - service: "my-db-service" - database: "the-db" - username: "alice" -- memory: {} - database: - service: "my-db-service" - configs: - - mongo -- memory: {} - database: - service: "my-db-service" - configs: - - tls -- memory: {} - database: - service: "my-db-service" - configs: - - cockroach -- memory: {} - roles: ["foo"] - configs: - - ssh_host_cert: - principals: - - example.com - - second.example.com -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "example.teleport.sh:443", - Oneshot: true, - Debug: true, - CredentialLifetime: bot.CredentialLifetime{ - RenewalInterval: time.Minute * 10, - TTL: time.Minute * 30, - }, - DiagAddr: "127.0.0.1:621", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "my-token", - CAPins: []string{ - "sha256:my-pin", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/path/storage", - ACLs: botfs.ACLRequired, - Symlinks: botfs.SymlinksSecure, - }, - }, - Services: ServiceConfigs{ - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/path/destination", - }, - Roles: []string{"foo"}, - }, - &application.OutputConfig{ - Destination: &destination.Memory{}, - AppName: "my-app", - SpecificTLSExtensions: true, - }, - &application.OutputConfig{ - Destination: &destination.Memory{}, - AppName: "my-app", - }, - &k8s.OutputV1Config{ - Destination: &destination.Memory{}, - KubernetesCluster: "my-kubernetes-cluster", - }, - &database.OutputConfig{ - Destination: &destination.Memory{}, - Service: "my-db-service", - Database: "the-db", - Username: "alice", - Format: database.UnspecifiedDatabaseFormat, - }, - &database.OutputConfig{ - Destination: &destination.Memory{}, - Service: "my-db-service", - Format: database.MongoDatabaseFormat, - }, - &database.OutputConfig{ - Destination: &destination.Memory{}, - Service: "my-db-service", - Format: database.TLSDatabaseFormat, - }, - &database.OutputConfig{ - Destination: &destination.Memory{}, - Service: "my-db-service", - Format: database.CockroachDatabaseFormat, - }, - &ssh.HostOutputConfig{ - Destination: &destination.Memory{}, - Roles: []string{"foo"}, - Principals: []string{"example.com", "second.example.com"}, - }, - }, - }, - }, - // Backwards compat with GHA - { - name: "backwards compat with @teleport-actions/auth", - input: ` -auth_server: example.teleport.sh:443 -oneshot: true -debug: true -onboarding: - join_method: github - token: my-token -storage: - memory: true -destinations: -- directory: - path: /path/example - symlinks: try-secure - roles: [] -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "example.teleport.sh:443", - Oneshot: true, - Debug: true, - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodGitHub, - TokenValue: "my-token", - }, - Storage: &StorageConfig{ - Destination: &destination.Memory{}, - }, - Services: ServiceConfigs{ - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/path/example", - Symlinks: "try-secure", - }, - Roles: []string{}, - }, - }, - }, - }, - { - name: "backwards compat with @teleport-actions/auth-k8s", - input: ` -auth_server: example.teleport.sh:443 -oneshot: true -debug: true -onboarding: - join_method: github - token: my-token -storage: - memory: true -destinations: -- directory: - path: /path/example - symlinks: try-secure - roles: [] - kubernetes_cluster: my-cluster -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "example.teleport.sh:443", - Oneshot: true, - Debug: true, - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodGitHub, - TokenValue: "my-token", - }, - Storage: &StorageConfig{ - Destination: &destination.Memory{}, - }, - Services: ServiceConfigs{ - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/path/example", - Symlinks: "try-secure", - }, - Roles: []string{}, - KubernetesCluster: "my-cluster", - }, - }, - }, - }, - { - name: "backwards compat with @teleport-actions/auth-application", - input: ` -auth_server: example.teleport.sh:443 -oneshot: true -debug: true -onboarding: - join_method: github - token: my-token -storage: - memory: true -destinations: -- directory: - path: /path/example - symlinks: try-secure - roles: [] - app: my-app -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "example.teleport.sh:443", - Oneshot: true, - Debug: true, - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodGitHub, - TokenValue: "my-token", - }, - Storage: &StorageConfig{ - Destination: &destination.Memory{}, - }, - Services: ServiceConfigs{ - &application.OutputConfig{ - Destination: &destination.Directory{ - Path: "/path/example", - Symlinks: "try-secure", - }, - Roles: []string{}, - AppName: "my-app", - }, - }, - }, - }, - // Backwards compat with guides - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/deployment/jenkins/", - input: ` -auth_server: "auth.example.com:3025" -onboarding: - join_method: "token" - token: "00000000000000000000000000000000" - ca_pins: - - "sha256:1111111111111111111111111111111111111111111111111111111111111111" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "auth.example.com:3025", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "00000000000000000000000000000000", - CAPins: []string{ - "sha256:1111111111111111111111111111111111111111111111111111111111111111", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/databases/", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - - database: - service: example-server - username: alice - database: example -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &database.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - Service: "example-server", - Username: "alice", - Database: "example", - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/databases/ - mongo", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - - database: - service: example-server - username: alice - database: example - - # If using MongoDB, be sure to include the Mongo-formatted certificates: - configs: - - mongo -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &database.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - Format: database.MongoDatabaseFormat, - Service: "example-server", - Username: "alice", - Database: "example", - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/databases/ - cockroach", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - - database: - service: example-server - username: alice - database: example - - configs: - - cockroach -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &database.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - Format: database.CockroachDatabaseFormat, - Service: "example-server", - Username: "alice", - Database: "example", - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/databases/ - tls", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - - database: - service: example-server - username: alice - database: example - - configs: - - tls -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &database.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - Format: database.TLSDatabaseFormat, - Service: "example-server", - Username: "alice", - Database: "example", - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id - host-certificate", - input: ` -onboarding: - token: "1234abcd5678efgh9" - ca_path: "" - ca_pins: - - sha256:1234abcd5678efgh910ijklmnop - join_method: token -storage: - directory: - path: /var/lib/teleport/bot - symlinks: secure - acls: try -destinations: - - directory: - path: /opt/machine-id - configs: - - ssh_host_cert: - principals: [nodename.my.domain.com] -debug: false -auth_server: example.teleport.sh:443 -certificate_ttl: 1h0m0s -renewal_interval: 20m0s -oneshot: false -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "example.teleport.sh:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "1234abcd5678efgh9", - CAPins: []string{ - "sha256:1234abcd5678efgh910ijklmnop", - }, - }, - CredentialLifetime: bot.CredentialLifetime{ - RenewalInterval: DefaultRenewInterval, - TTL: DefaultCertificateTTL, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - Symlinks: "secure", - ACLs: "try", - }, - }, - Services: ServiceConfigs{ - &ssh.HostOutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - Principals: []string{"nodename.my.domain.com"}, - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/applications/", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - app: grafana-example -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &application.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - AppName: "grafana-example", - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/applications/ - with tls config", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - app: grafana-example - - configs: - - tls -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &application.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - AppName: "grafana-example", - SpecificTLSExtensions: true, - }, - }, - }, - }, - { - name: "backwards compat with https://goteleport.com/docs/enroll-resources/machine-id/access-guides/kubernetes/", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" - ca_pins: - - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - kubernetes_cluster: example-k8s-cluster -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - CAPins: []string{ - "sha256:abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678abdc1245efgh5678", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - KubernetesCluster: "example-k8s-cluster", - }, - }, - }, - }, - // Niche cases - { - name: "no storage config", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "token" - token: "abcd123-insecure-do-not-use-this" -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodToken, - TokenValue: "abcd123-insecure-do-not-use-this", - }, - Storage: nil, - Outputs: nil, - }, - }, - // Real-world cases - { - name: "real world 1", - input: ` -auth_server: "teleport.example.com:443" -onboarding: - join_method: "iam" - token: "iam-token-kube" -storage: - directory: - path: /var/lib/teleport/bot - symlinks: insecure - acls: off -debug: true -destinations: - - directory: - path: /opt/machine-id - symlinks: insecure - acls: off - - directory: - path: /opt/machine-id/tools - symlinks: insecure - acls: off - kubernetes_cluster: "tools" -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleport.example.com:443", - Onboarding: onboarding.Config{ - JoinMethod: types.JoinMethodIAM, - TokenValue: "iam-token-kube", - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - Symlinks: botfs.SymlinksInsecure, - ACLs: botfs.ACLOff, - }, - }, - Debug: true, - Services: ServiceConfigs{ - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - Symlinks: botfs.SymlinksInsecure, - ACLs: botfs.ACLOff, - }, - }, - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/opt/machine-id/tools", - Symlinks: botfs.SymlinksInsecure, - ACLs: botfs.ACLOff, - }, - KubernetesCluster: "tools", - }, - }, - }, - }, - { - name: "real world 2", - input: ` -storage: - directory: - path: /var/tmp/teleport/bot - symlinks: insecure - -destinations: - - directory: - path: /var/tmp/machine-id - symlinks: insecure -`, - wantOutput: &BotConfig{ - Version: V2, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/tmp/teleport/bot", - Symlinks: botfs.SymlinksInsecure, - }, - }, - Services: ServiceConfigs{ - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/var/tmp/machine-id", - Symlinks: botfs.SymlinksInsecure, - }, - }, - }, - }, - }, - { - name: "real world 3", - input: ` -Machine ID config created at /etc/tbot.yaml: -auth_server: teleportvm.example.com:443 -onboarding: - token: redacted -storage: - directory: /var/lib/teleport/bot - -destinations: - - directory: /opt/machine-id - roles: [access] - database: - service: self-hosted - username: alice - database: Payroll -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "teleportvm.example.com:443", - Onboarding: onboarding.Config{ - TokenValue: "redacted", - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &database.OutputConfig{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - Roles: []string{"access"}, - Service: "self-hosted", - Username: "alice", - Database: "Payroll", - }, - }, - }, - }, - { - name: "real world 4", - input: ` -auth_server: "redacted.teleport.sh:443" -onboarding: - join_method: "iam" - token: "redacted-scanner-token" - ca_pins: - - "sha256:redacted" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: /opt/machine-id - kubernetes_cluster: devops -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "redacted.teleport.sh:443", - Onboarding: onboarding.Config{ - TokenValue: "redacted-scanner-token", - JoinMethod: types.JoinMethodIAM, - CAPins: []string{ - "sha256:redacted", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/opt/machine-id", - }, - KubernetesCluster: "devops", - }, - }, - }, - }, - { - name: "real world 5", - input: ` -auth_server: "redacted.teleport.sh:443" -onboarding: - join_method: "iam" - token: "redacted-argocd-token" - ca_pins: - - "sha256:redacted" -storage: - directory: /var/lib/teleport/bot -destinations: - - directory: - path: /mount/redacted-prod-global - acls: off - kubernetes_cluster: redacted-prod-global - - directory: - path: /mount/redacted-prod-au - acls: off - kubernetes_cluster: redacted-prod-au - - directory: - path: /mount/redacted-prod-eu2 - acls: off - kubernetes_cluster: redacted-prod-eu2 - - directory: - path: /mount/redacted-prod-ca - acls: off - kubernetes_cluster: redacted-prod-ca - - directory: - path: /mount/redacted-prod-us - acls: off - kubernetes_cluster: redacted-prod-us -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "redacted.teleport.sh:443", - Onboarding: onboarding.Config{ - TokenValue: "redacted-argocd-token", - JoinMethod: types.JoinMethodIAM, - CAPins: []string{ - "sha256:redacted", - }, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/bot", - }, - }, - Services: ServiceConfigs{ - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/mount/redacted-prod-global", - ACLs: botfs.ACLOff, - }, - KubernetesCluster: "redacted-prod-global", - }, - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/mount/redacted-prod-au", - ACLs: botfs.ACLOff, - }, - KubernetesCluster: "redacted-prod-au", - }, - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/mount/redacted-prod-eu2", - ACLs: botfs.ACLOff, - }, - KubernetesCluster: "redacted-prod-eu2", - }, - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/mount/redacted-prod-ca", - ACLs: botfs.ACLOff, - }, - KubernetesCluster: "redacted-prod-ca", - }, - &k8s.OutputV1Config{ - Destination: &destination.Directory{ - Path: "/mount/redacted-prod-us", - ACLs: botfs.ACLOff, - }, - KubernetesCluster: "redacted-prod-us", - }, - }, - }, - }, - { - name: "real world 6", - // up to 10 roles/destinations depending on the environment - input: ` -auth_server: "redacted.teleport.sh:443" -onboarding: - join_method: "token" - token: "redacted" - -storage: - directory: "/var/lib/teleport/tbot" - -destinations: - - directory: - acls: required - path: /path/to/role1_creds - roles: - - role1 - - directory: - acls: required - path: /path/to/role2_creds - roles: - - role2 - - directory: - acls: required - path: /path/to/roleN_creds - roles: - - roleN -`, - wantOutput: &BotConfig{ - Version: V2, - AuthServer: "redacted.teleport.sh:443", - Onboarding: onboarding.Config{ - TokenValue: "redacted", - JoinMethod: types.JoinMethodToken, - }, - Storage: &StorageConfig{ - Destination: &destination.Directory{ - Path: "/var/lib/teleport/tbot", - }, - }, - Services: ServiceConfigs{ - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/path/to/role1_creds", - ACLs: botfs.ACLRequired, - }, - Roles: []string{"role1"}, - }, - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/path/to/role2_creds", - ACLs: botfs.ACLRequired, - }, - Roles: []string{"role2"}, - }, - &identity.OutputConfig{ - Destination: &destination.Directory{ - Path: "/path/to/roleN_creds", - ACLs: botfs.ACLRequired, - }, - Roles: []string{"roleN"}, - }, - }, - }, - }, - // Error cases - { - name: "storage config with no destination", - input: `storage: {}`, - wantError: "at least one of `memory' and 'directory' must be specified", - }, - { - name: "storage config with absurd destination", - input: ` -storage: - memory: true - directory: - path: /opt/machine-id`, - wantError: "both 'memory' and 'directory' cannot be specified", - }, - { - name: "destination with duplicate config types", - input: ` -destinations: -- memory: true - configs: - - ssh_client - - ssh_client: {}`, - wantError: `multiple config template entries found for "ssh_client"`, - }, - { - name: "destination with unsupported config type", - input: ` -destinations: -- memory: true - app: my-app - configs: - - kubernetes`, - wantError: `config template "kubernetes" unsupported by new output type`, - }, - { - name: "destination with empty config type", - input: ` -destinations: -- memory: true - configs: - - {}`, - wantError: `config template must not be empty`, - }, - { - name: "destination with indeterminate type", - input: ` -destinations: -- memory: true - app: my-app - kubernetes_cluster: my-cluster -`, - wantError: `multiple potential output types detected, cannot determine correct type`, - }, - { - name: "v2 config without version field", - input: ` -outputs: - - type: identity - destination: - type: memory - - type: identity - destination: - type: memory -`, - wantError: "config has been detected as potentially v1, but includes the v2 outputs field", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - r := bytes.NewReader([]byte(tt.input)) - out, err := ReadConfig(r, true) - if tt.wantError != "" { - require.ErrorContains(t, err, tt.wantError) - return - } - require.NoError(t, err) - require.Equal(t, tt.wantOutput, out) - - }) - } -}