Skip to content

Commit 11da9c1

Browse files
authored
Merge pull request #83 from rancher-sandbox/ssh-key
Use user identify from $LIMA_HOME/_config/user
2 parents eb3ef88 + 0dfd10d commit 11da9c1

File tree

6 files changed

+135
-21
lines changed

6 files changed

+135
-21
lines changed

docs/internal.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,19 @@ Note that we intentionally avoid using `~/Library/Application Support/Lima` on m
99
We use `~/.lima` so that we can have enough space for the length of the socket path,
1010
which must be less than 104 characters on macOS.
1111

12+
### Config directory (`${LIMA_HOME}/_config`)
13+
14+
The config directory contains global lima settings that apply to all instances.
15+
16+
User identity:
17+
18+
Lima creates a default identity and uses its public key as the authorized key
19+
to access all lima instances. In addition lima will also configure all public
20+
keys from `~/.ssh/*.pub` as well, so the user can use the ssh endpoint without
21+
having to specify an identity explicitly.
22+
- `user`: private key
23+
- `user.pub`: public key
24+
1225
### Instance directory (`${LIMA_HOME}/<INSTANCE>`)
1326

1427
An instance directory contains the following files:

pkg/cidata/cidata.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,10 @@ func GenerateISO9660(isoPath, name string, y *limayaml.LimaYAML) error {
4141
Containerd: Containerd{System: *y.Containerd.System, User: *y.Containerd.User},
4242
}
4343

44-
pubKeys := sshutil.DefaultPubKeys()
44+
pubKeys, err := sshutil.DefaultPubKeys()
45+
if err != nil {
46+
return err
47+
}
4548
if len(pubKeys) == 0 {
4649
return errors.New("no SSH key was found, run `ssh-keygen`")
4750
}

pkg/sshutil/sshutil.go

Lines changed: 89 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"strings"
1010

1111
"github.com/AkihiroSuda/lima/pkg/osutil"
12+
"github.com/AkihiroSuda/lima/pkg/store"
1213
"github.com/AkihiroSuda/lima/pkg/store/filenames"
1314
"github.com/pkg/errors"
1415
"github.com/sirupsen/logrus"
@@ -19,34 +20,71 @@ type PubKey struct {
1920
Content string
2021
}
2122

