Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions infisical/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package infisical

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"sync"
"time"
)

const tokenExpiryBuffer = 5 * time.Second

type kmsEncryptDecrypter interface {
encrypt(plaintext string) (string, error)
decrypt(ciphertext string) (string, error)
}

type kmsClient struct {
httpClient *http.Client
baseURL string
kmsKeyID string
clientID string
clientSecret string

mu sync.RWMutex
token string
expiresAt time.Time
}

func newKmsClient(siteURL, kmsKeyID, clientID, clientSecret string) *kmsClient {
base := strings.TrimRight(siteURL, "/")
if !strings.HasSuffix(base, "/api") {
base += "/api"
}
return &kmsClient{
httpClient: &http.Client{Timeout: 30 * time.Second},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does Infisical support TLSConfig? Or can we enforce an "https" URL check to ensure that clientSecret is not passed as plaintext over the wire

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll update the code to add HTTPS enforcement.

baseURL: base,
kmsKeyID: kmsKeyID,
clientID: clientID,
clientSecret: clientSecret,
}
}

type loginRequest struct {
ClientID string `json:"clientId"`
ClientSecret string `json:"clientSecret"`
}

type loginResponse struct {
AccessToken string `json:"accessToken"`
ExpiresIn int64 `json:"expiresIn"`
}

func (c *kmsClient) login() error {
body, err := json.Marshal(loginRequest{
ClientID: c.clientID,
ClientSecret: c.clientSecret,
})
if err != nil {
return fmt.Errorf("infisical-kms: failed to marshal login request: %w", err)
}

resp, err := c.httpClient.Post(
c.baseURL+"/v1/auth/universal-auth/login",
"application/json",
bytes.NewReader(body),
)
if err != nil {
return fmt.Errorf("infisical-kms: login request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("infisical-kms: login returned %d: %s", resp.StatusCode, msg)
}

var result loginResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("infisical-kms: failed to decode login response: %w", err)
}

c.mu.Lock()
c.token = result.AccessToken
c.expiresAt = time.Now().Add(time.Duration(result.ExpiresIn)*time.Second - tokenExpiryBuffer)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We wont renew the token if it is revoked before this expiry time. Can we check for 401 Unauthorized errors in doKmsRequest and renew the token?

@victorvhs017 victorvhs017 May 14, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we get a 401 or 403 error (also used for expired tokens in Infisical API), we'll log in again and retry once.

c.mu.Unlock()

return nil
}

func (c *kmsClient) ensureToken() error {
c.mu.RLock()
valid := c.token != "" && time.Now().Before(c.expiresAt)
c.mu.RUnlock()
if valid {
return nil
}
return c.login()
}

func (c *kmsClient) doKmsRequest(path string, reqBody, respBody interface{}) error {
if err := c.ensureToken(); err != nil {
return err
}

body, err := json.Marshal(reqBody)
if err != nil {
return fmt.Errorf("infisical-kms: failed to marshal request: %w", err)
}

req, err := http.NewRequest(http.MethodPost, c.baseURL+path, bytes.NewReader(body))
if err != nil {
return fmt.Errorf("infisical-kms: failed to create request: %w", err)
}
req.Header.Set("Content-Type", "application/json")

c.mu.RLock()
req.Header.Set("Authorization", "Bearer "+c.token)
c.mu.RUnlock()

resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("infisical-kms: request failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
msg, _ := ioutil.ReadAll(resp.Body)
return fmt.Errorf("infisical-kms: request returned %d: %s", resp.StatusCode, msg)
}

if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
return fmt.Errorf("infisical-kms: failed to decode response: %w", err)
}
return nil
}

type encryptRequest struct {
Plaintext string `json:"plaintext"`
}

type encryptResponse struct {
Ciphertext string `json:"ciphertext"`
}

func (c *kmsClient) encrypt(plaintext string) (string, error) {
var resp encryptResponse
path := fmt.Sprintf("/v1/kms/keys/%s/encrypt", c.kmsKeyID)
encoded := base64.StdEncoding.EncodeToString([]byte(plaintext))
if err := c.doKmsRequest(path, encryptRequest{Plaintext: encoded}, &resp); err != nil {
return "", err
}
return resp.Ciphertext, nil
}

