diff --git a/cmd/pbm-agent/main.go b/cmd/pbm-agent/main.go index f5f5dedae..0c9dfcfbe 100644 --- a/cmd/pbm-agent/main.go +++ b/cmd/pbm-agent/main.go @@ -16,6 +16,7 @@ import ( "github.com/percona/percona-backup-mongodb/pbm/connect" "github.com/percona/percona-backup-mongodb/pbm/errors" + "github.com/percona/percona-backup-mongodb/pbm/kinit" "github.com/percona/percona-backup-mongodb/pbm/log" "github.com/percona/percona-backup-mongodb/pbm/util" "github.com/percona/percona-backup-mongodb/pbm/version" @@ -123,6 +124,18 @@ func setRootFlags(rootCmd *cobra.Command) { _ = viper.BindPFlag("log.level", rootCmd.Flags().Lookup("log-level")) _ = viper.BindEnv("log.level", "LOG_LEVEL") viper.SetDefault("log.level", log.D) + + rootCmd.Flags().String("kerberos-keytab", "", + "Path to Kerberos keytab (if empty uses host keytab)") + _ = viper.BindPFlag("kerberos.keytab", + rootCmd.Flags().Lookup("kerberos-keytab")) + _ = viper.BindEnv("kerberos.keytab", "PBM_KERBEROS_KEYTAB") + + rootCmd.Flags().Duration("kerberos-renew-interval", 0, + "How often to renew Kerberos ticket (if 0 renewal is disabled)") + _ = viper.BindPFlag("kerberos.renew-interval", + rootCmd.Flags().Lookup("kerberos-renew-interval")) + _ = viper.BindEnv("kerberos.renew-interval", "PBM_KERBEROS_RENEW_INTERVAL") } func versionCommand() *cobra.Command { @@ -243,5 +256,24 @@ func runAgent( } go agent.HbStatus(ctx) + setupKerberosRenewal(ctx, mongoURI, logger) + return errors.Wrap(agent.Start(ctx), "listen the commands stream") } + +func setupKerberosRenewal(ctx context.Context, mongoURI string, l log.Logger) { + interval := viper.GetDuration("kerberos.renew-interval") + if interval <= 0 { // 0 or negative means disabled + return + } + + keytab := viper.GetString("kerberos.keytab") + + rm, err := kinit.New(mongoURI, keytab, interval) + if err != nil { + l.Printf("Kerberos renewal skipped: %s", err) + return + } + + rm.Start(ctx) +} diff --git a/pbm/kinit/renewal.go b/pbm/kinit/renewal.go new file mode 100644 index 000000000..7d0828214 --- /dev/null +++ b/pbm/kinit/renewal.go @@ -0,0 +1,99 @@ +package kinit + +import ( + "context" + "net/url" + "os/exec" + "strings" + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/percona/percona-backup-mongodb/pbm/errors" + "github.com/percona/percona-backup-mongodb/pbm/log" +) + +// Renewal periodically refreshes a Kerberos TGT using the `kinit` command-line tool. +type Renewal struct { + Keytab string + Principal string + Interval time.Duration +} + +func New(mongoURI, keytab string, interval time.Duration) (*Renewal, error) { + if _, err := exec.LookPath("kinit"); err != nil { + return nil, errors.New("kinit not found in PATH") + } + + principal, err := principalFromMongoURI(mongoURI) + if err != nil { + return nil, errors.Wrap(err, "parsing principal") + } + + return &Renewal{ + Keytab: keytab, + Principal: principal, + Interval: interval, + }, nil +} + +// Start performs an immediate renewal and then continues to renew the ticket every Interval. +func (r Renewal) Start(ctx context.Context) { + l := log.FromContext(ctx).NewEvent("", "", "", primitive.Timestamp{}) + + if err := r.renew(ctx); err != nil { + l.Error("Kerberos ticket renewal failed: %v", err) + } else { + l.Info("Kerberos ticket obtained successfully") + } + + tkr := time.NewTicker(r.Interval) + go func() { + defer tkr.Stop() + for { + select { + case <-ctx.Done(): + return + case <-tkr.C: + if err := r.renew(ctx); err != nil { + l.Error("Kerberos ticket renewal failed: %v", err) + } else { + l.Info("Kerberos ticket obtained successfully") + } + } + } + }() +} + +func (r Renewal) renew(ctx context.Context) error { + args := []string{"-k"} + if r.Keytab != "" { + args = append(args, "-t", r.Keytab) + } + args = append(args, r.Principal) + + cmd := exec.CommandContext(ctx, "kinit", args...) + if out, err := cmd.CombinedOutput(); err != nil { + msg := strings.TrimSpace(string(out)) + return errors.New(msg) + } + return nil +} + +func principalFromMongoURI(uri string) (string, error) { + parsed, err := url.Parse(uri) + if err != nil { + return "", errors.Wrap(err, "cannot parse URI") + } + + if parsed.User == nil { + return "", errors.New("no user info in URI") + } + + principal := parsed.User.Username() + if principal == "" { + return "", errors.New("empty principal in URI") + } + + return principal, nil +}