/
ssh_driven_config.go
177 lines (155 loc) · 4.93 KB
/
ssh_driven_config.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
package gcp
import (
"archive/tar"
"compress/gzip"
"context"
"fmt"
"io"
"io/fs"
"net"
"net/http"
"os"
"path/filepath"
"github.com/rjkroege/gocloud/config"
"golang.org/x/crypto/ssh"
"log"
)
const metabase = "http://metadata.google.internal/computeMetadata/v1/instance/attributes/"
// TODO(rjk): This needs tests that can run locally. For that, I'd need a
// mock ssh server and a mock metadata service? (Yes?)
// ConfigureViaSsh invokes the specified command string via ssh to
// perform additional configuration of the target node. Significant
// additional featurism is possible.
func ConfigureViaSsh(settings *config.Settings, ni *NodeInfo, client *ssh.Client) error {
// I have no way of knowing the hostKey because I didn't set it. The
// system is newly launched and it makes the key for itself. But: I could
// make a bespoke key. Then, the "public" key would also be private. Or I
// could set some other kind of key and read it back.
//
// I want to preserve the key and use it when reconnecting. I need to
// verify that the node is who I think it is. I can set a secret _on_ the
// node at creation (shortly before) and then discover if if it has the
// secret?
//
// Given that the IP address comes over a secure connection, the only way
// that an adversary could man-in-the-middle me is if a router between me and
// Google has been misconfigured and can forward traffic to an arbitrary
// third party. I must validate some kind of shared secret.
pnm, err := config.GetNodeMetadata(
config.NewNodeProxiedMetadataClient(NewSshProxiedTransport(client)))
if err != nil {
return fmt.Errorf("can't read proxied node metadata: %v", err)
}
gottoken := pnm["instancetoken"]
if gottoken != ni.Token {
return fmt.Errorf("Got token %q, want %q. Maybe this is an IP hijack?", gottoken, ni.Token)
}
return InstallViaSsh(settings, ni, client)
}
func NewSshProxiedTransport(client *ssh.Client) http.RoundTripper {
dolly := http.DefaultTransport.(*http.Transport).Clone()
dolly.DialContext = func(_ context.Context, network, addr string) (net.Conn, error) {
return client.Dial(network, addr)
}
return dolly
}
func InstallViaSsh(settings *config.Settings, ni *NodeInfo, client *ssh.Client) error {
// Run tar on the remote to extract the copied binaries.
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("can't make an ssh execution session: %v", err)
}
session.Stdout = os.Stdout
session.Stderr = os.Stderr
inpipe, err := session.StdinPipe()
if err != nil {
return fmt.Errorf("can't get an ssh in pipe: %v", err)
}
// tar up the scripts and binaries locally.
go func() {
defer inpipe.Close()
if err := TarGZTools(inpipe); err != nil {
// TODO(rjk): I think that I can do something better about exiting.
log.Println("can't tar: %v", err)
}
}()
// TODO(rjk): Should I take these parameters from the toml file?
cmd := "cd / ; tar xzf -"
if err := session.Run(cmd); err != nil {
return fmt.Errorf("can't extract %q: %v", cmd, err)
}
// Start the supplementary services.
// TODO(rjk): Consider controlling this from the toml file?
for _, cmd := range []string{
"sudo /usr/local/bin/sessionender",
"sudo /usr/local/bin/cpud -pk /usr/local/keys/pk",
} {
session, err := client.NewSession()
if err != nil {
return fmt.Errorf("can't make an ssh execution session for %q: %v", cmd, err)
}
// Do the commands keep running?
if err := session.Start(cmd); err != nil {
return fmt.Errorf("can't Start %q: %v", cmd, err)
}
}
return nil
}
type Paths struct {
From string
To string
Pattern []string
}
func TarGZTools(w io.Writer) error {
zfd := gzip.NewWriter(w)
defer zfd.Close()
tw := tar.NewWriter(zfd)
defer tw.Close()
// TODO(rjk): This configuration should come from the TOML file.
for _, ptho := range []Paths{
{
From: "/usr/local/script",
To: "/usr/local/script",
Pattern: []string{"*"},
},
{
From: "/Users/rjkroege/wrks/archive/bins/linux/amd64",
To: "/usr/local/bin",
Pattern: []string{"cpud", "eza", "gotop", "rc", "sessionender", "mk", "p", "sam"},
},
} {
dfs := os.DirFS(ptho.From)
files := make([]string, 0, 20)
for _, g := range ptho.Pattern {
fs, err := fs.Glob(dfs, g)
if err != nil {
return fmt.Errorf("can't glob %q: %v", filepath.Join(ptho.From, g), err)
}
files = append(files, fs...)
}
for _, f := range files {
file := filepath.Join(ptho.From, f)
fi, err := os.Stat(file)
if err != nil {
return fmt.Errorf("can't stat %q: %v", file, err)
}
hdr := &tar.Header{
Name: filepath.Join(ptho.To, f),
Mode: int64(fi.Mode()),
Size: fi.Size(),
}
if err := tw.WriteHeader(hdr); err != nil {
return fmt.Errorf("fatal %v", err)
}
fd, err := os.Open(file)
if err != nil {
return fmt.Errorf("can't Open %q: %v", file, err)
}
defer fd.Close()
if _, err := io.Copy(tw, fd); err != nil {
return fmt.Errorf("can't copy %q: %v", file, err)
}
}
}
return nil
}