type decryptRequest struct {
Ciphertext string `json:"ciphertext"`
}

type decryptResponse struct {
Plaintext string `json:"plaintext"`
}

func (c *kmsClient) decrypt(ciphertext string) (string, error) {
var resp decryptResponse
path := fmt.Sprintf("/v1/kms/keys/%s/decrypt", c.kmsKeyID)
if err := c.doKmsRequest(path, decryptRequest{Ciphertext: ciphertext}, &resp); err != nil {
return "", err
}
decoded, err := base64.StdEncoding.DecodeString(resp.Plaintext)
if err != nil {
return "", fmt.Errorf("infisical-kms: failed to base64-decode plaintext: %w", err)
}
return string(decoded), nil
}
216 changes: 216 additions & 0 deletions infisical/infisical_kms.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package infisical

import (
"encoding/json"
"errors"
"fmt"
"os"

"github.com/libopenstorage/secrets"
"github.com/libopenstorage/secrets/pkg/store"
"github.com/portworx/kvdb"
"github.com/sirupsen/logrus"
)

const (
// Name of the secret store
Name = secrets.TypeInfisical
// SiteURLKey is the base URL of the Infisical instance
SiteURLKey = "INFISICAL_SITE_URL"
// ClientIDKey is the Infisical Universal Auth machine identity client ID
ClientIDKey = "INFISICAL_UNIVERSAL_AUTH_CLIENT_ID"
// ClientSecretKey is the Infisical Universal Auth machine identity client secret
ClientSecretKey = "INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET"
// KMSKeyIDKey is the ID of the Infisical KMS key used for encrypt/decrypt
KMSKeyIDKey = "INFISICAL_KMS_KEY_ID"
// KvdbKey is used to setup Infisical KMS with kvdb for persistence
KvdbKey = "KMS_KVDB"
defaultSiteURL = "https://app.infisical.com"
kvdbPublicBasePath = "infisical/secrets/public/"
kvdbDataBasePath = "infisical/secrets/data/"
)

var (
// ErrKvdbNotProvided is returned when a valid kvdb instance is not provided
ErrKvdbNotProvided = errors.New("a valid kvdb.Kvdb instance must be provided via the KMS_KVDB config key")
// ErrClientIDRequired is returned when INFISICAL_UNIVERSAL_AUTH_CLIENT_ID is not set
ErrClientIDRequired = errors.New("INFISICAL_UNIVERSAL_AUTH_CLIENT_ID is required (config key or env var)")
// ErrClientSecretRequired is returned when INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET is not set
ErrClientSecretRequired = errors.New("INFISICAL_UNIVERSAL_AUTH_CLIENT_SECRET is required (config key or env var)")
// ErrKMSKeyIDRequired is returned when INFISICAL_KMS_KEY_ID is not set
ErrKMSKeyIDRequired = errors.New("INFISICAL_KMS_KEY_ID is required (config key or env var)")
)

type infisicalKms struct {
client kmsEncryptDecrypter
ps store.PersistenceStore
}

func New(
secretConfig map[string]interface{},
) (secrets.Secrets, error) {
v, ok := secretConfig[KvdbKey]
if !ok {
return nil, ErrKvdbNotProvided
}
kv, ok := v.(kvdb.Kvdb)
if !ok {
return nil, ErrKvdbNotProvided
}
ps := store.NewKvdbPersistenceStore(kv, kvdbPublicBasePath, kvdbDataBasePath)

siteURL := configString(secretConfig, SiteURLKey, defaultSiteURL)
clientID := configString(secretConfig, ClientIDKey, "")
clientSecret := configString(secretConfig, ClientSecretKey, "")
kmsKeyID := configString(secretConfig, KMSKeyIDKey, "")

if clientID == "" {
return nil, ErrClientIDRequired
}
if clientSecret == "" {
return nil, ErrClientSecretRequired
}
if kmsKeyID == "" {
return nil, ErrKMSKeyIDRequired
}

client := newKmsClient(siteURL, kmsKeyID, clientID, clientSecret)
if err := client.login(); err != nil {
return nil, fmt.Errorf("infisical-kms: authentication failed: %w", err)
}

logrus.WithFields(logrus.Fields{
"site": siteURL,
"kmsKeyID": kmsKeyID,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is a KMS Key ID and is it ok to log it? Is it some sort of a Master Key ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This KMS Key ID is not a master key, it's the ID handle that identifies the key on the Infisical KMS server.

I'm dropping this to keep the same pattern used in other providers.

}).Info("infisical-kms: authenticated successfully")

return &infisicalKms{
client: client,
ps: ps,
}, nil
}

