Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adjusting ssh key indexes to start from max (32) to go backwards #1724

Merged
merged 4 commits into from
Nov 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/manual/kinds/vr-sros.md
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,9 @@ By combining file bindings and the automatic script execution of SROS it is poss

#### SSH keys

Containerlab v0.48.0+ supports SSH key injection into the Nokia SR OS nodes. First containerlab retrieves all public keys from `~/.ssh` directory of a user running containerlab, then it retrieves public keys from the ssh agent if one is running.
Containerlab v0.48.0+ supports SSH key injection into the Nokia SR OS nodes. First containerlab retrieves all public keys from `~/.ssh`[^1] directory and `~/.ssh/authorizde_keys` file, then it retrieves public keys from the ssh agent if one is running.

Next it will filter out public keys that are not of RSA/ECDSA type. The remaining valid public keys will be configured for the admin user of the Nokia SR OS node. This will enable key-based authentication next time you connect to the node.
Next it will filter out public keys that are not of RSA/ECDSA type. The remaining valid public keys will be configured for the admin user of the Nokia SR OS node using key IDs from 32 downwards[^2]. This will enable key-based authentication next time you connect to the node.

### License

Expand All @@ -361,3 +361,6 @@ When a user starts a lab, containerlab creates a node directory for storing [con
The following labs feature Nokia SR OS node:

* [SR Linux and vr-sros](../../lab-examples/vr-sros.md)

[^1]: `~` is the home directory of the user that runs containerlab.
[^2]: If a user wishes to provide a custom startup-config with public keys defined, then they should use key IDs from 1 onwards. This will minimize chances of key ID collision causing containerlab to overwrite user-defined keys.
89 changes: 50 additions & 39 deletions nodes/vr_sros/sshKey.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,52 +18,14 @@ import (
//go:embed ssh_keys.go.tpl
var SROSSSHKeysTemplate string

// mapSSHPubKeys goes over s.sshPubKeys and puts the supported keys to the corresponding
// slices associated with the supported SSH key algorithms.
// supportedSSHKeyAlgos key is a SSH key algorithm and the value is a pointer to the slice
// that is used to store the keys of the corresponding algorithm family.
// Two slices are used to store RSA and ECDSA keys separately.
// The slices are modified in place by reference, so no return values are needed.
func (s *vrSROS) mapSSHPubKeys(supportedSSHKeyAlgos map[string]*[]string) {
for _, k := range s.sshPubKeys {
sshKeys, ok := supportedSSHKeyAlgos[k.Type()]
if !ok {
log.Debugf("unsupported SSH Key Algo %q, skipping key", k.Type())
continue
}

// extract the fields
// <keytype> <key> <comment>
keyFields := strings.Fields(string(ssh.MarshalAuthorizedKey(k)))

*sshKeys = append(*sshKeys, keyFields[1])
}
}

// SROSTemplateData holds ssh keys for template generation.
type SROSTemplateData struct {
SSHPubKeysRSA []string
SSHPubKeysECDSA []string
}

// configureSSHPublicKeys cofigures public keys extracted from clab host
// on SR OS node using SSH.
func (s *vrSROS) configureSSHPublicKeys(
ctx context.Context, addr, platformName,
username, password string, pubKeys []ssh.PublicKey) error {
tplData := SROSTemplateData{}

// a map of supported SSH key algorithms and the template slices
// the keys should be added to.
// In mapSSHPubKeys we map supported SSH key algorithms to the template slices.
supportedSSHKeyAlgos := map[string]*[]string{
ssh.KeyAlgoRSA: &tplData.SSHPubKeysRSA,
ssh.KeyAlgoECDSA521: &tplData.SSHPubKeysECDSA,
ssh.KeyAlgoECDSA384: &tplData.SSHPubKeysECDSA,
ssh.KeyAlgoECDSA256: &tplData.SSHPubKeysECDSA,
}

s.mapSSHPubKeys(supportedSSHKeyAlgos)
s.prepareSSHPubKeys(&tplData)

t, err := template.New("SSHKeys").Funcs(
gomplate.CreateFuncs(context.Background(), new(data.Data))).Parse(SROSSSHKeysTemplate)
Expand All @@ -84,3 +46,52 @@ func (s *vrSROS) configureSSHPublicKeys(

return err
}

// prepareSSHPubKeys maps the ssh pub keys into the SSH key type based slice
// and checks that not more then 32 keys per type are present, otherwise truncates
// the slices since SROS allows a max of 32 public keys per algorithm.
func (s *vrSROS) prepareSSHPubKeys(tplData *SROSTemplateData) {
// a map of supported SSH key algorithms and the template slices
// the keys should be added to.
// In mapSSHPubKeys we map supported SSH key algorithms to the template slices.
supportedSSHKeyAlgos := map[string]*[]string{
ssh.KeyAlgoRSA: &tplData.SSHPubKeysRSA,
ssh.KeyAlgoECDSA521: &tplData.SSHPubKeysECDSA,
ssh.KeyAlgoECDSA384: &tplData.SSHPubKeysECDSA,
ssh.KeyAlgoECDSA256: &tplData.SSHPubKeysECDSA,
}

s.mapSSHPubKeys(supportedSSHKeyAlgos)

if len(tplData.SSHPubKeysRSA) > 32 {
log.Warnf("more then 32 public RSA ssh keys found on the system. Selecting first 32 keys since SROS supports max. 32 per key type")
tplData.SSHPubKeysRSA = tplData.SSHPubKeysRSA[:32]
}

if len(tplData.SSHPubKeysECDSA) > 32 {
log.Warnf("more then 32 public RSA ssh keys found on the system. Selecting first 32 keys since SROS supports max. 32 per key type")
tplData.SSHPubKeysECDSA = tplData.SSHPubKeysECDSA[:32]
}
}

// mapSSHPubKeys goes over s.sshPubKeys and puts the supported keys to the corresponding
// slices associated with the supported SSH key algorithms.
// supportedSSHKeyAlgos key is a SSH key algorithm and the value is a pointer to the slice
// that is used to store the keys of the corresponding algorithm family.
// Two slices are used to store RSA and ECDSA keys separately.
// The slices are modified in place by reference, so no return values are needed.
func (s *vrSROS) mapSSHPubKeys(supportedSSHKeyAlgos map[string]*[]string) {
for _, k := range s.sshPubKeys {
sshKeys, ok := supportedSSHKeyAlgos[k.Type()]
if !ok {
log.Debugf("unsupported SSH Key Algo %q, skipping key", k.Type())
continue
}

// extract the fields
// <keytype> <key> <comment>
keyFields := strings.Fields(string(ssh.MarshalAuthorizedKey(k)))

*sshKeys = append(*sshKeys, keyFields[1])
}
}
86 changes: 86 additions & 0 deletions nodes/vr_sros/sshKey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package vr_sros

import (
"testing"

"golang.org/x/crypto/ssh"
)

func Test_vrSROS_mapSSHPubKeys(t *testing.T) {
tests := []struct {
name string
keysAsStr [][]byte
expectedRSA int
expectedECDSA int
}{
{
name: "Test with 3 rsa and 4 ecdsa keys",
keysAsStr: [][]byte{
[]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDhR6+tNYAAP0i40O5INIrSGMuhQH2R6nsH0ZfPlMScmvORGgGPyFFHg76MiVIudEnwDKaytkZZmOJM1aWvF94WZJQDhzR6Uz9W8k14cRDPhOrLXjTbu2LKWRJtJpvcVB/gjhMoOuzGID/lBzpzC4bKLVYBze7Ek3XrTjf6xlB3I4G3GVCJHO6gDfpQqSPucXiXtO2gMhk1sKDVDD1N/31VSxVwHDB1BHUREM/rbmGHSSJaEjAByxYIL80kLQRTejnL3NMXGbuVisO6OL4H2h9gdTNppjQjSUCKcn/IFOx7LuYO64xGm3ThfBKRWVQNul2Ab/pL4/BqD4ziZ3Wad94Ob7kClKeY9rO9I1wfSckDaF1CvabeyM63ekNF8xOViDOfBJGK+eOWsN5nKVcD2lSzQZAolDGSYQPcSeZ82/oqcHrG/kUcVZxuU5Lkc8QpDYlW0EorrUIvgM0Ta7GrmcxAU1WxUD9W6GzNbrCm71eP4Gu9ZD+U6p0+r4XC0bGUThE="),
[]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC1XraioeGL9OVkGVu9yMLVkVWMYrp2yP377HoKqA63VuDuJMDHVKT/BDINh/NUiorS1jOwu3cE66ZvU8w2hC9gkIdCUZ6ZTsUP2Zb1bk1V/kNbjd77Ra9iSssK/o2SBzeOZ6k0xbz1Yp9cXbTNh2s5g5/E764rd2hdyRWYagC1re7hrTBuxEXEJC4+1iUsp2gC1ZxNG2ol1UOk1e7sHV/eiw9VyIDyNiU0d8Dul4a5/Fp4/vBiEI6BvVegeQTgJd29Guf2Tl7oOwFiQpdDevnN/CcsCuEF5zcdywIujaLwwEIjI7rf6210DXQdxNlruI1Oq8koIn/WAhH/geeRDA7mSIRxYF2Uqb+VErNF2iTq7P6/609bcXow8S5TfB8PIQH/E9gItTg2XlgAwAB/HYlUUUwrjhNYUtYvQq3a+EjdJFKTJdHQp08xVb1f4aD+61NSROwtwHm+NHttdSIo9nqqSpdm0KxlS9t6iVVJALqy8oiUEBWJDVilRB7RLyHt0sk="),
[]byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDXvF3x+IWYfy957yhPqN+kfO/vCXitJ4jAZJVp28P5yw43TKHFqmoecITEtS4i8Rrt+w+L6I7t4XKRptEeISLdOzxkMgVfutShqBrxMkWSmwRd2vFABT5In1gsU666cahAF2n7kNC2X2OBw8oANcsk+nsPGZzOfFyCPB1Vc8z/66UJAbe3bwSjnwzUAShXkPpMmaGhm7+LY9foH4aW0ho4vlGXnZ7M1WDlnBC+9ae8MUP5eAgNw+q0MPlKmzDXDoxQTKEtY1Mu66oacUmIsqxRFajrVCplc2O9MGhcNw8Z+TKkrP7cJxgX0kMh92m2LVV3TLMcI513aBhvDzwHdTH/Tqn/qpa2Jpqovg9RpJBCIjX8tN6j8TKsdfDnqadW8HL89Lrlv36lk6DOD872z3zxajH9FaJgVjQjxEVYafZDqgHHNnNLp05IBwuExV2q93ML7WdhWemSgIVV2GF7cgEcnHWm+Q7JKCJTMEUBimshJEaMm6ROcgfDO4KFpMYpJAc="),
[]byte("ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBBiXo8DavZ3rzNfynQCr5WJia1S5VT2Jx91OEjqeipcwzpqqeSciaIIwGt78Oq7PCNNrBoYDns+rjtzKq4ViFhk="),
[]byte("ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItbmlzdHAzODQAAAAIbmlzdHAzODQAAABhBDi9AlTU5OMFKIjLSCQMCwH+IQtv82sfU2BDsSLdFeM7xiZtNSUzoaH+WQA61rwNi4sc3iddQLv54LyxC9Z7auL+ItsoDulQ9TklBq7mlmQfS5yS0LsdSAaK7iGKfiLong=="),
[]byte("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBABZMizDux+9W92bOxwgPVTTau4xLzcUVF+vfkySVaop7cq8YtJ4QbxTbOkawpG8ZC9gCGzhiVqF7aFhoMIF0jpF5AGcBUxm1ahp7uURmI7YXjlnvzHMgrp0ot8sdY8ibjZSGYzK0BuGOsZFq1cFQRJjIWoNTJjqRFqA/gLIWaab2V9nrg=="),
[]byte("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAC+ZByJkRXxaZQQ3NDJRDQudtrV1PguOUJ/43UFMmHr8KfA5+oYG/lQzFgXwXxe4OWefJPjuSEqcNuw3xvb5MTVJwFEyXPRyyjljSUhdocWemAMb9oh8t9B4daPwoe0sVG7m9VH0h4uESFuf/SjHhngZgtuh22j9OpEbMiJjOgGL3JC/w=="),
},
expectedRSA: 3,
expectedECDSA: 4,
},
{
name: "Test without keys",
keysAsStr: [][]byte{},
expectedRSA: 0,
expectedECDSA: 0,
},
{
name: "Test with 35 RSA and 40 ecdsa keys",
keysAsStr: func() [][]byte {
rsaKey := []byte("ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDXvF3x+IWYfy957yhPqN+kfO/vCXitJ4jAZJVp28P5yw43TKHFqmoecITEtS4i8Rrt+w+L6I7t4XKRptEeISLdOzxkMgVfutShqBrxMkWSmwRd2vFABT5In1gsU666cahAF2n7kNC2X2OBw8oANcsk+nsPGZzOfFyCPB1Vc8z/66UJAbe3bwSjnwzUAShXkPpMmaGhm7+LY9foH4aW0ho4vlGXnZ7M1WDlnBC+9ae8MUP5eAgNw+q0MPlKmzDXDoxQTKEtY1Mu66oacUmIsqxRFajrVCplc2O9MGhcNw8Z+TKkrP7cJxgX0kMh92m2LVV3TLMcI513aBhvDzwHdTH/Tqn/qpa2Jpqovg9RpJBCIjX8tN6j8TKsdfDnqadW8HL89Lrlv36lk6DOD872z3zxajH9FaJgVjQjxEVYafZDqgHHNnNLp05IBwuExV2q93ML7WdhWemSgIVV2GF7cgEcnHWm+Q7JKCJTMEUBimshJEaMm6ROcgfDO4KFpMYpJAc=")

ecdsaKey := []byte("ecdsa-sha2-nistp521 AAAAE2VjZHNhLXNoYTItbmlzdHA1MjEAAAAIbmlzdHA1MjEAAACFBAC+ZByJkRXxaZQQ3NDJRDQudtrV1PguOUJ/43UFMmHr8KfA5+oYG/lQzFgXwXxe4OWefJPjuSEqcNuw3xvb5MTVJwFEyXPRyyjljSUhdocWemAMb9oh8t9B4daPwoe0sVG7m9VH0h4uESFuf/SjHhngZgtuh22j9OpEbMiJjOgGL3JC/w==")

keys := make([][]byte, 75)
for i := 0; i < 35; i++ {
keys[i] = rsaKey
}
for i := 35; i < 75; i++ {
keys[i] = ecdsaKey
}

return keys
}(),
expectedRSA: 32, // SROS supports a maximum of 32 keys per key type
expectedECDSA: 32,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keys := make([]ssh.PublicKey, 0, len(tt.keysAsStr))
for _, strKey := range tt.keysAsStr {
key, _, _, _, err := ssh.ParseAuthorizedKey(strKey)
if err != nil {
t.Error(err)
}
keys = append(keys, key)
}

s := &vrSROS{
sshPubKeys: keys,
}

tmplData := &SROSTemplateData{}

s.prepareSSHPubKeys(tmplData)

if len(tmplData.SSHPubKeysRSA) != tt.expectedRSA {
t.Errorf("expected %d RSA keys, got %d", tt.expectedRSA, len(tmplData.SSHPubKeysRSA))
}

if len(tmplData.SSHPubKeysECDSA) != tt.expectedECDSA {
t.Errorf("expected %d ECDSA keys, got %d", tt.expectedECDSA, len(tmplData.SSHPubKeysECDSA))
}
})
}
}
4 changes: 2 additions & 2 deletions nodes/vr_sros/ssh_keys.go.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@
/configure system security user-params attempts count 64

{{ range $index, $key := .SSHPubKeysRSA }}
/configure system security user-params local-user user "admin" public-keys rsa rsa-key {{ add $index 1 }} key-value {{ $key }}
/configure system security user-params local-user user "admin" public-keys rsa rsa-key {{ sub 32 $index }} key-value {{ $key }}
{{ end }}

{{ range $index, $key := .SSHPubKeysECDSA }}
/configure system security user-params local-user user "admin" public-keys ecdsa ecdsa-key {{ add $index 1 }} key-value {{ $key }}
/configure system security user-params local-user user "admin" public-keys ecdsa ecdsa-key {{ sub 32 $index }} key-value {{ $key }}
{{ end }}
9 changes: 8 additions & 1 deletion nodes/vr_sros/vr-sros.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ const (
licenseFName = "license.txt"
)

// SROSTemplateData holds ssh keys for template generation.
type SROSTemplateData struct {
SSHPubKeysRSA []string
SSHPubKeysECDSA []string
}

// Register registers the node in the NodeRegistry.
func Register(r *nodes.NodeRegistry) {
r.Register(kindnames, func() nodes.Node {
Expand Down Expand Up @@ -215,7 +221,8 @@ func (s *vrSROS) isHealthy(ctx context.Context) bool {
}

// applyPartialConfig applies partial configuration to the SR OS.
func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName, username, password string, config io.Reader) error {
func (s *vrSROS) applyPartialConfig(ctx context.Context, addr, platformName,
username, password string, config io.Reader) error { //skipcq: GO-R1005
var err error
var d *network.Driver

Expand Down