Skip to content

Commit 01f4e02

Browse files
author
Igor Drozdov
committed
Merge branch 'sh-sshd-add-host-cert-support' into 'main'
gitlab-sshd: Add support for configuring host certificates See merge request gitlab-org/gitlab-shell!661
2 parents 34ec4ec + 4919ec7 commit 01f4e02

File tree

12 files changed

+191
-8
lines changed

12 files changed

+191
-8
lines changed

config.yml.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,7 @@ sshd:
9999
- /run/secrets/ssh-hostkeys/ssh_host_rsa_key
100100
- /run/secrets/ssh-hostkeys/ssh_host_ecdsa_key
101101
- /run/secrets/ssh-hostkeys/ssh_host_ed25519_key
102+
host_key_certs:
103+
- /run/secrets/ssh-hostkeys/ssh_host_rsa_key-cert.pub
104+
- /run/secrets/ssh-hostkeys/ssh_host_ecdsa_key-cert.pub
105+
- /run/secrets/ssh-hostkeys/ssh_host_ed25519_key-cert.pub

internal/config/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ type ServerConfig struct {
3636
ReadinessProbe string `yaml:"readiness_probe"`
3737
LivenessProbe string `yaml:"liveness_probe"`
3838
HostKeyFiles []string `yaml:"host_key_files,omitempty"`
39+
HostCertFiles []string `yaml:"host_cert_files,omitempty"`
3940
MACs []string `yaml:"macs"`
4041
KexAlgorithms []string `yaml:"kex_algorithms"`
4142
Ciphers []string `yaml:"ciphers"`

internal/sshd/server_config.go

Lines changed: 64 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,17 +39,14 @@ var (
3939
type serverConfig struct {
4040
cfg *config.Config
4141
hostKeys []ssh.Signer
42+
hostKeyToCertMap map[string]*ssh.Certificate
4243
authorizedKeysClient *authorizedkeys.Client
4344
}
4445

45-
func newServerConfig(cfg *config.Config) (*serverConfig, error) {
46-
authorizedKeysClient, err := authorizedkeys.NewClient(cfg)
47-
if err != nil {
48-
return nil, fmt.Errorf("failed to initialize GitLab client: %w", err)
49-
}
50-
46+
func parseHostKeys(keyFiles []string) []ssh.Signer {
5147
var hostKeys []ssh.Signer
52-
for _, filename := range cfg.Server.HostKeyFiles {
48+
49+
for _, filename := range keyFiles {
5350
keyRaw, err := os.ReadFile(filename)
5451
if err != nil {
5552
log.WithError(err).WithFields(log.Fields{"filename": filename}).Warn("Failed to read host key")
@@ -63,11 +60,70 @@ func newServerConfig(cfg *config.Config) (*serverConfig, error) {
6360

6461
hostKeys = append(hostKeys, key)
6562
}
63+
64+
return hostKeys
65+
}
66+
67+
func parseHostCerts(hostKeys []ssh.Signer, certFiles []string) map[string]*ssh.Certificate {
68+
keyToCertMap := map[string]*ssh.Certificate{}
69+
hostKeyIndex := make(map[string]int)
70+
71+
for index, hostKey := range hostKeys {
72+
hostKeyIndex[string(hostKey.PublicKey().Marshal())] = index
73+
}
74+
75+
for _, filename := range certFiles {
76+
keyRaw, err := os.ReadFile(filename)
77+
if err != nil {
78+
log.WithError(err).WithFields(log.Fields{"filename": filename}).Warn("failed to read host certificate")
79+
continue
80+
}
81+
publicKey, _, _, _, err := ssh.ParseAuthorizedKey(keyRaw)
82+
if err != nil {
83+
log.WithError(err).WithFields(log.Fields{"filename": filename}).Warn("failed to parse host certificate")
84+
continue
85+
}
86+
87+
cert, ok := publicKey.(*ssh.Certificate)
88+
if !ok {
89+
log.WithFields(log.Fields{"filename": filename}).Warn("failed to decode host certificate")
90+
continue
91+
}
92+
93+
hostRawKey := string(cert.Key.Marshal())
94+
index, found := hostKeyIndex[hostRawKey]
95+
if found {
96+
keyToCertMap[hostRawKey] = cert
97+
98+
certSigner, err := ssh.NewCertSigner(cert, hostKeys[index])
99+
if err != nil {
100+
log.WithError(err).WithFields(log.Fields{"filename": filename}).Warn("the host certificate doesn't match the host private key")
101+
continue
102+
}
103+
104+
hostKeys[index] = certSigner
105+
} else {
106+
log.WithFields(log.Fields{"filename": filename}).Warnf("no matching private key for certificate %s", filename)
107+
}
108+
}
109+
110+
return keyToCertMap
111+
}
112+
113+
func newServerConfig(cfg *config.Config) (*serverConfig, error) {
114+
authorizedKeysClient, err := authorizedkeys.NewClient(cfg)
115+
if err != nil {
116+
return nil, fmt.Errorf("failed to initialize GitLab client: %w", err)
117+
}
118+
119+
hostKeys := parseHostKeys(cfg.Server.HostKeyFiles)
66120
if len(hostKeys) == 0 {
67121
return nil, fmt.Errorf("No host keys could be loaded, aborting")
68122
}
69123

70-
return &serverConfig{cfg: cfg, authorizedKeysClient: authorizedKeysClient, hostKeys: hostKeys}, nil
124+
hostKeyToCertMap := parseHostCerts(hostKeys, cfg.Server.HostCertFiles)
125+
126+
return &serverConfig{cfg: cfg, authorizedKeysClient: authorizedKeysClient, hostKeys: hostKeys, hostKeyToCertMap: hostKeyToCertMap}, nil
71127
}
72128

73129
func (s *serverConfig) getAuthKey(ctx context.Context, user string, key ssh.PublicKey) (*authorizedkeys.Response, error) {

internal/sshd/server_config_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"crypto/dsa"
66
"crypto/rand"
77
"crypto/rsa"
8+
"os"
89
"path"
910
"testing"
1011

@@ -22,6 +23,45 @@ func TestNewServerConfigWithoutHosts(t *testing.T) {
2223
require.Equal(t, "No host keys could be loaded, aborting", err.Error())
2324
}
2425

26+
func TestHostKeyAndCerts(t *testing.T) {
27+
testhelper.PrepareTestRootDir(t)
28+
29+
srvCfg := config.ServerConfig{
30+
Listen: "127.0.0.1",
31+
ConcurrentSessionsLimit: 1,
32+
HostKeyFiles: []string{
33+
path.Join(testhelper.TestRoot, "certs/valid/server.key"),
34+
},
35+
HostCertFiles: []string{
36+
path.Join(testhelper.TestRoot, "certs/valid/server-cert.pub"),
37+
path.Join(testhelper.TestRoot, "certs/valid/server2-cert.pub"),
38+
path.Join(testhelper.TestRoot, "certs/invalid/server-cert.pub"),
39+
path.Join(testhelper.TestRoot, "certs/invalid-path.key"),
40+
path.Join(testhelper.TestRoot, "certs/invalid/server.crt"),
41+
},
42+
}
43+
44+
cfg, err := newServerConfig(
45+
&config.Config{GitlabUrl: "http://localhost", User: "user", Server: srvCfg},
46+
)
47+
require.NoError(t, err)
48+
49+
require.Len(t, cfg.hostKeys, 1)
50+
require.Len(t, cfg.hostKeyToCertMap, 1)
51+
52+
// Check that the entry is pointing to the server's public key
53+
data, err := os.ReadFile(path.Join(testhelper.TestRoot, "certs/valid/server.pub"))
54+
require.NoError(t, err)
55+
56+
publicKey, _, _, _, err := ssh.ParseAuthorizedKey(data)
57+
require.NoError(t, err)
58+
require.NotNil(t, publicKey)
59+
cert, ok := cfg.hostKeyToCertMap[string(publicKey.Marshal())]
60+
require.True(t, ok)
61+
require.NotNil(t, cert)
62+
require.Equal(t, cert, cfg.hostKeys[0].PublicKey())
63+
}
64+
2565
func TestFailedAuthorizedKeysClient(t *testing.T) {
2666
_, err := newServerConfig(&config.Config{GitlabUrl: "ftp://localhost"})
2767

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[email protected] AAAAB3NzaC1yc2EAAAADAQABAAABAQCa17cb94P6q5qbDIWX7aMSjyeBIBPQVZ5jlkDBG90XgWC1MEu9sB1OfKLukcx6wJJSTLFccc9rMzhINXq6K7ks0oXSLP81jvqsu0WipIZSDKBNkdVtno1FcI1RnQ+yUP3nA4Ja9L233GA1evLrqTz6Z9k2ET5wVB+s7+k3lak24bJZN8qVRDDk1UveahuPe1KMj7DNKls8y9tNCgGJn9UeTLJzXlh2tt4/AUHZ0lvET9eCzKT9PBZJQWcCzqLXHa37jbc0ib2sgNN1bZhgkle/cxRx0MjEmdjRt4Z48wjKaf1khFQm0r9lebAxvna/vT5hNywbru5KbfUJHyM23yql not_a_valid_cert.pub
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
-----BEGIN OPENSSH PRIVATE KEY-----
2+
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
38+
-----END OPENSSH PRIVATE KEY-----
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDchQE3zT8Ekbm631TbfwXAgYB8xlCg5dhsW2JBmj5T5x1sYmUQaBX4hOgY3vxgnKmRsrw2Km0ww6BZnx67oMIkw3GM9vt7ddgauYc96grln2KgRIPDRBKTEMh/AWgtKfHT56JdLXfzKkQ2AtP+E4yX5+augDvhmm+S8ikr5GgCjWqVYJa0Ds3KCRCRZGCecCPiOQMqr1pPWIcZr2dBgu5O1+JfeXfn+g44wkWsODttvkQecGwp7AOgDF2o1vGnhb+xDJQXg1s4UsbNFq8Dp41F87H1Rdx3LS/35SAKa73aduI+Y0Gj3BjcP2mvBcBZ61MzUHdkIOTWGohxdGDqk0E0L17YW5tyGz1NystpmmviSAW9N6QZyh/WbajMlNWVwgmUmnL3manW5fDFM9t9coY+8q7s2uhHZq+QAh/BGPkT/wfCuAMfav/i5E9p1ELH2GbfwcjWfpLhiuuyYv198tbpIJ8Qbpzv5RENCvXxWr1jLEya7T0QrtWUMNfQkzRRqfk= [email protected]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgNGUtD+qw0Xj5NU2uj4+4LoCWPcvXP54F9Adw/hWN5LAAAAADAQABAAABAQCa17cb94P6q5qbDIWX7aMSjyeBIBPQVZ5jlkDBG90XgWC1MEu9sB1OfKLukcx6wJJSTLFccc9rMzhINXq6K7ks0oXSLP81jvqsu0WipIZSDKBNkdVtno1FcI1RnQ+yUP3nA4Ja9L233GA1evLrqTz6Z9k2ET5wVB+s7+k3lak24bJZN8qVRDDk1UveahuPe1KMj7DNKls8y9tNCgGJn9UeTLJzXlh2tt4/AUHZ0lvET9eCzKT9PBZJQWcCzqLXHa37jbc0ib2sgNN1bZhgkle/cxRx0MjEmdjRt4Z48wjKaf1khFQm0r9lebAxvna/vT5hNywbru5KbfUJHyM23yqlAAAAAAAAAAAAAAACAAAABnNlcnZlcgAAAAAAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAABlwAAAAdzc2gtcnNhAAAAAwEAAQAAAYEA3IUBN80/BJG5ut9U238FwIGAfMZQoOXYbFtiQZo+U+cdbGJlEGgV+IToGN78YJypkbK8NiptMMOgWZ8eu6DCJMNxjPb7e3XYGrmHPeoK5Z9ioESDw0QSkxDIfwFoLSnx0+eiXS138ypENgLT/hOMl+fmroA74ZpvkvIpK+RoAo1qlWCWtA7NygkQkWRgnnAj4jkDKq9aT1iHGa9nQYLuTtfiX3l35/oOOMJFrDg7bb5EHnBsKewDoAxdqNbxp4W/sQyUF4NbOFLGzRavA6eNRfOx9UXcdy0v9+UgCmu92nbiPmNBo9wY3D9prwXAWetTM1B3ZCDk1hqIcXRg6pNBNC9e2Fubchs9TcrLaZpr4kgFvTekGcof1m2ozJTVlcIJlJpy95mp1uXwxTPbfXKGPvKu7NroR2avkAIfwRj5E/8HwrgDH2r/4uRPadRCx9hm38HI1n6S4YrrsmL9ffLW6SCfEG6c7+URDQr18Vq9YyxMmu09EK7VlDDX0JM0Uan5AAABlAAAAAxyc2Etc2hhMi01MTIAAAGAapK5VzLDoRe88tnLORrH8VxLegTWPKGdn0k5Ye8tS5XUgd0N98gU669y4ErDNf0kPxlz40bjsfisEEtJ/N7m14IskCepScfZRh8w6QgPxbTOhmrc89xooqBUE50y5FU9hIIbzUrnEP+Dfu4IiFPToAguCa+KoAKiOX7lBQqEugV6uOWVZ2erPopEv+OiMLD8hXsuKKjQ+TomHZ/IjuXsFdXH7Vcl0VsPaAcxyCtDU7nCTJSTUoUZtMtwwpulp0e/zNmFrEn2Binz4jRaUlk3FM2fdbotviDQYeOY3npmxaWUvvQ/eKn0/DzUTAKAGr2LDa2XWnPhj51BS1XkNaUlnupdYmZ2Sok0R4U3bfVwokteREvAltGbXQSDtZwLS5NEY6vIdDrxpn5QRf9vGjqnc7piXxye9gcLne4YDUi24IhGyHrnWKCC0HjF7tuUhCOVKrqRdmHxRGWX3PlS8Xn6HHEPWU+YZnfT1V2W7LAFcDMozQbs4GPGzZdR3f3vCOJ8 server.pub
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCa17cb94P6q5qbDIWX7aMSjyeBIBPQVZ5jlkDBG90XgWC1MEu9sB1OfKLukcx6wJJSTLFccc9rMzhINXq6K7ks0oXSLP81jvqsu0WipIZSDKBNkdVtno1FcI1RnQ+yUP3nA4Ja9L233GA1evLrqTz6Z9k2ET5wVB+s7+k3lak24bJZN8qVRDDk1UveahuPe1KMj7DNKls8y9tNCgGJn9UeTLJzXlh2tt4/AUHZ0lvET9eCzKT9PBZJQWcCzqLXHa37jbc0ib2sgNN1bZhgkle/cxRx0MjEmdjRt4Z48wjKaf1khFQm0r9lebAxvna/vT5hNywbru5KbfUJHyM23yql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[email protected] AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAg3XO30wn4+lFxc5fLsanH4Pvu0OuKGHoR94zZjxx7qAEAAAADAQABAAABgQDK4MvpmiZ0cLS4+p3YEwcCwo4kVbJPTEeiIMuoEI7KJ3yqAjKJuns6VpnC6aRgu3LmuM4uBHcimQi415ClBOm4+tFiVsVNcAksA+QuU8rTEC6xPs96y1Y/qC+/WQn6+uKp1+fAspWsLpig3VXSTfq+YAcxZfTdO/6ck0kHVvxX096Ye7D0mdq2lWbGwSBlAbzU1wX/Znv5hLZD4DSG1cIjA9vQ/fJ9pwclZiS0qQ1VXdoIUAvL9tTAKj295VGT2NMGGYZQAQ0vXtM1YHOMAZ4XoPL1oVFjVEZoLRD58a6Dpe4hY8QKe6X1w7zU1rr5vVYrz7MTUa5pzsUSOeUTc3kyKJrExVkoLyBgYQq8qW9kYk/Ox9zHwTAKw9vwiIs2Mwe6nlxJ7cw4zykMsxPK4z9HkbZoTFWy3phuPFqs/WQoCvHTKNjWPab7UAameM6Dn0N83BF21tCTMWjkRCvtZVGIGZ7Y4cAiiN0OPzQWDasJ/IKVDRguZJRn3kgHCMYCgokAAAAAAAAAAAAAAAIAAAAHc2VydmVyMgAAAAAAAAAAAAAAAP//////////AAAAAAAAAAAAAAAAAAABlwAAAAdzc2gtcnNhAAAAAwEAAQAAAYEA3IUBN80/BJG5ut9U238FwIGAfMZQoOXYbFtiQZo+U+cdbGJlEGgV+IToGN78YJypkbK8NiptMMOgWZ8eu6DCJMNxjPb7e3XYGrmHPeoK5Z9ioESDw0QSkxDIfwFoLSnx0+eiXS138ypENgLT/hOMl+fmroA74ZpvkvIpK+RoAo1qlWCWtA7NygkQkWRgnnAj4jkDKq9aT1iHGa9nQYLuTtfiX3l35/oOOMJFrDg7bb5EHnBsKewDoAxdqNbxp4W/sQyUF4NbOFLGzRavA6eNRfOx9UXcdy0v9+UgCmu92nbiPmNBo9wY3D9prwXAWetTM1B3ZCDk1hqIcXRg6pNBNC9e2Fubchs9TcrLaZpr4kgFvTekGcof1m2ozJTVlcIJlJpy95mp1uXwxTPbfXKGPvKu7NroR2avkAIfwRj5E/8HwrgDH2r/4uRPadRCx9hm38HI1n6S4YrrsmL9ffLW6SCfEG6c7+URDQr18Vq9YyxMmu09EK7VlDDX0JM0Uan5AAABlAAAAAxyc2Etc2hhMi01MTIAAAGANOKG8Tq7kp9B5+CQEyb+mEatJOoQRV+4rpemWlfEw6TuVwQN2wSXc6XKBHzSG4NRnFkwk6GgiPLQEf4lBBKA8VYQnDKuhrHJlU4DFCRPw/aceHfCwNOruyJmuf91W3yEO/kYAd6EhkQiW/K3ky7BuXCqR34T2fBZSCeYhNcXWxhEMLoAuj0kEdX+YMNBmiPtinPE13KMFGyIVBm/ojgSZa8j4WnhDcK0cWv0OSGTgJF6q3hENCWRz2E1HroKUiABOy5Nca6gPVAi4OTd7gwER8eh9MngVHYorAJ3N9HjUh640SbL3zCC8f/lqIztqsHY0u3olsQ0gLXpFain+430HeyJlmVlsDZgQKRb90Mm1viSCKvHGpmVDYMimE9y0DCQS1i0yRGF1uSIPtuQ0NCbhS/HPKsT3nYgGCEuoB8aGOu3aGB/tmUkYXW+pwXRKqw0f/zX088XWYWvA+AR4hmmr6DDMnf/4EHgJp3xHTEwOBHCVj69xvlOawBNlL2X0b2p server2.pub

0 commit comments

Comments
 (0)