mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
1082 lines
34 KiB
Go
1082 lines
34 KiB
Go
/*
|
|
Copyright (c) 2023 Infisical Inc.
|
|
*/
|
|
package cmd
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"path"
|
|
"runtime"
|
|
"slices"
|
|
"sync"
|
|
"syscall"
|
|
"text/template"
|
|
"time"
|
|
|
|
infisicalSdk "github.com/infisical/go-sdk"
|
|
"github.com/rs/zerolog/log"
|
|
"gopkg.in/yaml.v2"
|
|
|
|
"github.com/Infisical/infisical-merge/packages/api"
|
|
"github.com/Infisical/infisical-merge/packages/config"
|
|
"github.com/Infisical/infisical-merge/packages/models"
|
|
"github.com/Infisical/infisical-merge/packages/util"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
const DEFAULT_INFISICAL_CLOUD_URL = "https://app.infisical.com"
|
|
|
|
// duration to reduce from expiry of dynamic leases so that it gets triggered before expiry
|
|
const DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER = -15
|
|
|
|
type Config struct {
|
|
Infisical InfisicalConfig `yaml:"infisical"`
|
|
Auth AuthConfig `yaml:"auth"`
|
|
Sinks []Sink `yaml:"sinks"`
|
|
Templates []Template `yaml:"templates"`
|
|
}
|
|
|
|
type InfisicalConfig struct {
|
|
Address string `yaml:"address"`
|
|
ExitAfterAuth bool `yaml:"exit-after-auth"`
|
|
}
|
|
|
|
type AuthConfig struct {
|
|
Type string `yaml:"type"`
|
|
Config interface{} `yaml:"config"`
|
|
}
|
|
|
|
type UniversalAuth struct {
|
|
ClientIDPath string `yaml:"client-id"`
|
|
ClientSecretPath string `yaml:"client-secret"`
|
|
RemoveClientSecretOnRead bool `yaml:"remove_client_secret_on_read"`
|
|
}
|
|
|
|
type KubernetesAuth struct {
|
|
IdentityID string `yaml:"identity-id"`
|
|
ServiceAccountToken string `yaml:"service-account-token"`
|
|
}
|
|
|
|
type AzureAuth struct {
|
|
IdentityID string `yaml:"identity-id"`
|
|
}
|
|
|
|
type GcpIdTokenAuth struct {
|
|
IdentityID string `yaml:"identity-id"`
|
|
}
|
|
|
|
type GcpIamAuth struct {
|
|
IdentityID string `yaml:"identity-id"`
|
|
ServiceAccountKey string `yaml:"service-account-key"`
|
|
}
|
|
|
|
type AwsIamAuth struct {
|
|
IdentityID string `yaml:"identity-id"`
|
|
}
|
|
|
|
type Sink struct {
|
|
Type string `yaml:"type"`
|
|
Config SinkDetails `yaml:"config"`
|
|
}
|
|
|
|
type SinkDetails struct {
|
|
Path string `yaml:"path"`
|
|
}
|
|
|
|
type Template struct {
|
|
SourcePath string `yaml:"source-path"`
|
|
Base64TemplateContent string `yaml:"base64-template-content"`
|
|
DestinationPath string `yaml:"destination-path"`
|
|
TemplateContent string `yaml:"template-content"`
|
|
|
|
Config struct { // Configurations for the template
|
|
PollingInterval string `yaml:"polling-interval"` // How often to poll for changes in the secret
|
|
Execute struct {
|
|
Command string `yaml:"command"` // Command to execute once the template has been rendered
|
|
Timeout int64 `yaml:"timeout"` // Timeout for the command
|
|
} `yaml:"execute"` // Command to execute once the template has been rendered
|
|
} `yaml:"config"`
|
|
}
|
|
|
|
type DynamicSecretLease struct {
|
|
LeaseID string
|
|
ExpireAt time.Time
|
|
Environment string
|
|
SecretPath string
|
|
Slug string
|
|
ProjectSlug string
|
|
Data map[string]interface{}
|
|
TemplateIDs []int
|
|
}
|
|
|
|
type DynamicSecretLeaseManager struct {
|
|
leases []DynamicSecretLease
|
|
mutex sync.Mutex
|
|
}
|
|
|
|
func (d *DynamicSecretLeaseManager) Prune() {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
d.leases = slices.DeleteFunc(d.leases, func(s DynamicSecretLease) bool {
|
|
return time.Now().After(s.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second))
|
|
})
|
|
}
|
|
|
|
func (d *DynamicSecretLeaseManager) Append(lease DynamicSecretLease) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
index := slices.IndexFunc(d.leases, func(s DynamicSecretLease) bool {
|
|
if lease.SecretPath == s.SecretPath && lease.Environment == s.Environment && lease.ProjectSlug == s.ProjectSlug && lease.Slug == s.Slug {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
if index != -1 {
|
|
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, lease.TemplateIDs...)
|
|
return
|
|
}
|
|
d.leases = append(d.leases, lease)
|
|
}
|
|
|
|
func (d *DynamicSecretLeaseManager) RegisterTemplate(projectSlug, environment, secretPath, slug string, templateId int) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
index := slices.IndexFunc(d.leases, func(lease DynamicSecretLease) bool {
|
|
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
|
|
return true
|
|
}
|
|
return false
|
|
})
|
|
|
|
if index != -1 {
|
|
d.leases[index].TemplateIDs = append(d.leases[index].TemplateIDs, templateId)
|
|
}
|
|
}
|
|
|
|
func (d *DynamicSecretLeaseManager) GetLease(projectSlug, environment, secretPath, slug string) *DynamicSecretLease {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
for _, lease := range d.leases {
|
|
if lease.SecretPath == secretPath && lease.Environment == environment && lease.ProjectSlug == projectSlug && lease.Slug == slug {
|
|
return &lease
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// for a given template find the first expiring lease
|
|
// The bool indicates whether it contains valid expiry list
|
|
func (d *DynamicSecretLeaseManager) GetFirstExpiringLeaseTime(templateId int) (time.Time, bool) {
|
|
d.mutex.Lock()
|
|
defer d.mutex.Unlock()
|
|
|
|
if len(d.leases) == 0 {
|
|
return time.Time{}, false
|
|
}
|
|
|
|
var firstExpiry time.Time
|
|
for i, el := range d.leases {
|
|
if i == 0 {
|
|
firstExpiry = el.ExpireAt
|
|
}
|
|
newLeaseTime := el.ExpireAt.Add(DYNAMIC_SECRET_PRUNE_EXPIRE_BUFFER * time.Second)
|
|
if newLeaseTime.Before(firstExpiry) {
|
|
firstExpiry = newLeaseTime
|
|
}
|
|
}
|
|
return firstExpiry, true
|
|
}
|
|
|
|
func NewDynamicSecretLeaseManager(sigChan chan os.Signal) *DynamicSecretLeaseManager {
|
|
manager := &DynamicSecretLeaseManager{}
|
|
return manager
|
|
}
|
|
|
|
func ReadFile(filePath string) ([]byte, error) {
|
|
return ioutil.ReadFile(filePath)
|
|
}
|
|
|
|
func ExecuteCommandWithTimeout(command string, timeout int64) error {
|
|
|
|
shell := [2]string{"sh", "-c"}
|
|
if runtime.GOOS == "windows" {
|
|
shell = [2]string{"cmd", "/C"}
|
|
} else {
|
|
currentShell := os.Getenv("SHELL")
|
|
if currentShell != "" {
|
|
shell[0] = currentShell
|
|
}
|
|
}
|
|
|
|
ctx := context.Background()
|
|
if timeout > 0 {
|
|
var cancel context.CancelFunc
|
|
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
|
|
defer cancel()
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, shell[0], shell[1], command)
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
if exitError, ok := err.(*exec.ExitError); ok { // type assertion
|
|
if exitError.ProcessState.ExitCode() == -1 {
|
|
return fmt.Errorf("command timed out")
|
|
}
|
|
}
|
|
return err
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func FileExists(filepath string) bool {
|
|
info, err := os.Stat(filepath)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
return !info.IsDir()
|
|
}
|
|
|
|
// WriteToFile writes data to the specified file path.
|
|
func WriteBytesToFile(data *bytes.Buffer, outputPath string) error {
|
|
outputFile, err := os.Create(outputPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer outputFile.Close()
|
|
|
|
_, err = outputFile.Write(data.Bytes())
|
|
return err
|
|
}
|
|
|
|
func ParseAuthConfig(authConfigFile []byte, destination interface{}) error {
|
|
if err := yaml.Unmarshal(authConfigFile, destination); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ParseAgentConfig(configFile []byte) (*Config, error) {
|
|
var rawConfig struct {
|
|
Infisical InfisicalConfig `yaml:"infisical"`
|
|
Auth struct {
|
|
Type string `yaml:"type"`
|
|
Config map[string]interface{} `yaml:"config"`
|
|
} `yaml:"auth"`
|
|
Sinks []Sink `yaml:"sinks"`
|
|
Templates []Template `yaml:"templates"`
|
|
}
|
|
|
|
if err := yaml.Unmarshal(configFile, &rawConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Set defaults
|
|
if rawConfig.Infisical.Address == "" {
|
|
rawConfig.Infisical.Address = DEFAULT_INFISICAL_CLOUD_URL
|
|
}
|
|
|
|
config.INFISICAL_URL = util.AppendAPIEndpoint(rawConfig.Infisical.Address)
|
|
|
|
log.Info().Msgf("Infisical instance address set to %s", rawConfig.Infisical.Address)
|
|
|
|
config := &Config{
|
|
Infisical: rawConfig.Infisical,
|
|
Auth: AuthConfig{
|
|
Type: rawConfig.Auth.Type,
|
|
Config: rawConfig.Auth.Config,
|
|
},
|
|
Sinks: rawConfig.Sinks,
|
|
Templates: rawConfig.Templates,
|
|
}
|
|
|
|
return config, nil
|
|
}
|
|
|
|
type secretArguments struct {
|
|
IsRecursive bool `json:"recursive"`
|
|
ShouldExpandSecretReferences *bool `json:"expandSecretReferences,omitempty"`
|
|
}
|
|
|
|
func (s *secretArguments) SetDefaults() {
|
|
if s.ShouldExpandSecretReferences == nil {
|
|
var bool = true
|
|
s.ShouldExpandSecretReferences = &bool
|
|
}
|
|
}
|
|
|
|
func secretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string, ...string) ([]models.SingleEnvironmentVariable, error) {
|
|
// ...string is because golang doesn't have optional arguments.
|
|
// thus we make it slice and pick it only first element
|
|
return func(projectID, envSlug, secretPath string, args ...string) ([]models.SingleEnvironmentVariable, error) {
|
|
var parsedArguments secretArguments
|
|
// to make it optional
|
|
if len(args) > 0 {
|
|
err := json.Unmarshal([]byte(args[0]), &parsedArguments)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
parsedArguments.SetDefaults()
|
|
|
|
res, err := util.GetPlainTextSecretsV3(accessToken, projectID, envSlug, secretPath, true, parsedArguments.IsRecursive, "", *parsedArguments.ShouldExpandSecretReferences)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if existingEtag != res.Etag {
|
|
*currentEtag = res.Etag
|
|
}
|
|
|
|
return res.Secrets, nil
|
|
}
|
|
}
|
|
|
|
func getSingleSecretTemplateFunction(accessToken string, existingEtag string, currentEtag *string) func(string, string, string, string) (models.SingleEnvironmentVariable, error) {
|
|
return func(projectID, envSlug, secretPath, secretName string) (models.SingleEnvironmentVariable, error) {
|
|
secret, requestEtag, err := util.GetSinglePlainTextSecretByNameV3(accessToken, projectID, envSlug, secretPath, secretName)
|
|
if err != nil {
|
|
return models.SingleEnvironmentVariable{}, err
|
|
}
|
|
|
|
if existingEtag != requestEtag {
|
|
*currentEtag = requestEtag
|
|
}
|
|
|
|
return secret, nil
|
|
}
|
|
}
|
|
|
|
func dynamicSecretTemplateFunction(accessToken string, dynamicSecretManager *DynamicSecretLeaseManager, templateId int) func(...string) (map[string]interface{}, error) {
|
|
return func(args ...string) (map[string]interface{}, error) {
|
|
argLength := len(args)
|
|
if argLength != 4 && argLength != 5 {
|
|
return nil, fmt.Errorf("invalid arguments found for dynamic-secret function. Check template %d", templateId)
|
|
}
|
|
|
|
projectSlug, envSlug, secretPath, slug, ttl := args[0], args[1], args[2], args[3], ""
|
|
if argLength == 5 {
|
|
ttl = args[4]
|
|
}
|
|
dynamicSecretData := dynamicSecretManager.GetLease(projectSlug, envSlug, secretPath, slug)
|
|
if dynamicSecretData != nil {
|
|
dynamicSecretManager.RegisterTemplate(projectSlug, envSlug, secretPath, slug, templateId)
|
|
return dynamicSecretData.Data, nil
|
|
}
|
|
|
|
res, err := util.CreateDynamicSecretLease(accessToken, projectSlug, envSlug, secretPath, slug, ttl)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
dynamicSecretManager.Append(DynamicSecretLease{LeaseID: res.Lease.Id, ExpireAt: res.Lease.ExpireAt, Environment: envSlug, SecretPath: secretPath, Slug: slug, ProjectSlug: projectSlug, Data: res.Data, TemplateIDs: []int{templateId}})
|
|
return res.Data, nil
|
|
}
|
|
}
|
|
|
|
func ProcessTemplate(templateId int, templatePath string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretManager *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
|
|
// custom template function to fetch secrets from Infisical
|
|
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag)
|
|
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretManager, templateId)
|
|
getSingleSecretFunction := getSingleSecretTemplateFunction(accessToken, existingEtag, currentEtag)
|
|
funcs := template.FuncMap{
|
|
"secret": secretFunction, // depreciated
|
|
"listSecrets": secretFunction,
|
|
"dynamic_secret": dynamicSecretFunction,
|
|
"getSecretByName": getSingleSecretFunction,
|
|
"minus": func(a, b int) int {
|
|
return a - b
|
|
},
|
|
"add": func(a, b int) int {
|
|
return a + b
|
|
},
|
|
}
|
|
|
|
templateName := path.Base(templatePath)
|
|
tmpl, err := template.New(templateName).Funcs(funcs).ParseFiles(templatePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &buf, nil
|
|
}
|
|
|
|
func ProcessBase64Template(templateId int, encodedTemplate string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
|
|
// custom template function to fetch secrets from Infisical
|
|
decoded, err := base64.StdEncoding.DecodeString(encodedTemplate)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
templateString := string(decoded)
|
|
|
|
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
|
|
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
|
|
funcs := template.FuncMap{
|
|
"secret": secretFunction,
|
|
"dynamic_secret": dynamicSecretFunction,
|
|
}
|
|
|
|
templateName := "base64Template"
|
|
|
|
tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &buf, nil
|
|
}
|
|
|
|
func ProcessLiteralTemplate(templateId int, templateString string, data interface{}, accessToken string, existingEtag string, currentEtag *string, dynamicSecretLeaser *DynamicSecretLeaseManager) (*bytes.Buffer, error) {
|
|
secretFunction := secretTemplateFunction(accessToken, existingEtag, currentEtag) // TODO: Fix this
|
|
dynamicSecretFunction := dynamicSecretTemplateFunction(accessToken, dynamicSecretLeaser, templateId)
|
|
funcs := template.FuncMap{
|
|
"secret": secretFunction,
|
|
"dynamic_secret": dynamicSecretFunction,
|
|
}
|
|
|
|
templateName := "literalTemplate"
|
|
|
|
tmpl, err := template.New(templateName).Funcs(funcs).Parse(templateString)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &buf, nil
|
|
}
|
|
|
|
type AgentManager struct {
|
|
accessToken string
|
|
accessTokenTTL time.Duration
|
|
accessTokenMaxTTL time.Duration
|
|
accessTokenFetchedTime time.Time
|
|
accessTokenRefreshedTime time.Time
|
|
mutex sync.Mutex
|
|
filePaths []Sink // Store file paths if needed
|
|
templates []Template
|
|
dynamicSecretLeases *DynamicSecretLeaseManager
|
|
|
|
authConfigBytes []byte
|
|
authStrategy util.AuthStrategyType
|
|
|
|
newAccessTokenNotificationChan chan bool
|
|
removeUniversalAuthClientSecretOnRead bool
|
|
cachedUniversalAuthClientSecret string
|
|
exitAfterAuth bool
|
|
|
|
infisicalClient infisicalSdk.InfisicalClientInterface
|
|
}
|
|
|
|
type NewAgentMangerOptions struct {
|
|
FileDeposits []Sink
|
|
Templates []Template
|
|
|
|
AuthConfigBytes []byte
|
|
AuthStrategy util.AuthStrategyType
|
|
|
|
NewAccessTokenNotificationChan chan bool
|
|
ExitAfterAuth bool
|
|
}
|
|
|
|
func NewAgentManager(options NewAgentMangerOptions) *AgentManager {
|
|
customHeaders, err := util.GetInfisicalCustomHeadersMap()
|
|
if err != nil {
|
|
util.HandleError(err, "Unable to get custom headers")
|
|
}
|
|
return &AgentManager{
|
|
filePaths: options.FileDeposits,
|
|
templates: options.Templates,
|
|
|
|
authConfigBytes: options.AuthConfigBytes,
|
|
authStrategy: options.AuthStrategy,
|
|
|
|
newAccessTokenNotificationChan: options.NewAccessTokenNotificationChan,
|
|
exitAfterAuth: options.ExitAfterAuth,
|
|
|
|
infisicalClient: infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
|
|
SiteUrl: config.INFISICAL_URL,
|
|
UserAgent: api.USER_AGENT, // ? Should we perhaps use a different user agent for the Agent for better analytics?
|
|
AutoTokenRefresh: false,
|
|
CustomHeaders: customHeaders,
|
|
}),
|
|
}
|
|
|
|
}
|
|
|
|
func (tm *AgentManager) SetToken(token string, accessTokenTTL time.Duration, accessTokenMaxTTL time.Duration) {
|
|
tm.mutex.Lock()
|
|
defer tm.mutex.Unlock()
|
|
|
|
tm.accessToken = token
|
|
tm.accessTokenTTL = accessTokenTTL
|
|
tm.accessTokenMaxTTL = accessTokenMaxTTL
|
|
|
|
tm.newAccessTokenNotificationChan <- true
|
|
}
|
|
|
|
func (tm *AgentManager) GetToken() string {
|
|
tm.mutex.Lock()
|
|
defer tm.mutex.Unlock()
|
|
|
|
return tm.accessToken
|
|
}
|
|
|
|
func (tm *AgentManager) FetchUniversalAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, e error) {
|
|
|
|
var universalAuthConfig UniversalAuth
|
|
if err := ParseAuthConfig(tm.authConfigBytes, &universalAuthConfig); err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err)
|
|
}
|
|
|
|
clientID, err := util.GetEnvVarOrFileContent(util.INFISICAL_UNIVERSAL_AUTH_CLIENT_ID_NAME, universalAuthConfig.ClientIDPath)
|
|
if err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get client id: %v", err)
|
|
}
|
|
|
|
clientSecret, err := util.GetEnvVarOrFileContent("INFISICAL_UNIVERSAL_CLIENT_SECRET", universalAuthConfig.ClientSecretPath)
|
|
if err != nil {
|
|
if len(tm.cachedUniversalAuthClientSecret) == 0 {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get client secret: %v", err)
|
|
}
|
|
clientSecret = tm.cachedUniversalAuthClientSecret
|
|
}
|
|
|
|
tm.cachedUniversalAuthClientSecret = clientSecret
|
|
if universalAuthConfig.RemoveClientSecretOnRead {
|
|
defer os.Remove(universalAuthConfig.ClientSecretPath)
|
|
}
|
|
|
|
return tm.infisicalClient.Auth().UniversalAuthLogin(clientID, clientSecret)
|
|
|
|
}
|
|
|
|
func (tm *AgentManager) FetchKubernetesAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) {
|
|
|
|
var kubernetesAuthConfig KubernetesAuth
|
|
if err := ParseAuthConfig(tm.authConfigBytes, &kubernetesAuthConfig); err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err)
|
|
}
|
|
|
|
identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, kubernetesAuthConfig.IdentityID)
|
|
if err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
|
|
}
|
|
|
|
serviceAccountTokenPath := os.Getenv(util.INFISICAL_KUBERNETES_SERVICE_ACCOUNT_TOKEN_NAME)
|
|
if serviceAccountTokenPath == "" {
|
|
serviceAccountTokenPath = kubernetesAuthConfig.ServiceAccountToken
|
|
if serviceAccountTokenPath == "" {
|
|
serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
|
|
}
|
|
}
|
|
|
|
return tm.infisicalClient.Auth().KubernetesAuthLogin(identityId, serviceAccountTokenPath)
|
|
|
|
}
|
|
|
|
func (tm *AgentManager) FetchAzureAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) {
|
|
|
|
var azureAuthConfig AzureAuth
|
|
if err := ParseAuthConfig(tm.authConfigBytes, &azureAuthConfig); err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err)
|
|
}
|
|
|
|
identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, azureAuthConfig.IdentityID)
|
|
if err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
|
|
}
|
|
|
|
return tm.infisicalClient.Auth().AzureAuthLogin(identityId, "")
|
|
|
|
}
|
|
|
|
func (tm *AgentManager) FetchGcpIdTokenAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) {
|
|
|
|
var gcpIdTokenAuthConfig GcpIdTokenAuth
|
|
if err := ParseAuthConfig(tm.authConfigBytes, &gcpIdTokenAuthConfig); err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err)
|
|
}
|
|
|
|
identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, gcpIdTokenAuthConfig.IdentityID)
|
|
if err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
|
|
}
|
|
|
|
return tm.infisicalClient.Auth().GcpIdTokenAuthLogin(identityId)
|
|
|
|
}
|
|
|
|
func (tm *AgentManager) FetchGcpIamAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) {
|
|
|
|
var gcpIamAuthConfig GcpIamAuth
|
|
if err := ParseAuthConfig(tm.authConfigBytes, &gcpIamAuthConfig); err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err)
|
|
}
|
|
|
|
identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, gcpIamAuthConfig.IdentityID)
|
|
if err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
|
|
}
|
|
|
|
serviceAccountKeyPath := os.Getenv(util.INFISICAL_GCP_IAM_SERVICE_ACCOUNT_KEY_FILE_PATH_NAME)
|
|
if serviceAccountKeyPath == "" {
|
|
// we don't need to read this file, because the service account key path is directly read inside the sdk
|
|
serviceAccountKeyPath = gcpIamAuthConfig.ServiceAccountKey
|
|
if serviceAccountKeyPath == "" {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("gcp service account key path not found")
|
|
}
|
|
}
|
|
|
|
return tm.infisicalClient.Auth().GcpIamAuthLogin(identityId, serviceAccountKeyPath)
|
|
|
|
}
|
|
|
|
func (tm *AgentManager) FetchAwsIamAuthAccessToken() (credential infisicalSdk.MachineIdentityCredential, err error) {
|
|
|
|
var awsIamAuthConfig AwsIamAuth
|
|
if err := ParseAuthConfig(tm.authConfigBytes, &awsIamAuthConfig); err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to parse auth config due to error: %v", err)
|
|
}
|
|
|
|
identityId, err := util.GetEnvVarOrFileContent(util.INFISICAL_MACHINE_IDENTITY_ID_NAME, awsIamAuthConfig.IdentityID)
|
|
|
|
if err != nil {
|
|
return infisicalSdk.MachineIdentityCredential{}, fmt.Errorf("unable to get identity id: %v", err)
|
|
}
|
|
|
|
return tm.infisicalClient.Auth().AwsIamAuthLogin(identityId)
|
|
|
|
}
|
|
|
|
// Fetches a new access token using client credentials
|
|
func (tm *AgentManager) FetchNewAccessToken() error {
|
|
|
|
authStrategies := map[util.AuthStrategyType]func() (credential infisicalSdk.MachineIdentityCredential, e error){
|
|
util.AuthStrategy.UNIVERSAL_AUTH: tm.FetchUniversalAuthAccessToken,
|
|
util.AuthStrategy.KUBERNETES_AUTH: tm.FetchKubernetesAuthAccessToken,
|
|
util.AuthStrategy.AZURE_AUTH: tm.FetchAzureAuthAccessToken,
|
|
util.AuthStrategy.GCP_ID_TOKEN_AUTH: tm.FetchGcpIdTokenAuthAccessToken,
|
|
util.AuthStrategy.GCP_IAM_AUTH: tm.FetchGcpIamAuthAccessToken,
|
|
util.AuthStrategy.AWS_IAM_AUTH: tm.FetchAwsIamAuthAccessToken,
|
|
}
|
|
|
|
if _, ok := authStrategies[tm.authStrategy]; !ok {
|
|
return fmt.Errorf("auth strategy %s not found", tm.authStrategy)
|
|
}
|
|
|
|
credential, err := authStrategies[tm.authStrategy]()
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accessTokenTTL := time.Duration(credential.ExpiresIn * int64(time.Second))
|
|
accessTokenMaxTTL := time.Duration(credential.AccessTokenMaxTTL * int64(time.Second))
|
|
|
|
if accessTokenTTL <= time.Duration(5)*time.Second {
|
|
util.PrintErrorMessageAndExit("At this time, agent does not support refresh of tokens with 5 seconds or less ttl. Please increase access token ttl and try again")
|
|
}
|
|
|
|
tm.accessTokenFetchedTime = time.Now()
|
|
tm.SetToken(credential.AccessToken, accessTokenTTL, accessTokenMaxTTL)
|
|
|
|
return nil
|
|
}
|
|
|
|
// Refreshes the existing access token
|
|
func (tm *AgentManager) RefreshAccessToken(accessToken string) error {
|
|
httpClient, err := util.GetRestyClientWithCustomHeaders()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpClient.SetRetryCount(10000).
|
|
SetRetryMaxWaitTime(20 * time.Second).
|
|
SetRetryWaitTime(5 * time.Second)
|
|
|
|
response, err := api.CallMachineIdentityRefreshAccessToken(httpClient, api.UniversalAuthRefreshRequest{AccessToken: accessToken})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
accessTokenTTL := time.Duration(response.AccessTokenTTL * int(time.Second))
|
|
accessTokenMaxTTL := time.Duration(response.AccessTokenMaxTTL * int(time.Second))
|
|
tm.accessTokenRefreshedTime = time.Now()
|
|
|
|
tm.SetToken(response.AccessToken, accessTokenTTL, accessTokenMaxTTL)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (tm *AgentManager) ManageTokenLifecycle() {
|
|
for {
|
|
accessTokenMaxTTLExpiresInTime := tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
|
|
accessTokenRefreshedTime := tm.accessTokenRefreshedTime
|
|
|
|
if accessTokenRefreshedTime.IsZero() {
|
|
accessTokenRefreshedTime = tm.accessTokenFetchedTime
|
|
}
|
|
|
|
// Calculate next expiry time at 2/3 of the TTL
|
|
nextAccessTokenExpiresInTime := accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3)
|
|
|
|
if tm.accessTokenFetchedTime.IsZero() && tm.accessTokenRefreshedTime.IsZero() {
|
|
// try to fetch token from sink files first
|
|
// if token is found, refresh the token right away and continue from there
|
|
isSavedTokenValid := false
|
|
token := tm.FetchTokenFromFiles()
|
|
if token != "" {
|
|
log.Info().Msg("found existing token in file, attempting to refresh...")
|
|
err := tm.RefreshAccessToken(token)
|
|
isSavedTokenValid = err == nil
|
|
if isSavedTokenValid {
|
|
log.Info().Msg("token refreshed successfully from saved file")
|
|
tm.accessTokenFetchedTime = time.Now()
|
|
} else {
|
|
log.Error().Msg("unable to refresh token from saved file")
|
|
}
|
|
}
|
|
|
|
if !isSavedTokenValid {
|
|
// case: init login to get access token
|
|
log.Info().Msg("attempting to authenticate...")
|
|
err := tm.FetchNewAccessToken()
|
|
if err != nil {
|
|
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
|
|
|
|
// wait a bit before trying again
|
|
time.Sleep((30 * time.Second))
|
|
continue
|
|
}
|
|
}
|
|
} else if time.Now().After(accessTokenMaxTTLExpiresInTime) {
|
|
// case: token has reached max ttl and we should re-authenticate entirely (cannot refresh)
|
|
log.Info().Msgf("token has reached max ttl, attempting to re authenticate...")
|
|
err := tm.FetchNewAccessToken()
|
|
if err != nil {
|
|
log.Error().Msgf("unable to authenticate because %v. Will retry in 30 seconds", err)
|
|
|
|
// wait a bit before trying again
|
|
time.Sleep((30 * time.Second))
|
|
continue
|
|
}
|
|
} else {
|
|
// case: token ttl has expired, but the token is still within max ttl, so we can refresh
|
|
log.Info().Msgf("attempting to refresh existing token...")
|
|
err := tm.RefreshAccessToken(tm.GetToken())
|
|
if err != nil {
|
|
log.Error().Msgf("unable to refresh token because %v. Will retry in 30 seconds", err)
|
|
|
|
// wait a bit before trying again
|
|
time.Sleep((30 * time.Second))
|
|
continue
|
|
}
|
|
}
|
|
|
|
if tm.exitAfterAuth {
|
|
time.Sleep(25 * time.Second)
|
|
os.Exit(0)
|
|
}
|
|
|
|
if accessTokenRefreshedTime.IsZero() {
|
|
accessTokenRefreshedTime = tm.accessTokenFetchedTime
|
|
} else {
|
|
accessTokenRefreshedTime = tm.accessTokenRefreshedTime
|
|
}
|
|
|
|
// Recalculate next expiry time at 2/3 of the TTL
|
|
nextAccessTokenExpiresInTime = accessTokenRefreshedTime.Add(tm.accessTokenTTL * 2 / 3)
|
|
accessTokenMaxTTLExpiresInTime = tm.accessTokenFetchedTime.Add(tm.accessTokenMaxTTL - (5 * time.Second))
|
|
|
|
if nextAccessTokenExpiresInTime.After(accessTokenMaxTTLExpiresInTime) {
|
|
// case: Refreshed so close that the next refresh would occur beyond max ttl
|
|
// Sleep until we're at 2/3 of the remaining time to max TTL
|
|
remainingTime := accessTokenMaxTTLExpiresInTime.Sub(time.Now())
|
|
time.Sleep(remainingTime * 2 / 3)
|
|
} else {
|
|
// Sleep until we're at 2/3 of the TTL
|
|
time.Sleep(tm.accessTokenTTL * 2 / 3)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tm *AgentManager) WriteTokenToFiles() {
|
|
token := tm.GetToken()
|
|
for _, sinkFile := range tm.filePaths {
|
|
if sinkFile.Type == "file" {
|
|
err := ioutil.WriteFile(sinkFile.Config.Path, []byte(token), 0644)
|
|
if err != nil {
|
|
log.Error().Msgf("unable to write file sink to path '%s' because %v", sinkFile.Config.Path, err)
|
|
}
|
|
|
|
log.Info().Msgf("new access token saved to file at path '%s'", sinkFile.Config.Path)
|
|
|
|
} else {
|
|
log.Error().Msg("unsupported sink type. Only 'file' type is supported")
|
|
}
|
|
}
|
|
}
|
|
|
|
func (tm *AgentManager) FetchTokenFromFiles() string {
|
|
for _, sinkFile := range tm.filePaths {
|
|
if sinkFile.Type == "file" {
|
|
tokenBytes, err := ioutil.ReadFile(sinkFile.Config.Path)
|
|
if err != nil {
|
|
log.Debug().Msgf("unable to read token from file '%s' because %v", sinkFile.Config.Path, err)
|
|
continue
|
|
}
|
|
|
|
token := string(tokenBytes)
|
|
if token != "" {
|
|
return token
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func (tm *AgentManager) WriteTemplateToFile(bytes *bytes.Buffer, template *Template) {
|
|
if err := WriteBytesToFile(bytes, template.DestinationPath); err != nil {
|
|
log.Error().Msgf("template engine: unable to write secrets to path because %s. Will try again on next cycle", err)
|
|
return
|
|
}
|
|
log.Info().Msgf("template engine: secret template at path %s has been rendered and saved to path %s", template.SourcePath, template.DestinationPath)
|
|
}
|
|
|
|
func (tm *AgentManager) MonitorSecretChanges(secretTemplate Template, templateId int, sigChan chan os.Signal) {
|
|
|
|
pollingInterval := time.Duration(5 * time.Minute)
|
|
|
|
if secretTemplate.Config.PollingInterval != "" {
|
|
interval, err := util.ConvertPollingIntervalToTime(secretTemplate.Config.PollingInterval)
|
|
|
|
if err != nil {
|
|
log.Error().Msgf("unable to convert polling interval to time because %v", err)
|
|
sigChan <- syscall.SIGINT
|
|
return
|
|
|
|
} else {
|
|
pollingInterval = interval
|
|
}
|
|
}
|
|
|
|
var existingEtag string
|
|
var currentEtag string
|
|
var firstRun = true
|
|
|
|
execTimeout := secretTemplate.Config.Execute.Timeout
|
|
execCommand := secretTemplate.Config.Execute.Command
|
|
|
|
for {
|
|
select {
|
|
case <-sigChan:
|
|
return
|
|
default:
|
|
{
|
|
tm.dynamicSecretLeases.Prune()
|
|
token := tm.GetToken()
|
|
if token != "" {
|
|
var processedTemplate *bytes.Buffer
|
|
var err error
|
|
|
|
if secretTemplate.SourcePath != "" {
|
|
processedTemplate, err = ProcessTemplate(templateId, secretTemplate.SourcePath, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
|
} else if secretTemplate.TemplateContent != "" {
|
|
processedTemplate, err = ProcessLiteralTemplate(templateId, secretTemplate.TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
|
} else {
|
|
processedTemplate, err = ProcessBase64Template(templateId, secretTemplate.Base64TemplateContent, nil, token, existingEtag, ¤tEtag, tm.dynamicSecretLeases)
|
|
}
|
|
|
|
if err != nil {
|
|
log.Error().Msgf("unable to process template because %v", err)
|
|
|
|
// case: if exit-after-auth is true, it should exit the agent once an error on secret fetching occurs with the appropriate exit code (1)
|
|
// previous behavior would exit after 25 sec with status code 0, even if this step errors
|
|
if tm.exitAfterAuth {
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
if (existingEtag != currentEtag) || firstRun {
|
|
|
|
tm.WriteTemplateToFile(processedTemplate, &secretTemplate)
|
|
existingEtag = currentEtag
|
|
|
|
if !firstRun && execCommand != "" {
|
|
log.Info().Msgf("executing command: %s", execCommand)
|
|
err := ExecuteCommandWithTimeout(execCommand, execTimeout)
|
|
|
|
if err != nil {
|
|
log.Error().Msgf("unable to execute command because %v", err)
|
|
}
|
|
|
|
}
|
|
if firstRun {
|
|
firstRun = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// now the idea is we pick the next sleep time in which the one shorter out of
|
|
// - polling time
|
|
// - first lease that's gonna get expired in the template
|
|
firstLeaseExpiry, isValid := tm.dynamicSecretLeases.GetFirstExpiringLeaseTime(templateId)
|
|
var waitTime = pollingInterval
|
|
if isValid && firstLeaseExpiry.Sub(time.Now()) < pollingInterval {
|
|
waitTime = firstLeaseExpiry.Sub(time.Now())
|
|
}
|
|
time.Sleep(waitTime)
|
|
} else {
|
|
// It fails to get the access token. So we will re-try in 3 seconds. We do this because if we don't, the user will have to wait for the next polling interval to get the first secret render.
|
|
time.Sleep(3 * time.Second)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// runCmd represents the run command
|
|
var agentCmd = &cobra.Command{
|
|
Example: `
|
|
infisical agent
|
|
`,
|
|
Use: "agent",
|
|
Short: "Used to launch a client daemon that streamlines authentication and secret retrieval processes in various environments",
|
|
DisableFlagsInUseLine: true,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
|
|
log.Info().Msg("starting Infisical agent...")
|
|
|
|
configPath, err := cmd.Flags().GetString("config")
|
|
if err != nil {
|
|
util.HandleError(err, "Unable to parse flag config")
|
|
}
|
|
|
|
var agentConfigInBytes []byte
|
|
|
|
agentConfigInBase64 := os.Getenv("INFISICAL_AGENT_CONFIG_BASE64")
|
|
|
|
if agentConfigInBase64 == "" {
|
|
data, err := ioutil.ReadFile(configPath)
|
|
if err != nil {
|
|
if !FileExists(configPath) {
|
|
log.Error().Msgf("Unable to locate %s. The provided agent config file path is either missing or incorrect", configPath)
|
|
return
|
|
}
|
|
}
|
|
agentConfigInBytes = data
|
|
}
|
|
|
|
if agentConfigInBase64 != "" {
|
|
decodedAgentConfig, err := base64.StdEncoding.DecodeString(agentConfigInBase64)
|
|
if err != nil {
|
|
log.Error().Msgf("Unable to decode base64 config file because %v", err)
|
|
return
|
|
}
|
|
|
|
agentConfigInBytes = decodedAgentConfig
|
|
}
|
|
|
|
if !FileExists(configPath) && agentConfigInBase64 == "" {
|
|
log.Error().Msgf("No agent config file provided at %v. Please provide a agent config file", configPath)
|
|
return
|
|
}
|
|
|
|
agentConfig, err := ParseAgentConfig(agentConfigInBytes)
|
|
if err != nil {
|
|
log.Error().Msgf("Unable to prase %s because %v. Please ensure that is follows the Infisical Agent config structure", configPath, err)
|
|
return
|
|
}
|
|
|
|
authMethodValid, authStrategy := util.IsAuthMethodValid(agentConfig.Auth.Type, false)
|
|
|
|
if !authMethodValid {
|
|
util.PrintErrorMessageAndExit(fmt.Sprintf("The auth method '%s' is not supported.", agentConfig.Auth.Type))
|
|
}
|
|
|
|
tokenRefreshNotifier := make(chan bool)
|
|
sigChan := make(chan os.Signal, 1)
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
|
|
|
filePaths := agentConfig.Sinks
|
|
|
|
configBytes, err := yaml.Marshal(agentConfig.Auth.Config)
|
|
if err != nil {
|
|
log.Error().Msgf("unable to marshal auth config because %v", err)
|
|
return
|
|
}
|
|
|
|
tm := NewAgentManager(NewAgentMangerOptions{
|
|
FileDeposits: filePaths,
|
|
Templates: agentConfig.Templates,
|
|
AuthConfigBytes: configBytes,
|
|
NewAccessTokenNotificationChan: tokenRefreshNotifier,
|
|
ExitAfterAuth: agentConfig.Infisical.ExitAfterAuth,
|
|
AuthStrategy: authStrategy,
|
|
})
|
|
|
|
tm.dynamicSecretLeases = NewDynamicSecretLeaseManager(sigChan)
|
|
|
|
go tm.ManageTokenLifecycle()
|
|
|
|
for i, template := range agentConfig.Templates {
|
|
log.Info().Msgf("template engine started for template %v...", i+1)
|
|
go tm.MonitorSecretChanges(template, i, sigChan)
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-tokenRefreshNotifier:
|
|
go tm.WriteTokenToFiles()
|
|
case <-sigChan:
|
|
log.Info().Msg("agent is gracefully shutting...")
|
|
// TODO: check if we are in the middle of writing files to disk
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
agentCmd.SetHelpFunc(func(command *cobra.Command, strings []string) {
|
|
command.Flags().MarkHidden("domain")
|
|
command.Parent().HelpFunc()(command, strings)
|
|
})
|
|
agentCmd.Flags().String("config", "agent-config.yaml", "The path to agent config yaml file")
|
|
rootCmd.AddCommand(agentCmd)
|
|
}
|