Skip to content

Commit

Permalink
dynamic SSH host certs use configurable algorithms
Browse files Browse the repository at this point in the history
  • Loading branch information
nklaassen committed Sep 6, 2024
1 parent 5f67a8b commit 0165bbb
Show file tree
Hide file tree
Showing 34 changed files with 143 additions and 425 deletions.
161 changes: 8 additions & 153 deletions lib/config/openssh/openssh.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,14 @@
package openssh

import (
"bytes"
"os/exec"
"regexp"
"strconv"
"io"
"strings"
"text/template"

"github.com/coreos/go-semver/semver"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"

"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/utils"
)

// proxyCommandQuote prepares a string for insertion into the ssh_config
Expand All @@ -57,7 +51,6 @@ Host *.{{ $clusterName }} {{ $dot.ProxyHost }}
UserKnownHostsFile "{{ $dot.KnownHostsPath }}"
IdentityFile "{{ $dot.IdentityFilePath }}"
CertificateFile "{{ $dot.CertificateFilePath }}"
HostKeyAlgorithms {{ if $dot.NewerHostKeyAlgorithmsSupported }}[email protected],[email protected],{{ end }}[email protected]
{{- if ne $dot.Username "" }}
User "{{ $dot.Username }}"
{{- end }}
Expand Down Expand Up @@ -110,16 +103,8 @@ type SSHConfigParameters struct {

type sshTmplParams struct {
SSHConfigParameters
sshConfigOptions
}

// openSSHVersionRegex is a regex used to parse OpenSSH version strings.
var openSSHVersionRegex = regexp.MustCompile(`^OpenSSH_(?P<major>\d+)\.(?P<minor>\d+)(?:p(?P<patch>\d+))?`)

// openSSHMinVersionForHostAlgos is the first version that understands all host keys required by us.
// HostKeyAlgorithms will be added to ssh config if the version is above listed here.
var openSSHMinVersionForHostAlgos = semver.New("7.8.0")

// SSHConfigApps represent apps that support ssh config generation.
type SSHConfigApps string

Expand All @@ -128,130 +113,14 @@ const (
TbotApp SSHConfigApps = teleport.ComponentTBot
)

// parseSSHVersion attempts to parse the local SSH version, used to determine
// certain config template parameters for client version compatibility.
func parseSSHVersion(versionString string) (*semver.Version, error) {
versionTokens := strings.Split(versionString, " ")
if len(versionTokens) == 0 {
return nil, trace.BadParameter("invalid version string: %s", versionString)
}

versionID := versionTokens[0]
matches := openSSHVersionRegex.FindStringSubmatch(versionID)
if matches == nil {
return nil, trace.BadParameter("cannot parse version string: %q", versionID)
}

major, err := strconv.Atoi(matches[1])
if err != nil {
return nil, trace.Wrap(err, "invalid major version number: %s", matches[1])
}

minor, err := strconv.Atoi(matches[2])
if err != nil {
return nil, trace.Wrap(err, "invalid minor version number: %s", matches[2])
}

patch := 0
if matches[3] != "" {
patch, err = strconv.Atoi(matches[3])
if err != nil {
return nil, trace.Wrap(err, "invalid patch version number: %s", matches[3])
}
}

return &semver.Version{
Major: int64(major),
Minor: int64(minor),
Patch: int64(patch),
}, nil
}

// GetSystemSSHVersion attempts to query the system SSH for its current version.
func GetSystemSSHVersion() (*semver.Version, error) {
var out bytes.Buffer

cmd := exec.Command("ssh", "-V")
cmd.Stderr = &out

err := cmd.Run()
if err != nil {
return nil, trace.Wrap(err)
}

return parseSSHVersion(out.String())
}

type sshConfigOptions struct {
// NewerHostKeyAlgorithmsSupported when true sets HostKeyAlgorithms OpenSSH configuration option
// to SHA256/512 compatible algorithms. Otherwise, SHA-1 is being used.
NewerHostKeyAlgorithmsSupported bool
}

func (c *sshConfigOptions) String() string {
sb := &strings.Builder{}
sb.WriteString("sshConfigOptions: ")

if c.NewerHostKeyAlgorithmsSupported {
sb.WriteString("HostKeyAlgorithms will include SHA-256, SHA-512 and SHA-1")
} else {
sb.WriteString("HostKeyAlgorithms will include SHA-1")
}

return sb.String()
}

func isNewerHostKeyAlgorithmsSupported(ver *semver.Version) bool {
return !ver.LessThan(*openSSHMinVersionForHostAlgos)
}

func getSSHConfigOptions(sshVer *semver.Version) *sshConfigOptions {
return &sshConfigOptions{
NewerHostKeyAlgorithmsSupported: isNewerHostKeyAlgorithmsSupported(sshVer),
}
}

func getDefaultSSHConfigOptions() *sshConfigOptions {
return &sshConfigOptions{
NewerHostKeyAlgorithmsSupported: true,
}
}

type SSHConfig struct {
getSSHVersion func() (*semver.Version, error)
log logrus.FieldLogger
}

// NewSSHConfig creates a SSHConfig initialized with provided values or defaults otherwise.
func NewSSHConfig(getSSHVersion func() (*semver.Version, error), log logrus.FieldLogger) *SSHConfig {
if getSSHVersion == nil {
getSSHVersion = GetSystemSSHVersion
}
if log == nil {
log = utils.NewLogger()
}
return &SSHConfig{getSSHVersion: getSSHVersion, log: log}
}

func (c *SSHConfig) GetSSHConfig(sb *strings.Builder, config *SSHConfigParameters) error {
var sshOptions *sshConfigOptions
version, err := c.getSSHVersion()
if err != nil {
c.log.WithError(err).Debugf("Could not determine SSH version, using default SSH config")
sshOptions = getDefaultSSHConfigOptions()
} else {
c.log.Debugf("Found OpenSSH version %s", version)
sshOptions = getSSHConfigOptions(version)
}
// WriteSSHConfig generates an ssh_config file for OpenSSH clients.
func WriteSSHConfig(w io.Writer, config *SSHConfigParameters) error {
if config.Port == 0 {
config.Port = defaults.SSHServerListenPort
}

c.log.Debugf("Using SSH options: %s", sshOptions)

if err := sshConfigTemplate.Execute(sb, sshTmplParams{
if err := sshConfigTemplate.Execute(w, sshTmplParams{
SSHConfigParameters: *config,
sshConfigOptions: *sshOptions,
}); err != nil {
return trace.Wrap(err)
}
Expand All @@ -268,9 +137,8 @@ var muxedSSHConfigTemplate = template.Must(template.New("muxed-ssh-config").Func
Host *.{{ $clusterName }}
Port {{ $dot.Port }}
UserKnownHostsFile {{ proxyCommandQuote $dot.KnownHostsPath }}
HostKeyAlgorithms {{ if $dot.NewerHostKeyAlgorithmsSupported }}[email protected],[email protected],{{ end }}[email protected]
IdentityFile none
IdentityAgent {{ proxyCommandQuote $dot.AgentSocketPath }}
IdentityAgent {{ proxyCommandQuote $dot.AgentSocketPath }}
ProxyCommand {{range $v := $dot.ProxyCommand}}{{ proxyCommandQuote $v }} {{end}}{{ proxyCommandQuote $dot.MuxSocketPath }} '%h:%p|{{ $clusterName }}'
ProxyUseFDPass yes
{{- end }}
Expand All @@ -292,29 +160,16 @@ type MuxedSSHConfigParameters struct {

type muxedSSHTmplParams struct {
MuxedSSHConfigParameters
sshConfigOptions
}

// GetMuxedSSHConfig generates a ssh_config file for the ssh-multiplexer service.
func (c *SSHConfig) GetMuxedSSHConfig(sb *strings.Builder, config *MuxedSSHConfigParameters) error {
var sshOptions *sshConfigOptions
version, err := c.getSSHVersion()
if err != nil {
c.log.WithError(err).Debugf("Could not determine SSH version, using default SSH config")
sshOptions = getDefaultSSHConfigOptions()
} else {
c.log.Debugf("Found OpenSSH version %s", version)
sshOptions = getSSHConfigOptions(version)
}
// WriteMuxedSSHConfig generates a ssh_config file for the ssh-multiplexer service.
func WriteMuxedSSHConfig(w io.Writer, config *MuxedSSHConfigParameters) error {
if config.Port == 0 {
config.Port = defaults.SSHServerListenPort
}

c.log.Debugf("Using SSH options: %s", sshOptions)

if err := muxedSSHConfigTemplate.Execute(sb, muxedSSHTmplParams{
if err := muxedSSHConfigTemplate.Execute(w, muxedSSHTmplParams{
MuxedSSHConfigParameters: *config,
sshConfigOptions: *sshOptions,
}); err != nil {
return trace.Wrap(err)
}
Expand Down
68 changes: 4 additions & 64 deletions lib/config/openssh/openssh_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,58 +22,12 @@ import (
"strings"
"testing"

"github.com/coreos/go-semver/semver"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/lib/utils/golden"
)

func TestParseSSHVersion(t *testing.T) {
tests := []struct {
str string
version *semver.Version
err bool
}{
{
str: "OpenSSH_8.2p1 Ubuntu-4ubuntu0.4, OpenSSL 1.1.1f 31 Mar 2020",
version: semver.New("8.2.1"),
},
{
str: "OpenSSH_8.8p1, OpenSSL 1.1.1m 14 Dec 2021",
version: semver.New("8.8.1"),
},
{
str: "OpenSSH_7.5p1, OpenSSL 1.0.2s-freebsd 28 May 2019",
version: semver.New("7.5.1"),
},
{
str: "OpenSSH_7.9p1 Raspbian-10+deb10u2, OpenSSL 1.1.1d 10 Sep 2019",
version: semver.New("7.9.1"),
},
{
// Couldn't find a full example but in theory patch is optional:
str: "OpenSSH_8.1 foo",
version: semver.New("8.1.0"),
},
{
str: "Teleport v8.0.0-dev.40 git:v8.0.0-dev.40-0-ge9194c256 go1.17.2",
err: true,
},
}

for _, test := range tests {
version, err := parseSSHVersion(test.str)
if test.err {
require.Error(t, err)
} else {
require.NoError(t, err)
require.True(t, version.Equal(*test.version), "got version = %v, want = %v", version, test.version)
}
}
}

func TestSSHConfig_GetSSHConfig(t *testing.T) {
func TestWriteSSHConfig(t *testing.T) {
tests := []struct {
name string
sshVersion string
Expand Down Expand Up @@ -157,15 +111,8 @@ func TestSSHConfig_GetSSHConfig(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &SSHConfig{
getSSHVersion: func() (*semver.Version, error) {
return semver.New(tt.sshVersion), nil
},
log: logrus.New(),
}

sb := &strings.Builder{}
err := c.GetSSHConfig(sb, tt.config)
err := WriteSSHConfig(sb, tt.config)
if golden.ShouldSet() {
golden.Set(t, []byte(sb.String()))
}
Expand All @@ -175,7 +122,7 @@ func TestSSHConfig_GetSSHConfig(t *testing.T) {
}
}

func TestSSHConfig_GetMuxedSSHConfig(t *testing.T) {
func TestWriteMuxedSSHConfig(t *testing.T) {
tests := []struct {
name string
sshVersion string
Expand Down Expand Up @@ -221,15 +168,8 @@ func TestSSHConfig_GetMuxedSSHConfig(t *testing.T) {

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &SSHConfig{
getSSHVersion: func() (*semver.Version, error) {
return semver.New(tt.sshVersion), nil
},
log: logrus.New(),
}

sb := &strings.Builder{}
err := c.GetMuxedSSHConfig(sb, tt.config)
err := WriteMuxedSSHConfig(sb, tt.config)
if golden.ShouldSet() {
golden.Set(t, []byte(sb.String()))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
Host *.example.com
Port 3022
UserKnownHostsFile '/opt/machine-id/known_hosts'
HostKeyAlgorithms [email protected]
IdentityFile none
IdentityAgent '/opt/machine-id/agent.sock'
IdentityAgent '/opt/machine-id/agent.sock'
ProxyCommand '/bin/fdpass-teleport' 'foo' '/opt/machine-id/v1.sock' '%h:%p|example.com'
ProxyUseFDPass yes
# End generated Teleport configuration
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,15 @@
Host *.example.com
Port 3022
UserKnownHostsFile '/opt/machine-id/known_hosts'
HostKeyAlgorithms [email protected],[email protected],[email protected]
IdentityFile none
IdentityAgent '/opt/machine-id/agent.sock'
IdentityAgent '/opt/machine-id/agent.sock'
ProxyCommand '/bin/fdpass-teleport' 'foo' '/opt/machine-id/v1.sock' '%h:%p|example.com'
ProxyUseFDPass yes
Host *.example.org
Port 3022
UserKnownHostsFile '/opt/machine-id/known_hosts'
HostKeyAlgorithms [email protected],[email protected],[email protected]
IdentityFile none
IdentityAgent '/opt/machine-id/agent.sock'
IdentityAgent '/opt/machine-id/agent.sock'
ProxyCommand '/bin/fdpass-teleport' 'foo' '/opt/machine-id/v1.sock' '%h:%p|example.org'
ProxyUseFDPass yes
# End generated Teleport configuration
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@
Host *.example.com
Port 3022
UserKnownHostsFile '/opt/machine-id/known_hosts'
HostKeyAlgorithms [email protected],[email protected],[email protected]
IdentityFile none
IdentityAgent '/opt/machine-id/agent.sock'
IdentityAgent '/opt/machine-id/agent.sock'
ProxyCommand '/bin/fdpass-teleport' 'foo' '/opt/machine-id/v1.sock' '%h:%p|example.com'
ProxyUseFDPass yes
# End generated Teleport configuration
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Host *.example.com proxy.example.com
UserKnownHostsFile "/home/alice/.tsh/known_hosts"
IdentityFile "/home/alice/.tsh/keys/example.com/bob"
CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub"
HostKeyAlgorithms [email protected]

# Flags for all example.com hosts except the proxy
Host *.example.com !proxy.example.com
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ Host *.root proxy.example.com
UserKnownHostsFile "/home/alice/.tsh/known_hosts"
IdentityFile "/home/alice/.tsh/keys/example.com/bob"
CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub"
HostKeyAlgorithms [email protected],[email protected],[email protected]

# Flags for all root hosts except the proxy
Host *.root !proxy.example.com
Expand All @@ -16,7 +15,6 @@ Host *.leaf proxy.example.com
UserKnownHostsFile "/home/alice/.tsh/known_hosts"
IdentityFile "/home/alice/.tsh/keys/example.com/bob"
CertificateFile "/home/alice/.tsh/keys/example.com/bob-ssh/example.com-cert.pub"
HostKeyAlgorithms [email protected],[email protected],[email protected]

# Flags for all leaf hosts except the proxy
Host *.leaf !proxy.example.com
Expand Down
Loading

0 comments on commit 0165bbb

Please sign in to comment.