diff --git a/cmd/main.go b/cmd/main.go index a083835..c487979 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -1,10 +1,12 @@ package main import ( + "context" "encoding/json" "fmt" "log/slog" "os" + "time" "github.com/spf13/cobra" awsspiffe "github.com/spiffe/aws-spiffe-workload-helper" @@ -120,6 +122,176 @@ func exchangeX509SVIDForAWSCredentials( return credentials, nil } +func oneshotX509CredentialFile( + ctx context.Context, + force bool, + replace bool, + awsCredentialsPath string, + sf *sharedFlags, +) error { + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + x509Ctx, err := client.FetchX509Context(ctx) + if err != nil { + return fmt.Errorf("fetching x509 context: %w", err) + } + svid := x509Ctx.DefaultSVID() + slog.Debug( + "Fetched X509 SVID", + slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + ), + ) + + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + + // Now we write this to disk in the format that the AWS CLI/SDK + // expects for a credentials file. + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credential to file", "path", "./my-credential") + return nil +} + +func daemonX509CredentialFile( + ctx context.Context, + force bool, + replace bool, + awsCredentialsPath string, + sf *sharedFlags, +) error { + slog.Info("Starting AWS credential file daemon") + client, err := workloadapi.New( + ctx, + workloadapi.WithAddr(sf.workloadAPIAddr), + ) + if err != nil { + return fmt.Errorf("creating workload api client: %w", err) + } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() + + slog.Debug("Fetching initial X509 SVID") + x509Source, err := workloadapi.NewX509Source(ctx, workloadapi.WithClient(client)) + if err != nil { + return fmt.Errorf("creating x509 source: %w", err) + } + defer func() { + if err := x509Source.Close(); err != nil { + slog.Warn("Failed to close x509 source", "error", err) + } + }() + + svidUpdate := x509Source.Updated() + svid, err := x509Source.GetX509SVID() + if err != nil { + return fmt.Errorf("fetching initial X509 SVID: %w", err) + } + slog.Debug("Fetched initial X509 SVID", slog.Group("svid", + "spiffe_id", svid.ID, + "hint", svid.Hint, + "expires_at", svid.Certificates[0].NotAfter, + )) + + for { + slog.Debug("Exchanging X509 SVID for AWS credentials") + credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) + if err != nil { + return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + } + slog.Info( + "Successfully exchanged X509 SVID for AWS credentials", + ) + + expiresAt, err := time.Parse(time.RFC3339, credentials.Expiration) + if err != nil { + return fmt.Errorf("parsing expiration time: %w", err) + } + + slog.Debug("Writing AWS credentials to file", "path", awsCredentialsPath) + err = internal.UpsertAWSCredentialsFileProfile( + slog.Default(), + internal.AWSCredentialsFileConfig{ + Path: awsCredentialsPath, + Force: force, + ReplaceFile: replace, + }, + internal.AWSCredentialsFileProfile{ + AWSAccessKeyID: credentials.AccessKeyId, + AWSSecretAccessKey: credentials.SecretAccessKey, + AWSSessionToken: credentials.SessionToken, + }, + ) + if err != nil { + return fmt.Errorf("writing credentials to file: %w", err) + } + slog.Info("Wrote AWS credentials to file", "path", awsCredentialsPath) + + slog.Info( + "Sleeping until a new X509 SVID is received or the AWS credentials are close to expiry", + "aws_expires_at", expiresAt, + "aws_ttl", expiresAt.Sub(time.Now()), + "svid_expires_at", svid.Certificates[0].NotAfter, + "svid_ttl", svid.Certificates[0].NotAfter.Sub(time.Now()), + ) + select { + case <-time.After(time.Second * 10): + slog.Info("Triggering renewal as AWS credentials are close to expiry") + // TODO: Add case for AWS credential approaching expiry + case <-svidUpdate: + slog.Debug("Received potential X509 SVID update") + newSVID, err := x509Source.GetX509SVID() + if err != nil { + return fmt.Errorf("fetching updated X509 SVID: %w", err) + } + slog.Info( + "Received new X509 SVID from Workload API, will update AWS credentials", + slog.Group("svid", + "spiffe_id", newSVID.ID, + "hint", newSVID.Hint, + "expires_at", newSVID.Certificates[0].NotAfter, + ), + ) + svid = newSVID + case <-ctx.Done(): + return nil + } + } + +} + func newX509CredentialFileCmd() (*cobra.Command, error) { oneshot := false force := false @@ -131,53 +303,14 @@ func newX509CredentialFileCmd() (*cobra.Command, error) { Short: ``, Long: ``, RunE: func(cmd *cobra.Command, args []string) error { - ctx := cmd.Context() - client, err := workloadapi.New( - ctx, - workloadapi.WithAddr(sf.workloadAPIAddr), - ) - if err != nil { - return fmt.Errorf("creating workload api client: %w", err) - } - - x509Ctx, err := client.FetchX509Context(ctx) - if err != nil { - return fmt.Errorf("fetching x509 context: %w", err) - } - svid := x509Ctx.DefaultSVID() - slog.Debug( - "Fetched X509 SVID", - slog.Group("svid", - "spiffe_id", svid.ID, - "hint", svid.Hint, - ), - ) - - credentials, err := exchangeX509SVIDForAWSCredentials(sf, svid) - if err != nil { - return fmt.Errorf("exchanging X509 SVID for AWS credentials: %w", err) + if oneshot { + return oneshotX509CredentialFile( + cmd.Context(), force, replace, awsCredentialsPath, sf, + ) } - - // Now we write this to disk in the format that the AWS CLI/SDK - // expects for a credentials file. - err = internal.UpsertAWSCredentialsFileProfile( - slog.Default(), - internal.AWSCredentialsFileConfig{ - Path: awsCredentialsPath, - Force: force, - ReplaceFile: replace, - }, - internal.AWSCredentialsFileProfile{ - AWSAccessKeyID: credentials.AccessKeyId, - AWSSecretAccessKey: credentials.SecretAccessKey, - AWSSessionToken: credentials.SessionToken, - }, + return daemonX509CredentialFile( + cmd.Context(), force, replace, awsCredentialsPath, sf, ) - if err != nil { - return fmt.Errorf("writing credentials to file: %w", err) - } - slog.Info("Wrote AWS credential to file", "path", "./my-credential") - return nil }, // Hidden for now as the daemon is likely more "usable" Hidden: true, @@ -211,6 +344,11 @@ func newX509CredentialProcessCmd() (*cobra.Command, error) { if err != nil { return fmt.Errorf("creating workload api client: %w", err) } + defer func() { + if err := client.Close(); err != nil { + slog.Warn("Failed to close workload API client", "error", err) + } + }() x509Ctx, err := client.FetchX509Context(ctx) if err != nil {