mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
Implement global tsh config file: /etc/tsh.yaml
(#12598)
* Implement global tsh config file: `/etc/tsh.yaml`. The default location can be changed with `TELEPORT_GLOBAL_TSH_CONFIG` env var. * The user config file is merged with the global config file. The user config file has a higher priority. * If the global config file is absent, no error is raised.
This commit is contained in:
parent
30bf2626c5
commit
80bdb11c89
|
@ -698,10 +698,15 @@ Environment variables configure your tsh client and can help you avoid using fla
|
|||
| TELEPORT_USER | A Teleport user name | alice |
|
||||
| TELEPORT_ADD_KEYS_TO_AGENT | Specifies if the user certificate should be stored on the running SSH agent | yes, no, auto, only |
|
||||
| TELEPORT_USE_LOCAL_SSH_AGENT | Disable or enable local SSH agent integration | true, false |
|
||||
| TELEPORT_GLOBAL_TSH_CONFIG | Override location of global `tsh` config file from default `/etc/tsh.yaml` | /opt/teleport/tsh.yaml |
|
||||
|
||||
### tsh configuration file
|
||||
### tsh configuration files
|
||||
|
||||
`tsh` has an optional configuration file that is stored in `$TELEPORT_HOME/config/config.yaml`.
|
||||
`tsh` has an optional configuration files:
|
||||
- global, shared config: `/etc/tsh.yaml`. Location can be overridden with `TELEPORT_GLOBAL_TSH_CONFIG` environment variable.
|
||||
- user specific config: `$TELEPORT_HOME/config/config.yaml`. Unless changed, `TELEPORT_HOME` defaults to `~/.tsh`.
|
||||
|
||||
The settings from both are merged, with the user config taking precedence.
|
||||
|
||||
The `tsh` configuration file enables you to specify HTTP headers to be
|
||||
included in requests to Teleport Proxy Servers with addresses matching
|
||||
|
|
|
@ -39,7 +39,6 @@ import (
|
|||
"github.com/gravitational/teleport"
|
||||
"github.com/gravitational/teleport/api/constants"
|
||||
apidefaults "github.com/gravitational/teleport/api/defaults"
|
||||
"github.com/gravitational/teleport/api/profile"
|
||||
"github.com/gravitational/teleport/api/types"
|
||||
"github.com/gravitational/teleport/api/types/wrappers"
|
||||
apiutils "github.com/gravitational/teleport/api/utils"
|
||||
|
@ -288,6 +287,9 @@ type CLIConf struct {
|
|||
// HomePath is where tsh stores profiles
|
||||
HomePath string
|
||||
|
||||
// GlobalTshConfigPath is a path to global TSH config. Can be overridden with TELEPORT_GLOBAL_TSH_CONFIG.
|
||||
GlobalTshConfigPath string
|
||||
|
||||
// LocalProxyPort is a port used by local proxy listener.
|
||||
LocalProxyPort string
|
||||
// LocalProxyCertFile is the client certificate used by local proxy.
|
||||
|
@ -381,6 +383,7 @@ const (
|
|||
userEnvVar = "TELEPORT_USER"
|
||||
addKeysToAgentEnvVar = "TELEPORT_ADD_KEYS_TO_AGENT"
|
||||
useLocalSSHAgentEnvVar = "TELEPORT_USE_LOCAL_SSH_AGENT"
|
||||
globalTshConfigEnvVar = "TELEPORT_GLOBAL_TSH_CONFIG"
|
||||
|
||||
clusterHelp = "Specify the Teleport cluster to connect"
|
||||
browserHelp = "Set to 'none' to suppress browser opening on login"
|
||||
|
@ -754,11 +757,11 @@ func Run(args []string, opts ...cliOption) error {
|
|||
|
||||
setEnvFlags(&cf, os.Getenv)
|
||||
|
||||
fullConfigPath := filepath.Join(profile.FullProfilePath(cf.HomePath), tshConfigPath)
|
||||
confOptions, err := loadConfig(fullConfigPath)
|
||||
confOptions, err := loadAllConfigs(cf)
|
||||
if err != nil {
|
||||
return trace.Wrap(err, "failed to load tsh config from %s", fullConfigPath)
|
||||
return trace.Wrap(err)
|
||||
}
|
||||
|
||||
cf.ExtraProxyHeaders = confOptions.ExtraHeaders
|
||||
|
||||
switch command {
|
||||
|
@ -3007,7 +3010,10 @@ func setEnvFlags(cf *CLIConf, fn envGetter) {
|
|||
if cf.KubernetesCluster == "" {
|
||||
setKubernetesClusterFromEnv(cf, fn)
|
||||
}
|
||||
|
||||
// these can only be set with env vars.
|
||||
setTeleportHomeFromEnv(cf, fn)
|
||||
setGlobalTshConfigPathFromEnv(cf, fn)
|
||||
}
|
||||
|
||||
// setSiteNameFromEnv sets teleport site name from environment if configured.
|
||||
|
@ -3035,6 +3041,13 @@ func setKubernetesClusterFromEnv(cf *CLIConf, fn envGetter) {
|
|||
}
|
||||
}
|
||||
|
||||
// setGlobalTshConfigPathFromEnv sets path to global tsh config file.
|
||||
func setGlobalTshConfigPathFromEnv(cf *CLIConf, fn envGetter) {
|
||||
if configPath := fn(globalTshConfigEnvVar); configPath != "" {
|
||||
cf.GlobalTshConfigPath = path.Clean(configPath)
|
||||
}
|
||||
}
|
||||
|
||||
func handleUnimplementedError(ctx context.Context, perr error, cf CLIConf) error {
|
||||
const (
|
||||
errMsgFormat = "This server does not implement this feature yet. Likely the client version you are using is newer than the server. The server version: %v, the client version: %v. Please upgrade the server."
|
||||
|
|
|
@ -908,6 +908,20 @@ func TestEnvFlags(t *testing.T) {
|
|||
},
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("tsh global config path", func(t *testing.T) {
|
||||
t.Run("nothing set", testEnvFlag(testCase{
|
||||
outCLIConf: CLIConf{},
|
||||
}))
|
||||
t.Run("TELEPORT_GLOBAL_TSH_CONFIG set", testEnvFlag(testCase{
|
||||
envMap: map[string]string{
|
||||
globalTshConfigEnvVar: "/opt/teleport/tsh.yaml",
|
||||
},
|
||||
outCLIConf: CLIConf{
|
||||
GlobalTshConfigPath: "/opt/teleport/tsh.yaml",
|
||||
},
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestKubeConfigUpdate(t *testing.T) {
|
||||
|
|
|
@ -20,6 +20,9 @@ import (
|
|||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gravitational/teleport/api/profile"
|
||||
|
||||
"github.com/gravitational/trace"
|
||||
"gopkg.in/yaml.v2"
|
||||
|
@ -30,11 +33,14 @@ import (
|
|||
// unmarshal errors.
|
||||
const tshConfigPath = "config/config.yaml"
|
||||
|
||||
// default location of global tsh config file.
|
||||
const globalTshConfigPathDefault = "/etc/tsh.yaml"
|
||||
|
||||
// TshConfig represents configuration loaded from the tsh config file.
|
||||
type TshConfig struct {
|
||||
// ExtraHeaders are additional http headers to be included in
|
||||
// webclient requests.
|
||||
ExtraHeaders []ExtraProxyHeaders `yaml:"add_headers"`
|
||||
ExtraHeaders []ExtraProxyHeaders `yaml:"add_headers,omitempty"`
|
||||
}
|
||||
|
||||
// ExtraProxyHeaders represents the headers to include with the
|
||||
|
@ -46,6 +52,26 @@ type ExtraProxyHeaders struct {
|
|||
Headers map[string]string `yaml:"headers,omitempty"`
|
||||
}
|
||||
|
||||
// Merge two configs into one. The passed in otherConfig argument has higher priority.
|
||||
func (config *TshConfig) Merge(otherConfig *TshConfig) TshConfig {
|
||||
baseConfig := config
|
||||
if baseConfig == nil {
|
||||
baseConfig = &TshConfig{}
|
||||
}
|
||||
|
||||
if otherConfig == nil {
|
||||
otherConfig = &TshConfig{}
|
||||
}
|
||||
|
||||
newConfig := TshConfig{}
|
||||
|
||||
// extra headers
|
||||
newConfig.ExtraHeaders = append(baseConfig.ExtraHeaders, otherConfig.ExtraHeaders...)
|
||||
|
||||
return newConfig
|
||||
}
|
||||
|
||||
// loadConfig load a single config file from given path. If the path does not exist, an empty config is returned instead.
|
||||
func loadConfig(fullConfigPath string) (*TshConfig, error) {
|
||||
bs, err := os.ReadFile(fullConfigPath)
|
||||
if err != nil {
|
||||
|
@ -61,3 +87,26 @@ func loadConfig(fullConfigPath string) (*TshConfig, error) {
|
|||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// loadAllConfigs loads all tsh configs and merges them in appropriate order.
|
||||
func loadAllConfigs(cf CLIConf) (*TshConfig, error) {
|
||||
// default to globalTshConfigPathDefault
|
||||
globalConfigPath := cf.GlobalTshConfigPath
|
||||
if globalConfigPath == "" {
|
||||
globalConfigPath = globalTshConfigPathDefault
|
||||
}
|
||||
|
||||
globalConf, err := loadConfig(globalConfigPath)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to load global tsh config from %q", cf.GlobalTshConfigPath)
|
||||
}
|
||||
|
||||
fullConfigPath := filepath.Join(profile.FullProfilePath(cf.HomePath), tshConfigPath)
|
||||
userConf, err := loadConfig(fullConfigPath)
|
||||
if err != nil {
|
||||
return nil, trace.Wrap(err, "failed to load tsh config from %q", fullConfigPath)
|
||||
}
|
||||
|
||||
confOptions := globalConf.Merge(userConf)
|
||||
return &confOptions, nil
|
||||
}
|
||||
|
|
|
@ -18,10 +18,12 @@ package main
|
|||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
func TestLoadConfigNonExistingFile(t *testing.T) {
|
||||
|
@ -43,3 +45,165 @@ func TestLoadConfigEmptyFile(t *testing.T) {
|
|||
require.NoError(t, gotErr)
|
||||
require.Equal(t, &TshConfig{}, gotConfig)
|
||||
}
|
||||
|
||||
func TestLoadAllConfigs(t *testing.T) {
|
||||
writeConf := func(fn string, config TshConfig) {
|
||||
dir, _ := path.Split(fn)
|
||||
err := os.MkdirAll(dir, 0777)
|
||||
require.NoError(t, err)
|
||||
out, err := yaml.Marshal(config)
|
||||
require.NoError(t, err)
|
||||
err = os.WriteFile(fn, out, 0777)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
tmp := t.TempDir()
|
||||
|
||||
globalPath := path.Join(tmp, "etc", "tsh_global.yaml")
|
||||
globalConf := TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "global",
|
||||
Headers: map[string]string{"bar": "123"},
|
||||
}},
|
||||
}
|
||||
|
||||
homeDir := path.Join(tmp, "home", "myuser", ".tsh")
|
||||
userPath := path.Join(homeDir, "config", "config.yaml")
|
||||
userConf := TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "user",
|
||||
Headers: map[string]string{"bar": "456"},
|
||||
}},
|
||||
}
|
||||
|
||||
writeConf(globalPath, globalConf)
|
||||
writeConf(userPath, userConf)
|
||||
|
||||
config, err := loadAllConfigs(CLIConf{
|
||||
GlobalTshConfigPath: globalPath,
|
||||
HomePath: homeDir,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{
|
||||
{
|
||||
Proxy: "global",
|
||||
Headers: map[string]string{"bar": "123"},
|
||||
},
|
||||
{
|
||||
Proxy: "user",
|
||||
Headers: map[string]string{"bar": "456"},
|
||||
},
|
||||
},
|
||||
}, config)
|
||||
|
||||
}
|
||||
|
||||
func TestTshConfigMerge(t *testing.T) {
|
||||
sampleConfig := TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "baz",
|
||||
},
|
||||
}},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config1 *TshConfig
|
||||
config2 *TshConfig
|
||||
want TshConfig
|
||||
}{
|
||||
{
|
||||
name: "empty + empty = empty",
|
||||
config1: nil,
|
||||
config2: nil,
|
||||
want: TshConfig{},
|
||||
},
|
||||
{
|
||||
name: "empty + x = x",
|
||||
config1: &sampleConfig,
|
||||
config2: nil,
|
||||
want: sampleConfig,
|
||||
},
|
||||
{
|
||||
name: "x + empty = x",
|
||||
config1: nil,
|
||||
config2: &sampleConfig,
|
||||
want: sampleConfig,
|
||||
},
|
||||
{
|
||||
name: "headers combine different proxies",
|
||||
config1: &TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "123",
|
||||
},
|
||||
}}},
|
||||
config2: &TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "bar",
|
||||
Headers: map[string]string{
|
||||
"baz": "456",
|
||||
},
|
||||
}}},
|
||||
want: TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{
|
||||
{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
Proxy: "bar",
|
||||
Headers: map[string]string{
|
||||
"baz": "456",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "headers combine same proxy",
|
||||
config1: &TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "123",
|
||||
},
|
||||
}}},
|
||||
config2: &TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "456",
|
||||
},
|
||||
}}},
|
||||
want: TshConfig{
|
||||
ExtraHeaders: []ExtraProxyHeaders{
|
||||
{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "123",
|
||||
},
|
||||
},
|
||||
{
|
||||
Proxy: "foo",
|
||||
Headers: map[string]string{
|
||||
"bar": "456",
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
config3 := tt.config1.Merge(tt.config2)
|
||||
require.Equal(t, tt.want, config3)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue