-
Notifications
You must be signed in to change notification settings - Fork 1.7k
/
main.go
298 lines (252 loc) · 11.3 KB
/
main.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
/*
Copyright 2021-2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package main
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"strings"
"syscall"
"time"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"gopkg.in/yaml.v2"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/tbot"
"github.com/gravitational/teleport/lib/tbot/config"
"github.com/gravitational/teleport/lib/utils"
)
var log = logrus.WithFields(logrus.Fields{
trace.Component: teleport.ComponentTBot,
})
const (
authServerEnvVar = "TELEPORT_AUTH_SERVER"
tokenEnvVar = "TELEPORT_BOT_TOKEN"
)
func main() {
if err := Run(os.Args[1:], os.Stdout); err != nil {
utils.FatalError(err)
}
}
const appHelp = `Teleport Machine ID
Machine ID issues and renews short-lived certificates so your machines can
access Teleport protected resources in the same way your engineers do!
Find out more at https://goteleport.com/docs/machine-id/introduction/`
func Run(args []string, stdout io.Writer) error {
var cf config.CLIConf
utils.InitLogger(utils.LoggingForDaemon, logrus.InfoLevel)
app := utils.InitCLIParser("tbot", appHelp).Interspersed(false)
app.Flag("debug", "Verbose logging to stdout.").Short('d').BoolVar(&cf.Debug)
app.Flag("config", "Path to a configuration file.").Short('c').StringVar(&cf.ConfigPath)
app.Flag("fips", "Runs tbot in FIPS compliance mode. This requires the FIPS binary is in use.").BoolVar(&cf.FIPS)
app.HelpFlag.Short('h')
joinMethodList := fmt.Sprintf(
"(%s)",
strings.Join(config.SupportedJoinMethods, ", "),
)
versionCmd := app.Command("version", "Print the version of your tbot binary.")
startCmd := app.Command("start", "Starts the renewal bot, writing certificates to the data dir at a set interval.")
startCmd.Flag("auth-server", "Address of the Teleport Auth Server or Proxy Server.").Short('a').Envar(authServerEnvVar).StringVar(&cf.AuthServer)
startCmd.Flag("token", "A bot join token, if attempting to onboard a new bot; used on first connect.").Envar(tokenEnvVar).StringVar(&cf.Token)
startCmd.Flag("ca-pin", "CA pin to validate the Teleport Auth Server; used on first connect.").StringsVar(&cf.CAPins)
startCmd.Flag("data-dir", "Directory to store internal bot data. Access to this directory should be limited.").StringVar(&cf.DataDir)
startCmd.Flag("destination-dir", "Directory to write short-lived machine certificates.").StringVar(&cf.DestinationDir)
startCmd.Flag("certificate-ttl", "TTL of short-lived machine certificates.").DurationVar(&cf.CertificateTTL)
startCmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&cf.RenewalInterval)
startCmd.Flag("join-method", "Method to use to join the cluster. "+joinMethodList).Default(config.DefaultJoinMethod).EnumVar(&cf.JoinMethod, config.SupportedJoinMethods...)
startCmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&cf.Oneshot)
initCmd := app.Command("init", "Initialize a certificate destination directory for writes from a separate bot user.")
initCmd.Flag("destination-dir", "Directory to write short-lived machine certificates to.").StringVar(&cf.DestinationDir)
initCmd.Flag("owner", "Defines Linux \"user:group\" owner of \"--destination-dir\". Defaults to the Linux user running tbot if unspecified.").StringVar(&cf.Owner)
initCmd.Flag("bot-user", "Enables POSIX ACLs and defines Linux user that can read/write short-lived certificates to \"--destination-dir\".").StringVar(&cf.BotUser)
initCmd.Flag("reader-user", "Enables POSIX ACLs and defines Linux user that will read short-lived certificates from \"--destination-dir\".").StringVar(&cf.ReaderUser)
initCmd.Flag("init-dir", "If using a config file and multiple destinations are configured, controls which destination dir to configure.").StringVar(&cf.InitDir)
initCmd.Flag("clean", "If set, remove unexpected files and directories from the destination.").BoolVar(&cf.Clean)
configureCmd := app.Command("configure", "Creates a config file based on flags provided, and writes it to stdout or a file (-c <path>).")
configureCmd.Flag("auth-server", "Address of the Teleport Auth Server (On-Prem installs) or Proxy Server (Cloud installs).").Short('a').Envar(authServerEnvVar).StringVar(&cf.AuthServer)
configureCmd.Flag("ca-pin", "CA pin to validate the Teleport Auth Server; used on first connect.").StringsVar(&cf.CAPins)
configureCmd.Flag("certificate-ttl", "TTL of short-lived machine certificates.").Default("60m").DurationVar(&cf.CertificateTTL)
configureCmd.Flag("data-dir", "Directory to store internal bot data. Access to this directory should be limited.").StringVar(&cf.DataDir)
configureCmd.Flag("join-method", "Method to use to join the cluster. "+joinMethodList).Default(config.DefaultJoinMethod).EnumVar(&cf.JoinMethod, config.SupportedJoinMethods...)
configureCmd.Flag("oneshot", "If set, quit after the first renewal.").BoolVar(&cf.Oneshot)
configureCmd.Flag("renewal-interval", "Interval at which short-lived certificates are renewed; must be less than the certificate TTL.").DurationVar(&cf.RenewalInterval)
configureCmd.Flag("token", "A bot join token, if attempting to onboard a new bot; used on first connect.").Envar(tokenEnvVar).StringVar(&cf.Token)
configureCmd.Flag("output", "Path to write the generated configuration file to rather than write to stdout.").Short('o').StringVar(&cf.ConfigureOutput)
watchCmd := app.Command("watch", "Watch a destination directory for changes.").Hidden()
dbCmd := app.Command("db", "Execute database commands through tsh.")
dbCmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Required().StringVar(&cf.Proxy)
dbCmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&cf.DestinationDir)
dbCmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&cf.Cluster)
dbRemaining := config.RemainingArgs(dbCmd.Arg(
"args",
"Arguments to `tsh db ...`; prefix with `-- ` to ensure flags are passed correctly.",
))
proxyCmd := app.Command("proxy", "Start a local TLS proxy via tsh to connect to Teleport in single-port mode.")
proxyCmd.Flag("proxy", "The Teleport proxy server to use, in host:port form.").Required().StringVar(&cf.Proxy)
proxyCmd.Flag("destination-dir", "The destination directory with which to authenticate tsh").StringVar(&cf.DestinationDir)
proxyCmd.Flag("cluster", "The cluster name. Extracted from the certificate if unset.").StringVar(&cf.Cluster)
proxyRemaining := config.RemainingArgs(proxyCmd.Arg(
"args",
"Arguments to `tsh proxy ...`; prefix with `-- ` to ensure flags are passed correctly.",
))
kubeCmd := app.Command("kube", "Kubernetes helpers").Hidden()
kubeCredentialsCmd := kubeCmd.Command("credentials", "Get credentials for kubectl access").Hidden()
kubeCredentialsCmd.Flag("destination-dir", "The destination directory with which to generate Kubernetes credentials").Required().StringVar(&cf.DestinationDir)
utils.UpdateAppUsageTemplate(app, args)
command, err := app.Parse(args)
if err != nil {
app.Usage(args)
return trace.Wrap(err)
}
// Remaining args are stored directly to a []string rather than written to
// a shared ref like most other kingpin args, so we'll need to manually
// move them to the remaining args field.
if len(*dbRemaining) > 0 {
cf.RemainingArgs = *dbRemaining
} else if len(*proxyRemaining) > 0 {
cf.RemainingArgs = *proxyRemaining
}
// While in debug mode, send logs to stdout.
if cf.Debug {
utils.InitLogger(utils.LoggingForDaemon, logrus.DebugLevel)
}
botConfig, err := config.FromCLIConf(&cf)
if err != nil {
return trace.Wrap(err)
}
switch command {
case versionCmd.FullCommand():
err = onVersion()
case startCmd.FullCommand():
err = onStart(botConfig)
case configureCmd.FullCommand():
err = onConfigure(cf, stdout)
case initCmd.FullCommand():
err = onInit(botConfig, &cf)
case watchCmd.FullCommand():
err = onWatch(botConfig)
case dbCmd.FullCommand():
err = onDBCommand(botConfig, &cf)
case proxyCmd.FullCommand():
err = onProxyCommand(botConfig, &cf)
case kubeCredentialsCmd.FullCommand():
err = onKubeCredentialsCommand(botConfig, &cf)
default:
// This should only happen when there's a missing switch case above.
err = trace.BadParameter("command %q not configured", command)
}
return err
}
func onVersion() error {
utils.PrintVersion()
return nil
}
func onConfigure(
cf config.CLIConf,
stdout io.Writer,
) error {
out := stdout
outPath := cf.ConfigureOutput
if outPath != "" {
f, err := os.Create(outPath)
if err != nil {
return trace.Wrap(err)
}
defer f.Close()
out = f
}
// We do not want to load an existing configuration file as this will cause
// it to be merged with the provided flags and defaults.
cf.ConfigPath = ""
cfg, err := config.FromCLIConf(&cf)
if err != nil {
return nil
}
fmt.Fprintln(out, "# tbot config file generated by `configure` command")
enc := yaml.NewEncoder(out)
if err := enc.Encode(cfg); err != nil {
return trace.Wrap(err)
}
if err := enc.Close(); err != nil {
return trace.Wrap(err)
}
if outPath != "" {
log.Infof(
"Generated config file written to file: %s", outPath,
)
}
return nil
}
func onWatch(botConfig *config.BotConfig) error {
return trace.NotImplemented("watch not yet implemented")
}
func onStart(botConfig *config.BotConfig) error {
reloadChan := make(chan struct{})
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go handleSignals(log, reloadChan, cancel)
telemetrySentCh := make(chan struct{})
go func() {
defer close(telemetrySentCh)
if err := sendTelemetry(
ctx, telemetryClient(os.Getenv), os.Getenv, log, botConfig,
); err != nil {
log.WithError(err).Error(
"Failed to send anonymous telemetry.",
)
}
}()
// Ensures telemetry finishes sending before function exits.
defer func() {
select {
case <-telemetrySentCh:
return
case <-ctx.Done():
default:
}
waitTime := 10 * time.Second
log.Infof(
"Waiting up to %s for anonymous telemetry to finish sending before exiting. Press CTRL-C to cancel.",
waitTime,
)
ctx, cancel := context.WithTimeout(ctx, waitTime)
defer cancel()
select {
case <-ctx.Done():
log.Warn(
"Anonymous telemetry transmission canceled due to signal or timeout.",
)
case <-telemetrySentCh:
}
}()
b := tbot.New(botConfig, log, reloadChan)
return trace.Wrap(b.Run(ctx))
}
// handleSignals handles incoming Unix signals.
func handleSignals(log logrus.FieldLogger, reload chan struct{}, cancel context.CancelFunc) {
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGHUP, syscall.SIGUSR1)
for signal := range signals {
switch signal {
case syscall.SIGINT:
log.Info("Received interrupt, canceling...")
cancel()
return
case syscall.SIGHUP, syscall.SIGUSR1:
log.Info("Received reload signal, reloading...")
reload <- struct{}{}
}
}
}