Skip to content

Commit

Permalink
Add basic daemon mode loop
Browse files Browse the repository at this point in the history
Signed-off-by: Noah Stride <noah.stride@goteleport.com>
  • Loading branch information
strideynet committed Nov 28, 2024
1 parent 3bf726c commit 022905d
Showing 1 changed file with 183 additions and 45 deletions.
228 changes: 183 additions & 45 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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()),

Check failure on line 265 in cmd/main.go

View workflow job for this annotation

GitHub Actions / lint

S1024: should use time.Until instead of t.Sub(time.Now()) (gosimple)
"svid_expires_at", svid.Certificates[0].NotAfter,
"svid_ttl", svid.Certificates[0].NotAfter.Sub(time.Now()),

Check failure on line 267 in cmd/main.go

View workflow job for this annotation

GitHub Actions / lint

S1024: should use time.Until instead of t.Sub(time.Now()) (gosimple)
)
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
Expand All @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 022905d

Please sign in to comment.