22-
// DefaultPubKeys finds ssh public keys from ~/.ssh
23-
func DefaultPubKeys() []PubKey {
23+
func readPublicKey(f string) (PubKey, error) {
24+
entry := PubKey{
25+
Filename: f,
26+
}
27+
content, err := os.ReadFile(f)
28+
if err == nil {
29+
entry.Content = strings.TrimSpace(string(content))
30+
} else {
31+
err = errors.Wrapf(err, "failed to read ssh public key %q", f)
32+
}
33+
return entry, err
34+
}
35+
36+
// DefaultPubKeys returns the public key from $LIMA_HOME/_config/user.pub.
37+
// The key will be created if it does not yet exist. All public keys
38+
// ~/.ssh/*.pub will be appended to make the VM accessible without specifying
39+
// and identity explicitly.
40+
func DefaultPubKeys() ([]PubKey, error) {
41+
// Read $LIMA_HOME/_config/user.pub
42+
configDir, err := store.LimaConfigDir()
43+
if err != nil {
44+
return nil, err
45+
}
46+
_, err = os.Stat(filepath.Join(configDir, filenames.UserPrivateKey))
47+
if err != nil {
48+
if !errors.Is(err, os.ErrNotExist) {
49+
return nil, err
50+
}
51+
if err := os.MkdirAll(configDir, 0700); err != nil {
52+
return nil, errors.Wrapf(err, "could not create %q directory", configDir)
53+
}
54+
keygenCmd := exec.Command("ssh-keygen", "-t", "ed25519", "-q", "-N", "", "-f",
55+
filepath.Join(configDir, filenames.UserPrivateKey))
56+
logrus.Debugf("executing %v", keygenCmd.Args)
57+
if out, err := keygenCmd.CombinedOutput(); err != nil {
58+
return nil, errors.Wrapf(err, "failed to run %v: %q", keygenCmd.Args, string(out))
59+
}
60+
}
61+
entry, err := readPublicKey(filepath.Join(configDir, filenames.UserPublicKey))
62+
if err != nil {
63+
return nil, err
64+
}
65+
res := []PubKey{entry}
66+
67+
// Append all of ~/.ssh/*.pub
2468
homeDir, err := os.UserHomeDir()
2569
if err != nil {
26-
logrus.Warn(err)
27-
return nil
70+
return nil, err
2871
}
2972
files, err := filepath.Glob(filepath.Join(homeDir, ".ssh/*.pub"))
3073
if err != nil {
31-
logrus.Warn(err)
32-
return nil
74+
panic(err) // Only possible error is ErrBadPattern, so this should be unreachable.
3375
}
34-
var res []PubKey
3576
for _, f := range files {
3677
if !strings.HasSuffix(f, ".pub") {
3778
panic(errors.Errorf("unexpected ssh public key filename %q", f))
3879
}
39-
entry := PubKey{
40-
Filename: f,
41-
}
42-
if content, err := os.ReadFile(f); err == nil {
43-
entry.Content = strings.TrimSpace(string(content))
44-
} else {
45-
logrus.WithError(err).Warningf("failed to read ssh public key %q", f)
80+
entry, err := readPublicKey(f)
81+
if err == nil {
82+
res = append(res, entry)
83+
} else if !errors.Is(err, os.ErrNotExist) {
84+
return nil, err
4685
}
47-
res = append(res, entry)
4886
}
49-
return res
87+
return res, nil
5088
}
5189

5290
func RemoveKnownHostEntries(sshLocalPort int) error {
@@ -77,7 +115,40 @@ func SSHArgs(instDir string) ([]string, error) {
77115
if len(controlSock) >= osutil.UnixPathMax {
78116
return nil, errors.Errorf("socket path %q is too long: >= UNIX_PATH_MAX=%d", controlSock, osutil.UnixPathMax)
79117
}
80-
args := []string{
118+
configDir, err := store.LimaConfigDir()
119+
if err != nil {
120+
return nil, err
121+
}
122+
privateKeyPath := filepath.Join(configDir, filenames.UserPrivateKey)
123+
_, err = os.Stat(privateKeyPath)
124+
if err != nil {
125+
return nil, err
126+
}
127+
args := []string{"-i", privateKeyPath}
128+
129+
// Append all private keys corresponding to ~/.ssh/*.pub to keep old instances workin
130+
// that had been created before lima started using an internal identity.
131+
homeDir, err := os.UserHomeDir()
132+
if err != nil {
133+
return nil, err
134+
}
135+
files, err := filepath.Glob(filepath.Join(homeDir, ".ssh/*.pub"))
136+
if err != nil {
137+
panic(err) // Only possible error is ErrBadPattern, so this should be unreachable.
138+
}
139+
for _, f := range files {
140+
if !strings.HasSuffix(f, ".pub") {
141+
panic(errors.Errorf("unexpected ssh public key filename %q", f))
142+
}
143+
privateKeyPath := strings.TrimSuffix(f, ".pub")
144+
_, err = os.Stat(privateKeyPath)
145+
if err != nil {
146+
return nil, err
147+
}
148+
args = append(args, "-i", privateKeyPath)
149+
}
150+
151+
args = append(args,
81152
"-l", u.Username, // guest and host have the same username, but we should specify the username explicitly (#85)
82153
"-o", "ControlMaster=auto",
83154
"-o", "ControlPath=" + controlSock,
@@ -88,6 +159,6 @@ func SSHArgs(instDir string) ([]string, error) {
88159
"-o", "PreferredAuthentications=publickey",
89160
"-o", "Compression=no",
90161
"-o", "BatchMode=yes",
91-
}
162+
)
92163
return args, nil
93164
}

pkg/sshutil/sshutil_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package sshutil
33
import "testing"
44

55
func TestDefaultPubKeys(t *testing.T) {
6-
keys := DefaultPubKeys()
6+
keys, _ := DefaultPubKeys()
77
t.Logf("found %d public keys", len(keys))
88
for _, key := range keys {
99
t.Logf("%s: %q", key.Filename, key.Content)

pkg/store/filenames/filenames.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
// Package filenames defines the names of the files that appear under an instance dir.
1+
// Package filenames defines the names of the files that appear under an instance dir
2+
// or inside the config directory.
23
//
34
// See docs/internal.md .
45
package filenames
56

7+
// Instance names starting with an underscore are reserved for lima internal usage
8+
9+
const (
10+
ConfigDir = "_config"
11+
CacheDir = "_cache" // not yet implemented
12+
)
13+
14+
// Filenames used inside the ConfigDir
15+
16+
const (
17+
UserPrivateKey = "user"
18+
UserPublicKey = UserPrivateKey + ".pub"
19+
)
20+
21+
// Filenames that may appear under an instance directory
22+
623
const (
724
LimaYAML = "lima.yaml"
825
CIDataISO = "cidata.iso"

pkg/store/store.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package store
22

33
import (
4+
"github.com/AkihiroSuda/lima/pkg/store/filenames"
45
"os"
56
"path/filepath"
67
"strings"
@@ -29,6 +30,15 @@ func LimaDir() (string, error) {
2930
return dir, nil
3031
}
3132

33+
// LimaConfigDir returns the path of the config directory, $LIMA_HOME/_config.
34+
func LimaConfigDir() (string, error) {
35+
limaDir, err := LimaDir()
36+
if err != nil {
37+
return "", err
38+
}
39+
return filepath.Join(limaDir, filenames.ConfigDir), nil
40+
}
41+
3242
// Instances returns the names of the instances under LimaDir.
3343
func Instances() ([]string, error) {
3444
limaDir, err := LimaDir()

0 commit comments

Comments
 (0)