Skip to content

Commit

Permalink
Provision ssh keys for srlinux via both authorized_keys file and conf…
Browse files Browse the repository at this point in the history
…ig (#1504)

* add srl linuxadmin authorized keys via config

* update

* dedup sshkeys

* store ssh pub keys in CLab

* renamed func

* use map for keys dedup, drop extra ssh type

* extract keys once and use in authz file creation

also do not fail when keys extraction fails

* store SrlVersion in the node

* renamed pointer receiver s->n for unification

* store ssh keys in srl struct, retrieve srl version early and catenate keys

* catenate without extra space after the last key

* added a test for FileLines

* added key filtering

and stopped using pointers to interfaces

* removed unused context in banner and added debug logs

* blind eye for unsafe defer

---------

Co-authored-by: Roman Dodin <dodin.roman@gmail.com>
  • Loading branch information
steiler and hellt authored Aug 9, 2023
1 parent 28cfadb commit 635aecf
Show file tree
Hide file tree
Showing 16 changed files with 387 additions and 65 deletions.
95 changes: 63 additions & 32 deletions clab/authz_keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package clab

import (
"bytes"
"errors"
"fmt"
"net"
"os"
Expand All @@ -14,6 +15,7 @@ import (

log "github.com/sirupsen/logrus"
"github.com/srl-labs/containerlab/utils"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)

Expand All @@ -23,69 +25,91 @@ const (
authzKeysFPath = "~/.ssh/authorized_keys"
)

// CreateAuthzKeysFile creats the authorized_keys file in the lab directory
// if any files ~/.ssh/*.pub found.
// CreateAuthzKeysFile creates the authorized_keys file in the lab directory
// using the public ssh keys retrieved from agent and local files.
func (c *CLab) CreateAuthzKeysFile() error {
b := new(bytes.Buffer)

for _, k := range c.SSHPubKeys {
x := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(k)))
addKeyToBuffer(b, x)
}

clabAuthzKeysFPath := c.TopoPaths.AuthorizedKeysFilename()
if err := utils.CreateFile(clabAuthzKeysFPath, b.String()); err != nil {
return err
}

// ensure authz_keys will have the permissions allowing it to be read by anyone
return os.Chmod(clabAuthzKeysFPath, 0644) // skipcq: GSC-G302
}