func (k *infisicalKms) String() string {
return Name
}

func (k *infisicalKms) GetSecret(
secretId string,
keyContext map[string]string,
) (map[string]interface{}, secrets.Version, error) {
if secretId == "" {
return nil, secrets.NoVersion, secrets.ErrEmptySecretId
}

exists, err := k.ps.Exists(secretId)
if err != nil {
return nil, secrets.NoVersion, err
}
if !exists {
return nil, secrets.NoVersion, secrets.ErrInvalidSecretId
}

ciphertextBytes, err := k.ps.GetPublic(secretId)
if err != nil {
return nil, secrets.NoVersion, err
}

plaintext, err := k.client.decrypt(string(ciphertextBytes))
if err != nil {
return nil, secrets.NoVersion, fmt.Errorf("infisical-kms: decryption failed: %w", err)
}

result := make(map[string]interface{})
if err := json.Unmarshal([]byte(plaintext), &result); err != nil {
return nil, secrets.NoVersion, fmt.Errorf("infisical-kms: failed to unmarshal decrypted data: %w", err)
}

return result, secrets.NoVersion, nil
}

func (k *infisicalKms) PutSecret(
secretId string,
plainText map[string]interface{},
keyContext map[string]string,
) (secrets.Version, error) {
if secretId == "" {
return secrets.NoVersion, secrets.ErrEmptySecretId
}
if len(plainText) == 0 {
return secrets.NoVersion, secrets.ErrEmptySecretData
}

_, override := keyContext[secrets.OverwriteSecretDataInStore]

jsonBytes, err := json.Marshal(plainText)
if err != nil {
return secrets.NoVersion, fmt.Errorf("infisical-kms: failed to marshal secret data: %w", err)
}

ciphertext, err := k.client.encrypt(string(jsonBytes))
if err != nil {
return secrets.NoVersion, fmt.Errorf("infisical-kms: encryption failed: %w", err)
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do you envision the use of Infisical with this library? The callers of this library in certain cases also expect a KMS provider to store "secret data". I see that Infisical supports that, so in such a case you wont encrypt and return back the ciphertext but instead simply store this "secret data" in the KMS provider.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Infisical indeed also has a secrets management product. However, for the purposes of Portworx, we are currently only looking to integrate our KMS product. If there's demand for integrating secrets management, we can absolutely look into that in the future.


return secrets.NoVersion, k.ps.Set(secretId, []byte(ciphertext), nil, nil, override)
}

func (k *infisicalKms) DeleteSecret(
secretId string,
keyContext map[string]string,
) error {
if secretId == "" {
return secrets.ErrEmptySecretId
}
return k.ps.Delete(secretId)
}

func (k *infisicalKms) ListSecrets() ([]string, error) {
return k.ps.List()
}

func (k *infisicalKms) Encrypt(
secretId string,
plaintTextData string,
keyContext map[string]string,
) (string, error) {
return "", secrets.ErrNotSupported
}

func (k *infisicalKms) Decrypt(
secretId string,
encryptedData string,
keyContext map[string]string,
) (string, error) {
return "", secrets.ErrNotSupported
}

func (k *infisicalKms) Rencrypt(
originalSecretId string,
newSecretId string,
originalKeyContext map[string]string,
newKeyContext map[string]string,
encryptedData string,
) (string, error) {
return "", secrets.ErrNotSupported
}

func configString(config map[string]interface{}, key, fallback string) string {
if config != nil {
if v, ok := config[key]; ok {
if s, ok := v.(string); ok && s != "" {
return s
}
}
}
if env := os.Getenv(key); env != "" {
return env
}
return fallback
}

func init() {
if err := secrets.Register(Name, New); err != nil {
panic(err.Error())
}
}
Loading