// RetrieveSSHPubKeysFromFiles retrieves public keys from the ~/.ssh/*.authorized_keys
// and ~/.ssh/*.pub files.
func RetrieveSSHPubKeysFromFiles() ([]ssh.PublicKey, error) {
var keys []ssh.PublicKey
p := utils.ResolvePath(pubKeysGlob, "")

all, err := filepath.Glob(p)
if err != nil {
return fmt.Errorf("failed globbing the path %s", p)
return nil, fmt.Errorf("failed globbing the path %s", p)
}

f := utils.ResolvePath(authzKeysFPath, "")

if utils.FileExists(f) {
log.Debugf("%s found, adding the public keys it contains", f)
log.Debugf("%s found, adding it to the list of files to get public keys from", f)
all = append(all, f)
}

// get keys registered with ssh-agent
keys, err := SSHAgentKeys()
keys, err = utils.LoadSSHPubKeysFromFiles(all)
if err != nil {
log.Debug(err)
return nil, err
}

log.Debugf("extracted %d keys from ssh-agent", len(keys))
for _, k := range keys {
addKeyToBuffer(b, k)
return keys, nil
}

// RetrieveSSHPubKeys retrieves the PubKeys from the different sources
// SSHAgent as well as all home dir based /.ssh/*.pub files.
func (c *CLab) RetrieveSSHPubKeys() ([]ssh.PublicKey, error) {
keys := make([]ssh.PublicKey, 0)

var errs error

// any errors encountered during the retrieval of the keys are not fatal
// we accumulate them and log.
fkeys, err := RetrieveSSHPubKeysFromFiles()
if err != nil {
errs = errors.Join(err)
}

for _, fn := range all {
rb, err := os.ReadFile(fn)
if err != nil {
return fmt.Errorf("failed reading the file %s: %v", fn, err)
}
agentKeys, err := RetrieveSSHAgentKeys()
if err != nil {
errs = errors.Join(err)
}

addKeyToBuffer(b, string(rb))
keysM := map[string]ssh.PublicKey{}
for _, k := range append(fkeys, agentKeys...) {
keysM[string(ssh.MarshalAuthorizedKey(k))] = k
}

clabAuthzKeysFPath := c.TopoPaths.AuthorizedKeysFilename()
if err := utils.CreateFile(clabAuthzKeysFPath, b.String()); err != nil {
return err
for _, k := range keysM {
keys = append(keys, k)
}

// ensure authz_keys will have the permissions allowing it to be read by anyone
return os.Chmod(clabAuthzKeysFPath, 0644) // skipcq: GSC-G302
return keys, errs
}

// addKeyToBuffer adds a key to the buffer if the key is not already present.
func addKeyToBuffer(b *bytes.Buffer, key string) {
// since they key might have a comment as a third field, we need to strip it
elems := strings.Fields(key)
if len(elems) < 2 {
return
}

if !strings.Contains(b.String(), elems[1]) {
if !strings.Contains(b.String(), key) {
b.WriteString(key + "\n")
}
}

// SSHAgentKeys retrieves public keys registered with the ssh-agent.
func SSHAgentKeys() ([]string, error) {
// RetrieveSSHAgentKeys retrieves public keys registered with the ssh-agent.
func RetrieveSSHAgentKeys() ([]ssh.PublicKey, error) {
socket := os.Getenv("SSH_AUTH_SOCK")
if len(socket) == 0 {
return nil, fmt.Errorf("SSH_AUTH_SOCK not set, skipping pubkey fetching")
Expand All @@ -101,9 +125,16 @@ func SSHAgentKeys() ([]string, error) {
return nil, fmt.Errorf("error listing agent's pub keys %w", err)
}

var pubKeys []string
log.Debugf("extracted %d keys from ssh-agent", len(keys))

var pubKeys []ssh.PublicKey

for _, key := range keys {
pubKeys = append(pubKeys, key.String())
pkey, err := ssh.ParsePublicKey(key.Blob)
if err != nil {
return nil, err
}
pubKeys = append(pubKeys, pkey)
}

return pubKeys, nil
Expand Down
6 changes: 6 additions & 0 deletions clab/clab.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/srl-labs/containerlab/runtime/docker"
"github.com/srl-labs/containerlab/runtime/ignite"
"github.com/srl-labs/containerlab/types"
"golang.org/x/crypto/ssh"
"golang.org/x/exp/slices"
"golang.org/x/sync/semaphore"
)
Expand All @@ -38,6 +39,10 @@ type CLab struct {
// reg is a registry of node kinds
Reg *nodes.NodeRegistry
Cert *cert.Cert
// List of SSH public keys extracted from the ~/.ssh/authorized_keys file
// and ~/.ssh/*.pub files.
// The keys are used to enable key-based SSH access for the nodes.
SSHPubKeys []ssh.PublicKey

timeout time.Duration
}
Expand Down Expand Up @@ -404,6 +409,7 @@ func (c *CLab) scheduleNodes(ctx context.Context, maxWorkers int,
Cert: c.Cert,
TopologyName: c.Config.Name,
TopoPaths: c.TopoPaths,
SSHPubKeys: c.SSHPubKeys,
},
)
if err != nil {
Expand Down
5 changes: 5 additions & 0 deletions cmd/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,11 @@ func deployFn(_ *cobra.Command, _ []string) error {
return err
}

c.SSHPubKeys, err = c.RetrieveSSHPubKeys()
if err != nil {
log.Warn(err)
}

if err := c.CreateAuthzKeysFile(); err != nil {
return err
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ require (
go4.org/intern v0.0.0-20230205224052-192e9f60865c // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e // indirect
gocloud.dev v0.29.0 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/mod v0.10.0
golang.org/x/net v0.14.0
golang.org/x/oauth2 v0.9.0 // indirect
golang.org/x/sync v0.3.0
Expand Down
2 changes: 2 additions & 0 deletions nodes/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/srl-labs/containerlab/clab/exec"
"github.com/srl-labs/containerlab/runtime"
"github.com/srl-labs/containerlab/types"
"golang.org/x/crypto/ssh"
)

const (
Expand Down Expand Up @@ -50,6 +51,7 @@ type PreDeployParams struct {
Cert *cert.Cert
TopologyName string
TopoPaths *types.TopoPaths
SSHPubKeys []ssh.PublicKey
}

// DeployParams contains parameters for the Deploy function.
Expand Down
27 changes: 6 additions & 21 deletions nodes/srl/banner.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package srl

import (
"context"
"fmt"

log "github.com/sirupsen/logrus"
"github.com/srl-labs/containerlab/clab/exec"
)

const banner = `................................................................
Expand All @@ -26,28 +22,17 @@ const banner = `................................................................
`

// banner returns a banner string with a docs version filled in based on the version information queried from the node.
func (s *srl) banner(ctx context.Context) (string, error) {
cmd, _ := exec.NewExecCmdFromString(`sr_cli -d "info from state /system information version | grep version"`)

execResult, err := s.RunExec(ctx, cmd)
if err != nil {
return "", err
}

log.Debugf("node %s. stdout: %s, stderr: %s", s.Cfg.ShortName, execResult.GetStdOutString(), execResult.GetStdErrString())

v := s.parseVersionString(execResult.GetStdOutString())

func (n *srl) banner() (string, error) {
// if minor is a single digit value, we need to add extra space to patch version
// to have banner table aligned nicely
if len(v.minor) == 1 {
v.patch = v.patch + " "
if len(n.swVersion.minor) == 1 {
n.swVersion.patch = n.swVersion.patch + " "
}

b := fmt.Sprintf(banner,
v.major, v.minor,
v.major, v.minor, v.patch,
v.major, v.minor, v.patch)
n.swVersion.major, n.swVersion.minor,
n.swVersion.major, n.swVersion.minor, n.swVersion.patch,
n.swVersion.major, n.swVersion.minor, n.swVersion.patch)

return b, nil
}
44 changes: 36 additions & 8 deletions nodes/srl/srl.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import (
"github.com/hairyhenderson/gomplate/v3/data"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/ssh"
"golang.org/x/mod/semver"

"github.com/srl-labs/containerlab/cert"
"github.com/srl-labs/containerlab/clab/exec"
Expand Down Expand Up @@ -65,6 +67,10 @@ set / interface ethernet-{{index $parts 0}}/{{index $parts 1}} breakout-mode num
set / interface ethernet-{{index $parts 0}}/{{index $parts 1}}/{{index $parts 2}} admin-state enable
{{- end }}
{{ end -}}
{{- if .SSHPubKeys }}
set / system aaa authentication linuxadmin-user ssh-key [ {{ .SSHPubKeys }} ]
set / system aaa authentication admin-user ssh-key [ {{ .SSHPubKeys }} ]
{{- end }}
set / system banner login-banner "{{ .Banner }}"
commit save`
)
Expand Down Expand Up @@ -137,6 +143,10 @@ type srl struct {
// to generate certificates
cert *cert.Cert
topologyName string
// SSH public keys extracted from the clab host
sshPubKeys []ssh.PublicKey
// software version SR Linux node runs
swVersion *SrlVersion
}

func (s *srl) Init(cfg *types.NodeConfig, opts ...nodes.NodeOption) error {
Expand Down Expand Up @@ -247,6 +257,9 @@ func (s *srl) PreDeploy(_ context.Context, params *nodes.PreDeployParams) error
)
}

// store provided pubkeys
s.sshPubKeys = params.SSHPubKeys

// store the certificate-related parameters
// for cert generation to happen in Post-Deploy phase with mgmt IPs as SANs
s.cert = params.Cert
Expand Down Expand Up @@ -285,6 +298,11 @@ func (s *srl) PostDeploy(ctx context.Context, params *nodes.PostDeployParams) er
return err
}

s.swVersion, err = s.RunningVersion(ctx)
if err != nil {
return err
}

// return if config file is found in the lab directory.
// This can be either if the startup-config has been mounted by that path
// or the config has been previously generated and saved
Expand Down Expand Up @@ -505,19 +523,29 @@ func generateSRLTopologyFile(cfg *types.NodeConfig) error {
}

// addDefaultConfig adds srl default configuration such as tls certs, gnmi/json-rpc, login-banner.
func (s *srl) addDefaultConfig(ctx context.Context) error {
b, err := s.banner(ctx)
func (n *srl) addDefaultConfig(ctx context.Context) error {
b, err := n.banner()
if err != nil {
return err
}

// struct that holds data used in templating of the default config snippet
tplData := struct {
*types.NodeConfig
Banner string
Banner string
SSHPubKeys string
}{
s.Cfg,
n.Cfg,
b,
"",
}

n.filterSSHPubKeys()

// in srlinux >= v23.7+ linuxadmin and admin user ssh keys can only be configured via the cli
// so we add the keys to the template data for rendering.
if semver.Compare(n.swVersion.String(), "v23.7") >= 0 || n.swVersion.major == "0" {
tplData.SSHPubKeys = catenateKeys(n.sshPubKeys)
}

// remove newlines from tls key/cert so that they nicely apply via the cli provisioning
Expand All @@ -531,13 +559,13 @@ func (s *srl) addDefaultConfig(ctx context.Context) error {
return err
}

log.Debugf("Node %q additional config:\n%s", s.Cfg.ShortName, buf.String())
log.Debugf("Node %q additional config:\n%s", n.Cfg.ShortName, buf.String())

execCmd := exec.NewExecCmdFromSlice([]string{
"bash", "-c",
fmt.Sprintf("echo '%s' > /tmp/clab-config", buf.String()),
})
_, err = s.RunExec(ctx, execCmd)
_, err = n.RunExec(ctx, execCmd)
if err != nil {
return err
}
Expand All @@ -547,12 +575,12 @@ func (s *srl) addDefaultConfig(ctx context.Context) error {
return err
}

execResult, err := s.RunExec(ctx, cmd)
execResult, err := n.RunExec(ctx, cmd)
if err != nil {
return err
}

log.Debugf("node %s. stdout: %s, stderr: %s", s.Cfg.ShortName, execResult.GetStdOutString(), execResult.GetStdErrString())
log.Debugf("node %s. stdout: %s, stderr: %s", n.Cfg.ShortName, execResult.GetStdOutString(), execResult.GetStdErrString())

return nil
}
Expand Down
Loading

0 comments on commit 635aecf

Please sign in to comment.