1
0
mirror of https://github.com/Infisical/infisical.git synced 2025-03-24 00:15:26 +00:00

Compare commits

..

31 Commits

Author SHA1 Message Date
f5a0641671 Add requirement for ssh issue credentials command to include either outFilePath or addToAgent flag 2024-12-19 11:55:44 -08:00
f97f98b2c3 Add ability to load retrieved ssh credentials into ssh agent with new addToAgent flag 2024-12-18 23:27:11 -08:00
89848a2f5c Merge pull request from Infisical/ssh-cli
CLI - SSH Capabilities
2024-12-18 14:12:06 -08:00
1936f7cc4f Merge pull request from Infisical/ssh-certs
Add OpenSSH dependency to standalone Dockerfiles
2024-12-18 11:55:46 -08:00
058475fc3f Ran go mod tidy 2024-12-18 11:45:09 -08:00
ee4eb7f84b Update Go SDK version dependency 2024-12-18 11:32:09 -08:00
8122433f5c Merge pull request from Infisical/ssh-certs
Expose ssh endpoints to api reference, update ssh sign/issue endpoints
2024-12-18 14:02:10 -05:00
f3cf1a3f50 Merge pull request from Infisical/ssh-certs
Infisical SSH (SSH Certificates)
2024-12-18 12:40:24 -05:00
b4b417658f Update ssh cli api error messages 2024-12-18 09:22:06 -08:00
e70ca57510 update minor version 2024-12-18 10:48:11 -05:00
06f321e4bf Update Chart.yaml 2024-12-18 10:44:51 -05:00
3c3fcd0db8 update k8 version to 7.7 2024-12-18 10:44:37 -05:00
21eb2bed7e Merge pull request from Infisical/daniel/k8-push-secret
feat(k8-operator): push secrets
2024-12-18 00:26:55 -05:00
31a21a432d Update isValidKeyAlgorithm impl 2024-12-17 20:40:03 -08:00
7eb05afe2a misc: better condition naming 2024-12-18 04:09:44 +01:00
0b54948b15 fix: better condition error 2024-12-18 04:09:44 +01:00
39e598e408 fix: move fixes from different branch 2024-12-18 04:09:44 +01:00
b735618601 fix(k8-operator): resource-based finalizer names 2024-12-18 04:09:44 +01:00
3a5e862def fix(k8-operator): helm and cleanup 2024-12-18 04:09:44 +01:00
d1c4a9c75a updated env slugs 2024-12-18 03:28:32 +01:00
5532844ee7 Added RBAC 2024-12-18 03:28:32 +01:00
dd5aab973f Update PROJECT 2024-12-18 03:28:32 +01:00
ced12baf5d Update conditions.go 2024-12-18 03:28:32 +01:00
7db1e62654 fix: requested changes 2024-12-18 03:28:32 +01:00
0ab3ae442e cleanup and resource seperation 2024-12-18 03:28:32 +01:00
ed9472efc8 remove print 2024-12-18 03:28:32 +01:00
e094844601 docs(k8-operator): push secrets 2024-12-18 03:28:32 +01:00
e761b49964 feat(k8-operator): push secrets 2024-12-18 03:28:32 +01:00
765280eef6 Update ssh cli issue/sign according to review 2024-12-17 13:12:47 -08:00
be36827392 Finish ssh cli sign/issue commands 2024-12-16 17:42:11 -08:00
097512c691 Begin adding ssh commands to cli 2024-12-15 17:30:57 -08:00
37 changed files with 3951 additions and 459 deletions

@ -10,7 +10,7 @@ require (
github.com/fatih/semgroup v1.2.0
github.com/gitleaks/go-gitdiff v0.8.0
github.com/h2non/filetype v1.1.3
github.com/infisical/go-sdk v0.4.3
github.com/infisical/go-sdk v0.4.7
github.com/mattn/go-isatty v0.0.20
github.com/muesli/ansi v0.0.0-20221106050444-61f0cd9a192a
github.com/muesli/mango-cobra v1.2.0
@ -23,8 +23,8 @@ require (
github.com/spf13/cobra v1.6.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.9.0
golang.org/x/crypto v0.25.0
golang.org/x/term v0.22.0
golang.org/x/crypto v0.31.0
golang.org/x/term v0.27.0
gopkg.in/yaml.v2 v2.4.0
)
@ -93,9 +93,9 @@ require (
go.opentelemetry.io/otel/trace v1.24.0 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/oauth2 v0.21.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/api v0.188.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect

@ -265,8 +265,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/infisical/go-sdk v0.4.3 h1:O5ZJ2eCBAZDE9PIAfBPq9Utb2CgQKrhmj9R0oFTRu4U=
github.com/infisical/go-sdk v0.4.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
github.com/infisical/go-sdk v0.4.7 h1:+cxIdDfciMh0Syxbxbqjhvz9/ShnN1equ2zqlVQYGtw=
github.com/infisical/go-sdk v0.4.7/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M=
github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
@ -453,8 +453,8 @@ golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30=
golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@ -564,8 +564,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -620,16 +620,16 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk=
golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -643,8 +643,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

609
cli/packages/cmd/ssh.go Normal file

@ -0,0 +1,609 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"context"
"fmt"
"net"
"os"
"path/filepath"
"strings"
"time"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/config"
"github.com/Infisical/infisical-merge/packages/util"
infisicalSdk "github.com/infisical/go-sdk"
infisicalSdkUtil "github.com/infisical/go-sdk/packages/util"
"github.com/spf13/cobra"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
var sshCmd = &cobra.Command{
Example: `infisical ssh`,
Short: "Used to issue SSH credentials",
Use: "ssh",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
}
var sshIssueCredentialsCmd = &cobra.Command{
Example: `ssh issue-credentials`,
Short: "Used to issue SSH credentials against a certificate template",
Use: "issue-credentials",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: issueCredentials,
}
var sshSignKeyCmd = &cobra.Command{
Example: `ssh sign-key`,
Short: "Used to sign a SSH public key against a certificate template",
Use: "sign-key",
DisableFlagsInUseLine: true,
Args: cobra.NoArgs,
Run: signKey,
}
var algoToFileName = map[infisicalSdkUtil.CertKeyAlgorithm]string{
infisicalSdkUtil.RSA2048: "id_rsa_2048",
infisicalSdkUtil.RSA4096: "id_rsa_4096",
infisicalSdkUtil.ECDSAP256: "id_ecdsa_p256",
infisicalSdkUtil.ECDSAP384: "id_ecdsa_p384",
}
func isValidKeyAlgorithm(algo infisicalSdkUtil.CertKeyAlgorithm) bool {
_, exists := algoToFileName[algo]
return exists
}
func isValidCertType(certType infisicalSdkUtil.SshCertType) bool {
switch certType {
case infisicalSdkUtil.UserCert, infisicalSdkUtil.HostCert:
return true
default:
return false
}
}
func writeToFile(filePath string, content string, perm os.FileMode) error {
// Ensure the directory exists
dir := filepath.Dir(filePath)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
// Write the content to the file
err := os.WriteFile(filePath, []byte(content), perm)
if err != nil {
return fmt.Errorf("failed to write to file %s: %w", filePath, err)
}
return nil
}
func addCredentialsToAgent(privateKeyContent, certContent string) error {
// Parse the private key
privateKey, err := ssh.ParseRawPrivateKey([]byte(privateKeyContent))
if err != nil {
return fmt.Errorf("failed to parse private key: %w", err)
}
// Parse the certificate
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(certContent))
if err != nil {
return fmt.Errorf("failed to parse certificate: %w", err)
}
cert, ok := pubKey.(*ssh.Certificate)
if !ok {
return fmt.Errorf("parsed key is not a certificate")
}
// Calculate LifetimeSecs based on certificate's valid-to time
validUntil := time.Unix(int64(cert.ValidBefore), 0)
now := time.Now()
// Handle ValidBefore as either a timestamp or an enumeration
// SSH certificates use ValidBefore as a timestamp unless set to 0 or ~0
if cert.ValidBefore == ssh.CertTimeInfinity {
// If certificate never expires, set default lifetime to 1 year (can adjust as needed)
validUntil = now.Add(365 * 24 * time.Hour)
}
// Calculate the duration until expiration
lifetime := validUntil.Sub(now)
if lifetime <= 0 {
return fmt.Errorf("certificate is already expired")
}
// Convert duration to seconds
lifetimeSecs := uint32(lifetime.Seconds())
// Connect to the SSH agent
socket := os.Getenv("SSH_AUTH_SOCK")
if socket == "" {
return fmt.Errorf("SSH_AUTH_SOCK not set")
}
conn, err := net.Dial("unix", socket)
if err != nil {
return fmt.Errorf("failed to connect to SSH agent: %w", err)
}
defer conn.Close()
agentClient := agent.NewClient(conn)
// Add the key with certificate to the agent
err = agentClient.Add(agent.AddedKey{
PrivateKey: privateKey,
Certificate: cert,
Comment: "Added via Infisical CLI",
LifetimeSecs: lifetimeSecs,
})
if err != nil {
return fmt.Errorf("failed to add key to agent: %w", err)
}
return nil
}
func issueCredentials(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
var infisicalToken string
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
infisicalToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
}
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if certificateTemplateId == "" {
util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag")
}
principalsStr, err := cmd.Flags().GetString("principals")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
// Check if the input string is empty before splitting
if principalsStr == "" {
util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty")
}
// Convert the comma-delimited string into a slice of strings
principals := strings.Split(principalsStr, ",")
for i, principal := range principals {
principals[i] = strings.TrimSpace(principal)
}
keyAlgorithm, err := cmd.Flags().GetString("keyAlgorithm")
if err != nil {
util.HandleError(err, "Unable to parse keyAlgorithm flag")
}
if !isValidKeyAlgorithm(infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)) {
util.HandleError(fmt.Errorf("invalid keyAlgorithm: %s", keyAlgorithm),
"Valid values: RSA_2048, RSA_4096, EC_prime256v1, EC_secp384r1")
}
certType, err := cmd.Flags().GetString("certType")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) {
util.HandleError(fmt.Errorf("invalid certType: %s", certType),
"Valid values: user, host")
}
ttl, err := cmd.Flags().GetString("ttl")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
keyId, err := cmd.Flags().GetString("keyId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
outFilePath, err := cmd.Flags().GetString("outFilePath")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
addToAgent, err := cmd.Flags().GetBool("addToAgent")
if err != nil {
util.HandleError(err, "Unable to parse addToAgent flag")
}
if outFilePath == "" && addToAgent == false {
util.PrintErrorMessageAndExit("You must provide either --outFilePath or --addToAgent flag to use this command")
}
var (
outputDir string
privateKeyPath string
publicKeyPath string
signedKeyPath string
)
if outFilePath != "" {
// Expand ~ to home directory if present
if strings.HasPrefix(outFilePath, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
util.HandleError(err, "Failed to resolve home directory")
}
outFilePath = strings.Replace(outFilePath, "~", homeDir, 1)
}
// Check if outFilePath ends with "-cert.pub"
if strings.HasSuffix(outFilePath, "-cert.pub") {
// Treat outFilePath as the signed key path
signedKeyPath = outFilePath
// Derive the base name by removing "-cert.pub"
baseName := strings.TrimSuffix(filepath.Base(outFilePath), "-cert.pub")
// Set the output directory
outputDir = filepath.Dir(outFilePath)
// Define private and public key paths
privateKeyPath = filepath.Join(outputDir, baseName)
publicKeyPath = filepath.Join(outputDir, baseName+".pub")
} else {
// Treat outFilePath as a directory
outputDir = outFilePath
// Check if the directory exists; if not, create it
info, err := os.Stat(outputDir)
if os.IsNotExist(err) {
err = os.MkdirAll(outputDir, 0755)
if err != nil {
util.HandleError(err, "Failed to create output directory")
}
} else if err != nil {
util.HandleError(err, "Failed to access output directory")
} else if !info.IsDir() {
util.PrintErrorMessageAndExit("The provided --outFilePath is not a directory")
}
}
}
// Define file names based on key algorithm
fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]
// Define file paths
privateKeyPath = filepath.Join(outputDir, fileName)
publicKeyPath = filepath.Join(outputDir, fileName+".pub")
signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub")
// If outFilePath ends with "-cert.pub", ensure the signedKeyPath is set
if strings.HasSuffix(outFilePath, "-cert.pub") {
// Ensure the signedKeyPath was set
if signedKeyPath == "" {
util.HandleError(fmt.Errorf("signedKeyPath is not set correctly"), "Internal error")
}
} else {
// Ensure all paths are set
if privateKeyPath == "" || publicKeyPath == "" || signedKeyPath == "" {
util.HandleError(fmt.Errorf("file paths are not set correctly"), "Internal error")
}
}
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
AutoTokenRefresh: false,
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
creds, err := infisicalClient.Ssh().IssueCredentials(infisicalSdk.IssueSshCredsOptions{
CertificateTemplateID: certificateTemplateId,
Principals: principals,
KeyAlgorithm: infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm),
CertType: infisicalSdkUtil.SshCertType(certType),
TTL: ttl,
KeyID: keyId,
})
if err != nil {
util.HandleError(err, "Failed to issue SSH credentials")
}
if outFilePath != "" {
// If signedKeyPath wasn't set in the directory scenario, set it now
if signedKeyPath == "" {
fileName := algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)]
signedKeyPath = filepath.Join(outputDir, fileName+"-cert.pub")
}
if privateKeyPath == "" {
privateKeyPath = filepath.Join(outputDir, algoToFileName[infisicalSdkUtil.CertKeyAlgorithm(keyAlgorithm)])
}
err = writeToFile(privateKeyPath, creds.PrivateKey, 0600)
if err != nil {
util.HandleError(err, "Failed to write Private Key to file")
}
if publicKeyPath == "" {
publicKeyPath = privateKeyPath + ".pub"
}
err = writeToFile(publicKeyPath, creds.PublicKey, 0644)
if err != nil {
util.HandleError(err, "Failed to write Public Key to file")
}
err = writeToFile(signedKeyPath, creds.SignedKey, 0644)
if err != nil {
util.HandleError(err, "Failed to write Signed Key to file")
}
fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
}
// Add SSH credentials to the SSH agent if needed
if addToAgent {
// Call the helper function to handle add-to-agent flow
err := addCredentialsToAgent(creds.PrivateKey, creds.SignedKey)
if err != nil {
util.HandleError(err, "Failed to add keys to SSH agent")
} else {
fmt.Println("The SSH key and certificate have been successfully added to your ssh-agent.")
}
}
}
func signKey(cmd *cobra.Command, args []string) {
token, err := util.GetInfisicalToken(cmd)
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
var infisicalToken string
if token != nil && (token.Type == util.SERVICE_TOKEN_IDENTIFIER || token.Type == util.UNIVERSAL_AUTH_TOKEN_IDENTIFIER) {
infisicalToken = token.Token
} else {
util.RequireLogin()
util.RequireLocalWorkspaceFile()
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails(true)
if err != nil {
util.HandleError(err, "Unable to authenticate")
}
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
infisicalToken = loggedInUserDetails.UserCredentials.JTWToken
}
certificateTemplateId, err := cmd.Flags().GetString("certificateTemplateId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if certificateTemplateId == "" {
util.PrintErrorMessageAndExit("You must set the --certificateTemplateId flag")
}
publicKey, err := cmd.Flags().GetString("publicKey")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
publicKeyFilePath, err := cmd.Flags().GetString("publicKeyFilePath")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if publicKey == "" && publicKeyFilePath == "" {
util.HandleError(fmt.Errorf("either --publicKey or --publicKeyFilePath must be provided"), "Invalid input")
}
if publicKey != "" && publicKeyFilePath != "" {
util.HandleError(fmt.Errorf("only one of --publicKey or --publicKeyFile can be provided"), "Invalid input")
}
if publicKeyFilePath != "" {
if strings.HasPrefix(publicKeyFilePath, "~") {
// Expand the tilde (~) to the user's home directory
homeDir, err := os.UserHomeDir()
if err != nil {
util.HandleError(err, "Failed to resolve home directory")
}
publicKeyFilePath = strings.Replace(publicKeyFilePath, "~", homeDir, 1)
}
// Ensure the file has a .pub extension
if !strings.HasSuffix(publicKeyFilePath, ".pub") {
util.HandleError(fmt.Errorf("public key file must have a .pub extension"), "Invalid input")
}
content, err := os.ReadFile(publicKeyFilePath)
if err != nil {
util.HandleError(err, "Failed to read public key file")
}
publicKey = strings.TrimSpace(string(content))
}
if strings.TrimSpace(publicKey) == "" {
util.HandleError(fmt.Errorf("Public key is empty"), "Invalid input")
}
principalsStr, err := cmd.Flags().GetString("principals")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
// Check if the input string is empty before splitting
if principalsStr == "" {
util.HandleError(fmt.Errorf("no principals provided"), "The 'principals' flag cannot be empty")
}
// Convert the comma-delimited string into a slice of strings
principals := strings.Split(principalsStr, ",")
for i, principal := range principals {
principals[i] = strings.TrimSpace(principal)
}
certType, err := cmd.Flags().GetString("certType")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if !isValidCertType(infisicalSdkUtil.SshCertType(certType)) {
util.HandleError(fmt.Errorf("invalid certType: %s", certType),
"Valid values: user, host")
}
ttl, err := cmd.Flags().GetString("ttl")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
keyId, err := cmd.Flags().GetString("keyId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
outFilePath, err := cmd.Flags().GetString("outFilePath")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
var (
outputDir string
signedKeyPath string
)
if outFilePath == "" {
// Use current working directory
if err != nil {
util.HandleError(err, "Failed to get current working directory")
}
// check if public key path exists
if publicKeyFilePath == "" {
util.PrintErrorMessageAndExit("--outFilePath must be specified when --publicKeyFilePath is not provided")
}
outputDir = filepath.Dir(publicKeyFilePath)
// Derive the base name by removing "-cert.pub"
baseName := strings.TrimSuffix(filepath.Base(publicKeyFilePath), ".pub")
signedKeyPath = filepath.Join(outputDir, baseName+"-cert.pub")
} else {
// Expand ~ to home directory if present
if strings.HasPrefix(outFilePath, "~") {
homeDir, err := os.UserHomeDir()
if err != nil {
util.HandleError(err, "Failed to resolve home directory")
}
outFilePath = strings.Replace(outFilePath, "~", homeDir, 1)
}
// Check if outFilePath ends with "-cert.pub"
if !strings.HasSuffix(outFilePath, "-cert.pub") {
util.PrintErrorMessageAndExit("--outFilePath must end with -cert.pub")
}
// Extract the directory from outFilePath
outputDir = filepath.Dir(outFilePath)
// Validate the output directory
info, err := os.Stat(outputDir)
if os.IsNotExist(err) {
// Directory does not exist; attempt to create it
err = os.MkdirAll(outputDir, 0755)
if err != nil {
util.HandleError(err, "Failed to create output directory")
}
} else if err != nil {
// Other errors accessing the directory
util.HandleError(err, "Failed to access output directory")
} else if !info.IsDir() {
// Path exists but is not a directory
util.PrintErrorMessageAndExit("The provided --outFilePath's directory is not valid")
}
signedKeyPath = outFilePath
}
infisicalClient := infisicalSdk.NewInfisicalClient(context.Background(), infisicalSdk.Config{
SiteUrl: config.INFISICAL_URL,
UserAgent: api.USER_AGENT,
AutoTokenRefresh: false,
})
infisicalClient.Auth().SetAccessToken(infisicalToken)
creds, err := infisicalClient.Ssh().SignKey(infisicalSdk.SignSshPublicKeyOptions{
CertificateTemplateID: certificateTemplateId,
PublicKey: publicKey,
Principals: principals,
CertType: infisicalSdkUtil.SshCertType(certType),
TTL: ttl,
KeyID: keyId,
})
if err != nil {
util.HandleError(err, "Failed to sign SSH public key")
}
err = writeToFile(signedKeyPath, creds.SignedKey, 0644)
if err != nil {
util.HandleError(err, "Failed to write Signed Key to file")
}
fmt.Println("Successfully wrote SSH certificate to:", signedKeyPath)
}
func init() {
sshSignKeyCmd.Flags().String("token", "", "Issue SSH certificate using machine identity access token")
sshSignKeyCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue the SSH certificate for")
sshSignKeyCmd.Flags().String("publicKey", "", "The public key to sign")
sshSignKeyCmd.Flags().String("publicKeyFilePath", "", "The file path to the public key file to sign")
sshSignKeyCmd.Flags().String("outFilePath", "", "The path to write the SSH certificate to such as ~/.ssh/id_rsa-cert.pub. If not provided, the credentials will be saved to the directory of the specified public key file path or the current working directory")
sshSignKeyCmd.Flags().String("principals", "", "The principals that the certificate should be signed for")
sshSignKeyCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type for the created certificate")
sshSignKeyCmd.Flags().String("ttl", "", "The ttl for the created certificate")
sshSignKeyCmd.Flags().String("keyId", "", "The keyId that the created certificate should have")
sshCmd.AddCommand(sshSignKeyCmd)
sshIssueCredentialsCmd.Flags().String("token", "", "Issue SSH credentials using machine identity access token")
sshIssueCredentialsCmd.Flags().String("certificateTemplateId", "", "The ID of the SSH certificate template to issue SSH credentials for")
sshIssueCredentialsCmd.Flags().String("principals", "", "The principals to issue SSH credentials for")
sshIssueCredentialsCmd.Flags().String("keyAlgorithm", string(infisicalSdkUtil.RSA2048), "The key algorithm to issue SSH credentials for")
sshIssueCredentialsCmd.Flags().String("certType", string(infisicalSdkUtil.UserCert), "The cert type to issue SSH credentials for")
sshIssueCredentialsCmd.Flags().String("ttl", "", "The ttl to issue SSH credentials for")
sshIssueCredentialsCmd.Flags().String("keyId", "", "The keyId to issue SSH credentials for")
sshIssueCredentialsCmd.Flags().String("outFilePath", "", "The path to write the SSH credentials to such as ~/.ssh, ./some_folder, ./some_folder/id_rsa-cert.pub. If not provided, the credentials will be saved to the current working directory")
sshIssueCredentialsCmd.Flags().Bool("addToAgent", false, "Whether to add issued SSH credentials to the SSH agent")
sshCmd.AddCommand(sshIssueCredentialsCmd)
rootCmd.AddCommand(sshCmd)
}

@ -159,7 +159,19 @@ as part of the SSH operation.
## Guide to Using Infisical SSH to Access a Host
We show how to obtain a SSH certificate (and optionally a new SSH key pair) for a client to access a host via CLI:
We show how to obtain a SSH certificate and use it for a client to access a host via CLI:
<Note>
The subsequent guide assumes the following prerequisites:
- SSH Agent is running: The `ssh-agent` must be actively running on the host machine.
- OpenSSH is installed: The system should have OpenSSH installed; this includes
both the `ssh` client and `ssh-agent`.
- `SSH_AUTH_SOCK` environment variable
is set; the `SSH_AUTH_SOCK` variable should point to the UNIX socket that
`ssh-agent` uses for communication.
</Note>
<Steps>
<Step title="Authenticate with Infisical">
@ -169,70 +181,25 @@ infisical login
```
</Step>
<Step title="Obtain a SSH certificate (and optionally new key-pair)">
Depending on the use-case, a client may either request a SSH certificate along with a new SSH key pair or obtain a SSH certificate for an existing SSH key pair to access a host.
<Step title="Obtain a SSH certificate and load it into the SSH agent">
Run the `infisical ssh issue-credentials` command, specifying the `--addToAgent` flag to automatically load the SSH certificate into the SSH agent.
```bash
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username> --addToAgent
```
<AccordionGroup>
<Accordion title="Using New Key Pair (Recommended)">
If you wish to obtain a new SSH key pair in conjunction with the SSH certificate, then you can use the `infisical ssh issue-credentials` command.
Here's some guidance on each flag:
```bash
infisical ssh issue-credentials --certificateTemplateId=<certificate-template-id> --principals=<username>
```
The following flags may be relevant:
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
- `outFilePath` (optional): The path to the file to write the SSH certificate to.
<Note>
If `outFilePath` is not specified, the SSH certificate will be written to the current working directory where the command is run.
</Note>
</Accordion>
<Accordion title="Using Existing Key Pair">
If you have an existing SSH key pair, then you can use the `infisical ssh sign-key` command with either
the `--publicKey` flag or the `--publicKeyFilePath` flag to obtain a SSH certificate corresponding to
the existing credential.
```bash
infisical ssh sign-key --publicKeyFilePath=<public-key-file-path> --certificateTemplateId=<certificate-template-id> --principals=<username>
```
The following flags may be relevant:
- `publicKey`: The public key to sign.
- `publicKeyFilePath`: The path to the public key file to sign.
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
- `outFilePath` (optional): The path to the file to write the SSH certificate to.
<Note>
If `outFilePath` is not specified but `publicKeyFilePath` is then the SSH certificate will be written to the directory of the public key file; if the public key file is called `id_rsa.pub`, then the file containing the SSH certificate will be called `id_rsa-cert.pub`.
Otherwise, if `outFilePath` is not specified, the SSH certificate will be written to the current working directory where the command is run.
</Note>
</Accordion>
</AccordionGroup>
- `certificateTemplateId`: The ID of the certificate template to use for issuing the SSH certificate.
- `principals`: The comma-delimited username(s) or hostname(s) to include in the SSH certificate.
</Step>
<Step title="SSH into the host">
Once you have obtained the SSH certificate, you can use it to SSH into the desired host.
Finally, SSH into the desired host; the SSH operation will be performed using the SSH certificate loaded into the SSH agent.
```bash
ssh -i /path/to/private_key.pem \
-o CertificateFile=/path/to/ssh-cert.pub \
username@hostname
ssh username@hostname
```
<Note>
We recommend setting up aliases so you can more easily SSH into the desired host.
For example, you may set up an SSH alias using the SSH client configuration file (usually `~/.ssh/config`), defining a host alias including the file path to the issued SSH credential(s).
</Note>
</Step>
</Steps>

@ -831,9 +831,9 @@ type: Opaque
</Accordion>
### Apply the Infisical CRD to your cluster
### Apply the InfisicalSecret CRD to your cluster
Once you have configured the Infisical CRD with the required fields, you can apply it to your cluster.
Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster.
After applying, you should notice that the managed secret has been created in the desired namespace your specified.
```
@ -1032,12 +1032,12 @@ stringData:
-----END CERTIFICATE-----
```
## Auto redeployment
### Auto redeployment
Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed.
To address this, we added functionality to automatically redeploy your deployment when its managed secret updates.
### Enabling auto redeploy
#### Enabling auto redeploy
To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret
@ -1080,6 +1080,419 @@ spec:
When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update.
Then, for each deployment that has this annotation present, a rolling update will be triggered.
</Info>
## Push Secrets to Infisical
### Example usage
Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical.
After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster.
Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD.
```bash
kubectl apply -f source-secret.yaml
```
After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD.
```bash
kubectl apply -f infisical-push-secret.yaml
```
After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical.
```yaml infisical-push-secret.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalPushSecret
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
hostAPI: https://app.infisical.com/api
# Optional, defaults to no replacement.
updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync.
# Optional, defaults to no deletion.
deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted.
destination:
projectId: <project-id>
environmentSlug: <env-slug>
secretsPath: <secret-path>
push:
secret:
secretName: push-secret-demo # Secret CRD
secretNamespace: default
# Only have one authentication method defined or you are likely to run into authentication issues.
# Remove all except one authentication method.
authentication:
awsIamAuth:
identityId: <machine-identity-id>
azureAuth:
identityId: <machine-identity-id>
gcpIamAuth:
identityId: <machine-identity-id>
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
gcpIdTokenAuth:
identityId: <machine-identity-id>
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <secret-name>
namespace: <secret-namespace>
universalAuth:
credentialsRef:
secretName: <secret-name> # universal-auth-credentials
secretNamespace: <secret-namespace> # default
```
```yaml source-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: push-secret-demo
namespace: default
stringData: # can also be "data", but needs to be base64 encoded
API_KEY: some-api-key
DATABASE_URL: postgres://127.0.0.1:5432
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
```
### InfisicalPushSecret CRD properties
<Accordion title="hostAPI">
If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to
` https://your-self-hosted-instace.com/api`
When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud.
<Accordion title="Advanced use case">
If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet.
To achieve this, use the following address for the hostAPI field:
``` bash
http://<backend-svc-name>.<namespace>.svc.cluster.local:4000/api
```
Make sure to replace `<backend-svc-name>` and `<namespace>` with the appropriate values for your backend service and namespace.
</Accordion>
</Accordion>
<Accordion title="resyncInterval">
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
The following units are supported:
- `s` for seconds (must be at least 5 seconds)
- `m` for minutes
- `h` for hours
- `d` for days
- `w` for weeks
The default value is `1m` (1 minute).
Valid intervals examples:
```yaml
resyncInterval: 5s # 10 seconds
resyncInterval: 10s # 10 seconds
resyncInterval: 5m # 5 minutes
resyncInterval: 1h # 1 hour
resyncInterval: 1d # 1 day
```
</Accordion>
<Accordion title="updatePolicy">
The field is optional and will default to `None` if not defined.
The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical.
Valid values are `None` and `Replace`.
Behavior of each policy:
- `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator.
- `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value.
```yaml
spec:
updatePolicy: Replace
```
</Accordion>
<Accordion title="deletionPolicy">
This field is optional and will default to `None` if not defined.
The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted.
Valid values are `None` and `Delete`.
Behavior of each policy:
- `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted.
- `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted.
```yaml
spec:
deletionPolicy: Delete
```
</Accordion>
<Accordion title="destination">
The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`.
```yaml
spec:
destination:
projectId: <project-id>
environmentSlug: <env-slug>
secretsPath: <secrets-path>
```
<Accordion title="destination.projectId">
The project ID where you want to create the secrets in Infisical.
</Accordion>
<Accordion title="destination.environmentSlug">
The environment slug where you want to create the secrets in Infisical.
</Accordion>
<Accordion title="destination.secretsPath">
The path where you want to create the secrets in Infisical. The root path is `/`.
</Accordion>
</Accordion>
<Accordion title="push">
The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below.
<Accordion title="secret">
The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`.
Example usage of the `push.secret` field:
```yaml infisical-push-secret.yaml
push:
secret:
secretName: push-secret-demo
secretNamespace: default
```
```yaml push-secret-demo.yaml
apiVersion: v1
kind: Secret
metadata:
name: push-secret-demo
namespace: default
# Pass in the secrets you wish to push to Infisical
stringData:
API_KEY: some-api-key
DATABASE_URL: postgres://127.0.0.1:5432
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab
```
</Accordion>
</Accordion>
<Accordion title="authentication">
The `authentication` field dictates which authentication method to use when pushing secrets to Infisical.
The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`.
<Accordion title="universalAuth">
The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider.
[Read more about Universal Auth](/documentation/platform/identities/universal-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token.
- `credentialsRef.secretName`: The name of the Kubernetes secret.
- `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret.
Example:
```yaml
# infisical-push-secret.yaml
spec:
universalAuth:
credentialsRef:
secretName: <secret-name>
secretNamespace: <secret-namespace>
```
```yaml
# machine-identity-credentials.yaml
apiVersion: v1
kind: Secret
metadata:
name: universal-auth-credentials
type: Opaque
stringData:
clientId: <machine-identity-client-id>
clientSecret: <machine-identity-client-secret>
```
</Accordion>
<Accordion title="kubernetesAuth">
The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment.
[Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical.
- `serviceAccountRef.name`: The name of the service account.
- `serviceAccountRef.namespace`: The namespace of the service account.
Example:
```yaml
spec:
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <secret-name>
namespace: <secret-namespace>
```
</Accordion>
<Accordion title="awsIamAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical.
[Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
awsIamAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="azureAuth">
The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment.
[Read more about Azure Auth](/documentation/platform/identities/azure-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
authentication:
azureAuth:
identityId: <machine-identity-id>
```
</Accordion>
<Accordion title="gcpIamAuth">
The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
- `serviceAccountKeyFilePath`: The path to the GCP service account key file.
Example:
```yaml
spec:
gcpIamAuth:
identityId: <machine-identity-id>
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
```
</Accordion>
<Accordion title="gcpIdTokenAuth">
The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments.
[Read more about Azure Auth](/documentation/platform/identities/gcp-auth).
Valid fields:
- `identityId`: The identity ID of the machine identity you created.
Example:
```yaml
spec:
gcpIdTokenAuth:
identityId: <machine-identity-id>
```
</Accordion>
</Accordion>
<Accordion title="tls">
This block defines the TLS settings to use for connecting to the Infisical
instance.
Fields:
<Accordion title="caRef">
This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Valid fields:
- `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
- `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS.
Example:
```yaml
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
```
</Accordion>
</Accordion>
### Applying the InfisicalPushSecret CRD to your cluster
Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster.
After applying, you should notice that the secrets have been pushed to Infisical.
```bash
kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field
kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself
```
### Connecting to instances with private/self-signed certificate
To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalPushSecret` CRD
to point to a CA certificate stored in a Kubernetes secret resource.
```yaml
spec:
hostAPI: https://app.infisical.com/api
resyncInterval: 30s
tls:
caRef:
secretName: custom-ca-certificate
secretNamespace: default
key: ca.crt
authentication:
# ...
```
## Global configuration
To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap.

@ -13,9 +13,9 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: v0.7.7
version: v0.8.0
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
# It is recommended to use it with quotes.
appVersion: "v0.7.7"
appVersion: "v0.8.0"

@ -0,0 +1,268 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: infisicalpushsecrets.secrets.infisical.com
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
labels:
{{- include "secrets-operator.labels" . | nindent 4 }}
spec:
group: secrets.infisical.com
names:
kind: InfisicalPushSecret
listKind: InfisicalPushSecretList
plural: infisicalpushsecrets
singular: infisicalpushsecret
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: InfisicalPushSecret is the Schema for the infisicalpushsecrets
API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
properties:
authentication:
properties:
awsIamAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
azureAuth:
properties:
identityId:
type: string
resource:
type: string
required:
- identityId
type: object
gcpIamAuth:
properties:
identityId:
type: string
serviceAccountKeyFilePath:
type: string
required:
- identityId
- serviceAccountKeyFilePath
type: object
gcpIdTokenAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
kubernetesAuth:
properties:
identityId:
type: string
serviceAccountRef:
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
required:
- identityId
- serviceAccountRef
type: object
universalAuth:
description: PushSecretUniversalAuth defines universal authentication
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret
is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- credentialsRef
type: object
type: object
deletionPolicy:
type: string
destination:
properties:
environmentSlug:
type: string
projectId:
type: string
secretsPath:
type: string
required:
- environmentSlug
- projectId
- secretsPath
type: object
hostAPI:
description: Infisical host to pull secrets from
type: string
push:
properties:
secret:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- secret
type: object
resyncInterval:
type: string
tls:
properties:
caRef:
description: Reference to secret containing CA cert
properties:
key:
description: The name of the secret property with the CA certificate
value
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The namespace where the Kubernetes Secret is located
type: string
required:
- key
- secretName
- secretNamespace
type: object
type: object
updatePolicy:
type: string
required:
- destination
- push
- resyncInterval
type: object
status:
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a foo's
current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating details
about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers of
specific condition types may define expected values and meanings
for this field, and whether the values are considered a guaranteed
API. The value should be a CamelCase string. This field may
not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
managedSecrets:
additionalProperties:
type: string
description: managed secrets is a map where the key is the ID, and the
value is the secret key (string[id], string[key] )
type: object
required:
- conditions
- managedSecrets
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []

@ -51,6 +51,32 @@ rules:
- list
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets/finalizers
verbs:
- update
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets/status
verbs:
- get
- patch
- update
- apiGroups:
- secrets.infisical.com
resources:

@ -32,7 +32,7 @@ controllerManager:
- ALL
image:
repository: infisical/kubernetes-operator
tag: v0.7.7
tag: v0.8.0
resources:
limits:
cpu: 500m

@ -1,16 +1,29 @@
# Code generated by tool. DO NOT EDIT.
# This file is used to track the info used to scaffold your project
# and allow the plugins properly work.
# More info: https://book.kubebuilder.io/reference/project-config.html
domain: infisical.com
layout:
- go.kubebuilder.io/v3
- go.kubebuilder.io/v3
projectName: k8-operator
repo: github.com/Infisical/infisical/k8-operator
resources:
- api:
crdVersion: v1
namespaced: true
controller: true
domain: infisical.com
group: secrets
kind: InfisicalSecret
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: infisical.com
group: secrets
kind: InfisicalSecret
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
version: v1alpha1
- api:
crdVersion: v1
namespaced: true
controller: true
domain: infisical.com
group: secrets
kind: InfisicalPushSecretSecret
path: github.com/Infisical/infisical/k8-operator/api/v1alpha1
version: v1alpha1
version: "3"

@ -0,0 +1,140 @@
package v1alpha1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type InfisicalPushSecretDestination struct {
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
SecretsPath string `json:"secretsPath"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
EnvironmentSlug string `json:"environmentSlug"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
ProjectID string `json:"projectId"`
}
type PushSecretTlsConfig struct {
// Reference to secret containing CA cert
// +kubebuilder:validation:Optional
CaRef CaReference `json:"caRef,omitempty"`
}
// PushSecretUniversalAuth defines universal authentication
type PushSecretUniversalAuth struct {
// +kubebuilder:validation:Required
CredentialsRef KubeSecretReference `json:"credentialsRef"`
}
type PushSecretAwsIamAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
}
type PushSecretAzureAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Optional
Resource string `json:"resource,omitempty"`
}
type PushSecretGcpIdTokenAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
}
type PushSecretGcpIamAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required
ServiceAccountKeyFilePath string `json:"serviceAccountKeyFilePath"`
}
type PushSecretKubernetesAuth struct {
// +kubebuilder:validation:Required
IdentityID string `json:"identityId"`
// +kubebuilder:validation:Required
ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"`
}
type PushSecretAuthentication struct {
// +kubebuilder:validation:Optional
UniversalAuth PushSecretUniversalAuth `json:"universalAuth,omitempty"`
// +kubebuilder:validation:Optional
KubernetesAuth PushSecretKubernetesAuth `json:"kubernetesAuth,omitempty"`
// +kubebuilder:validation:Optional
AwsIamAuth PushSecretAwsIamAuth `json:"awsIamAuth,omitempty"`
// +kubebuilder:validation:Optional
AzureAuth PushSecretAzureAuth `json:"azureAuth,omitempty"`
// +kubebuilder:validation:Optional
GcpIdTokenAuth PushSecretGcpIdTokenAuth `json:"gcpIdTokenAuth,omitempty"`
// +kubebuilder:validation:Optional
GcpIamAuth PushSecretGcpIamAuth `json:"gcpIamAuth,omitempty"`
}
type SecretPush struct {
// +kubebuilder:validation:Required
Secret KubeSecretReference `json:"secret"`
}
// InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
type InfisicalPushSecretSpec struct {
// +kubebuilder:validation:Optional
UpdatePolicy string `json:"updatePolicy"`
// +kubebuilder:validation:Optional
DeletionPolicy string `json:"deletionPolicy"`
// +kubebuilder:validation:Required
// +kubebuilder:validation:Immutable
Destination InfisicalPushSecretDestination `json:"destination"`
// +kubebuilder:validation:Optional
Authentication PushSecretAuthentication `json:"authentication"`
// +kubebuilder:validation:Required
Push SecretPush `json:"push"`
ResyncInterval string `json:"resyncInterval"`
// Infisical host to pull secrets from
// +kubebuilder:validation:Optional
HostAPI string `json:"hostAPI"`
// +kubebuilder:validation:Optional
TLS PushSecretTlsConfig `json:"tls"`
}
// InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret
type InfisicalPushSecretStatus struct {
Conditions []metav1.Condition `json:"conditions"`
// managed secrets is a map where the key is the ID, and the value is the secret key (string[id], string[key] )
ManagedSecrets map[string]string `json:"managedSecrets"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// InfisicalPushSecret is the Schema for the infisicalpushsecrets API
type InfisicalPushSecret struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec InfisicalPushSecretSpec `json:"spec,omitempty"`
Status InfisicalPushSecretStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// InfisicalPushSecretList contains a list of InfisicalPushSecret
type InfisicalPushSecretList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []InfisicalPushSecret `json:"items"`
}
func init() {
SchemeBuilder.Register(&InfisicalPushSecret{}, &InfisicalPushSecretList{})
}

@ -128,6 +128,128 @@ func (in *GcpIamAuthDetails) DeepCopy() *GcpIamAuthDetails {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalPushSecret) DeepCopyInto(out *InfisicalPushSecret) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecret.
func (in *InfisicalPushSecret) DeepCopy() *InfisicalPushSecret {
if in == nil {
return nil
}
out := new(InfisicalPushSecret)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *InfisicalPushSecret) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalPushSecretDestination) DeepCopyInto(out *InfisicalPushSecretDestination) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretDestination.
func (in *InfisicalPushSecretDestination) DeepCopy() *InfisicalPushSecretDestination {
if in == nil {
return nil
}
out := new(InfisicalPushSecretDestination)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalPushSecretList) DeepCopyInto(out *InfisicalPushSecretList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]InfisicalPushSecret, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretList.
func (in *InfisicalPushSecretList) DeepCopy() *InfisicalPushSecretList {
if in == nil {
return nil
}
out := new(InfisicalPushSecretList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *InfisicalPushSecretList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) {
*out = *in
out.Destination = in.Destination
out.Authentication = in.Authentication
out.Push = in.Push
out.TLS = in.TLS
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretSpec.
func (in *InfisicalPushSecretSpec) DeepCopy() *InfisicalPushSecretSpec {
if in == nil {
return nil
}
out := new(InfisicalPushSecretSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalPushSecretStatus) DeepCopyInto(out *InfisicalPushSecretStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
if in.ManagedSecrets != nil {
in, out := &in.ManagedSecrets, &out.ManagedSecrets
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretStatus.
func (in *InfisicalPushSecretStatus) DeepCopy() *InfisicalPushSecretStatus {
if in == nil {
return nil
}
out := new(InfisicalPushSecretStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *InfisicalSecret) DeepCopyInto(out *InfisicalSecret) {
*out = *in
@ -332,6 +454,151 @@ func (in *MangedKubeSecretConfig) DeepCopy() *MangedKubeSecretConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretAuthentication) DeepCopyInto(out *PushSecretAuthentication) {
*out = *in
out.UniversalAuth = in.UniversalAuth
out.KubernetesAuth = in.KubernetesAuth
out.AwsIamAuth = in.AwsIamAuth
out.AzureAuth = in.AzureAuth
out.GcpIdTokenAuth = in.GcpIdTokenAuth
out.GcpIamAuth = in.GcpIamAuth
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAuthentication.
func (in *PushSecretAuthentication) DeepCopy() *PushSecretAuthentication {
if in == nil {
return nil
}
out := new(PushSecretAuthentication)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretAwsIamAuth) DeepCopyInto(out *PushSecretAwsIamAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAwsIamAuth.
func (in *PushSecretAwsIamAuth) DeepCopy() *PushSecretAwsIamAuth {
if in == nil {
return nil
}
out := new(PushSecretAwsIamAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretAzureAuth) DeepCopyInto(out *PushSecretAzureAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAzureAuth.
func (in *PushSecretAzureAuth) DeepCopy() *PushSecretAzureAuth {
if in == nil {
return nil
}
out := new(PushSecretAzureAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretGcpIamAuth) DeepCopyInto(out *PushSecretGcpIamAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretGcpIamAuth.
func (in *PushSecretGcpIamAuth) DeepCopy() *PushSecretGcpIamAuth {
if in == nil {
return nil
}
out := new(PushSecretGcpIamAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretGcpIdTokenAuth) DeepCopyInto(out *PushSecretGcpIdTokenAuth) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretGcpIdTokenAuth.
func (in *PushSecretGcpIdTokenAuth) DeepCopy() *PushSecretGcpIdTokenAuth {
if in == nil {
return nil
}
out := new(PushSecretGcpIdTokenAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretKubernetesAuth) DeepCopyInto(out *PushSecretKubernetesAuth) {
*out = *in
out.ServiceAccountRef = in.ServiceAccountRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretKubernetesAuth.
func (in *PushSecretKubernetesAuth) DeepCopy() *PushSecretKubernetesAuth {
if in == nil {
return nil
}
out := new(PushSecretKubernetesAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretTlsConfig) DeepCopyInto(out *PushSecretTlsConfig) {
*out = *in
out.CaRef = in.CaRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretTlsConfig.
func (in *PushSecretTlsConfig) DeepCopy() *PushSecretTlsConfig {
if in == nil {
return nil
}
out := new(PushSecretTlsConfig)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *PushSecretUniversalAuth) DeepCopyInto(out *PushSecretUniversalAuth) {
*out = *in
out.CredentialsRef = in.CredentialsRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretUniversalAuth.
func (in *PushSecretUniversalAuth) DeepCopy() *PushSecretUniversalAuth {
if in == nil {
return nil
}
out := new(PushSecretUniversalAuth)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretPush) DeepCopyInto(out *SecretPush) {
*out = *in
out.Secret = in.Secret
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush.
func (in *SecretPush) DeepCopy() *SecretPush {
if in == nil {
return nil
}
out := new(SecretPush)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) {
*out = *in

@ -0,0 +1,264 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: infisicalpushsecrets.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: InfisicalPushSecret
listKind: InfisicalPushSecretList
plural: infisicalpushsecrets
singular: infisicalpushsecret
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: InfisicalPushSecret is the Schema for the infisicalpushsecrets
API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
properties:
authentication:
properties:
awsIamAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
azureAuth:
properties:
identityId:
type: string
resource:
type: string
required:
- identityId
type: object
gcpIamAuth:
properties:
identityId:
type: string
serviceAccountKeyFilePath:
type: string
required:
- identityId
- serviceAccountKeyFilePath
type: object
gcpIdTokenAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
kubernetesAuth:
properties:
identityId:
type: string
serviceAccountRef:
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
required:
- identityId
- serviceAccountRef
type: object
universalAuth:
description: PushSecretUniversalAuth defines universal authentication
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret
is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- credentialsRef
type: object
type: object
deletionPolicy:
type: string
destination:
properties:
environmentSlug:
type: string
projectId:
type: string
secretsPath:
type: string
required:
- environmentSlug
- projectId
- secretsPath
type: object
hostAPI:
description: Infisical host to pull secrets from
type: string
push:
properties:
secret:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is
located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- secret
type: object
resyncInterval:
type: string
tls:
properties:
caRef:
description: Reference to secret containing CA cert
properties:
key:
description: The name of the secret property with the CA certificate
value
type: string
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The namespace where the Kubernetes Secret is
located
type: string
required:
- key
- secretName
- secretNamespace
type: object
type: object
updatePolicy:
type: string
required:
- destination
- push
- resyncInterval
type: object
status:
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current
state of this API Resource. --- This struct is intended for direct
use as an array at the field path .status.conditions. For example,
\n type FooStatus struct{ // Represents the observations of a
foo's current state. // Known .status.conditions.type are: \"Available\",
\"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge
// +listType=map // +listMapKey=type Conditions []metav1.Condition
`json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\"
protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition
transitioned from one status to another. This should be when
the underlying condition changed. If that is not known, then
using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating
details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation
that the condition was set based upon. For instance, if .metadata.generation
is currently 12, but the .status.conditions[x].observedGeneration
is 9, the condition is out of date with respect to the current
state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating
the reason for the condition's last transition. Producers
of specific condition types may define expected values and
meanings for this field, and whether the values are considered
a guaranteed API. The value should be a CamelCase string.
This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase.
--- Many .condition.type values are consistent across resources
like Available, but because arbitrary conditions can be useful
(see .node.status.conditions), the ability to deconflict is
important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
managedSecrets:
additionalProperties:
type: string
description: managed secrets is a map where the key is the ID, and
the value is the secret key (string[id], string[key] )
type: object
required:
- conditions
- managedSecrets
type: object
type: object
served: true
storage: true
subresources:
status: {}

@ -2,7 +2,8 @@
# since it depends on service name and namespace that are out of this kustomize package.
# It should be run by config/default
resources:
- bases/secrets.infisical.com_infisicalsecrets.yaml
- bases/secrets.infisical.com_infisicalsecrets.yaml
- bases/secrets.infisical.com_infisicalpushsecrets.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:
@ -18,4 +19,4 @@ patchesStrategicMerge:
# the following config is for teaching kustomize how to do kustomization for CRDs.
configurations:
- kustomizeconfig.yaml
- kustomizeconfig.yaml

@ -0,0 +1,27 @@
# permissions for end users to edit infisicalpushsecrets.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: k8-operator
app.kubernetes.io/managed-by: kustomize
name: infisicalpushsecret-editor-role
rules:
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets/status
verbs:
- get

@ -0,0 +1,23 @@
# permissions for end users to view infisicalpushsecrets.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
labels:
app.kubernetes.io/name: k8-operator
app.kubernetes.io/managed-by: kustomize
name: infisicalpushsecret-viewer-role
rules:
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets
verbs:
- get
- list
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets/status
verbs:
- get

@ -44,6 +44,32 @@ rules:
- list
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets/finalizers
verbs:
- update
- apiGroups:
- secrets.infisical.com
resources:
- infisicalpushsecrets/status
verbs:
- get
- patch
- update
- apiGroups:
- secrets.infisical.com
resources:

@ -0,0 +1,45 @@
apiVersion: secrets.infisical.com/v1alpha1
kind: InfisicalPushSecret
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
hostAPI: https://app.infisical.com/api
# Optional, defaults to replacement.
updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync.
# Optional, defaults to no deletion.
deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted.
destination:
projectId: <project-id>
environmentSlug: <env-slug>
secretsPath: <secret-path>
push:
secret:
secretName: push-secret-demo # Secret CRD
secretNamespace: default
# Only have one authentication method defined or you are likely to run into authentication issues.
# Remove all except one authentication method.
authentication:
awsIamAuth:
identityId: <machine-identity-id>
azureAuth:
identityId: <machine-identity-id>
gcpIamAuth:
identityId: <machine-identity-id>
serviceAccountKeyFilePath: </path-to-service-account-key-file.json>
gcpIdTokenAuth:
identityId: <machine-identity-id>
kubernetesAuth:
identityId: <machine-identity-id>
serviceAccountRef:
name: <secret-name>
namespace: <secret-namespace>
universalAuth:
credentialsRef:
secretName: <secret-name> # universal-auth-credentials
secretNamespace: <secret-namespace> # default

@ -0,0 +1,9 @@
apiVersion: v1
kind: Secret
metadata:
name: push-secret-demo
namespace: default
stringData: # can also be "data", but needs to be base64 encoded
API_KEY: some-api-key
DATABASE_URL: postgres://127.0.0.1:5432
ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab

@ -0,0 +1,156 @@
package controllers
import (
"context"
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (r *InfisicalPushSecretReconciler) SetReconcileStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, err error) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if err != nil {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Reconcile",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: fmt.Sprintf("Reconcile failed, secrets were not pushed to Infisical. Error: %s", err.Error()),
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Reconcile",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "Reconcile succeeded, secrets were pushed to Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToReplaceSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToReplaceSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToReplaceSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors, no secrets failed to be replaced in Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToCreateSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToCreateSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToCreateSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors encountered, no secrets failed to be created in Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToUpdateSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToUpdateSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToUpdateSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors encountered, no secrets failed to be updated in Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToDeleteSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToDeleteSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToDeleteSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors encountered, no secrets failed to be deleted",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetAuthenticatedStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, errorToConditionOn error) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn != nil {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Authenticated",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: "Failed to authenticate with Infisical API. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info",
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Authenticated",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Successfully authenticated with Infisical API",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}

@ -0,0 +1,249 @@
package controllers
import (
"context"
"fmt"
"time"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/handler"
"sigs.k8s.io/controller-runtime/pkg/predicate"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/constants"
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil"
"github.com/Infisical/infisical/k8-operator/packages/util"
"github.com/go-logr/logr"
)
// InfisicalSecretReconciler reconciles a InfisicalSecret object
type InfisicalPushSecretReconciler struct {
client.Client
BaseLogger logr.Logger
Scheme *runtime.Scheme
}
var infisicalPushSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
return r.BaseLogger.WithValues("infisicalpushsecret", req.NamespacedName)
}
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets/finalizers,verbs=update
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := r.GetLogger(req)
var infisicalPushSecretCRD secretsv1alpha1.InfisicalPushSecret
requeueTime := time.Minute // seconds
err := r.Get(ctx, req.NamespacedName, &infisicalPushSecretCRD)
if err != nil {
if errors.IsNotFound(err) {
logger.Info("Infisical Push Secret CRD not found")
return ctrl.Result{
Requeue: false,
}, nil
} else {
logger.Error(err, "Unable to fetch Infisical Secret CRD from cluster")
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
}
// Add finalizer if it doesn't exist
if !controllerutil.ContainsFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) {
controllerutil.AddFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME)
if err := r.Update(ctx, &infisicalPushSecretCRD); err != nil {
return ctrl.Result{}, err
}
}
// Check if it's being deleted
if !infisicalPushSecretCRD.DeletionTimestamp.IsZero() {
logger.Info("Handling deletion of InfisicalPushSecret")
if controllerutil.ContainsFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) {
// We remove finalizers before running deletion logic to be completely safe from stuck resources
infisicalPushSecretCRD.ObjectMeta.Finalizers = []string{}
if err := r.Update(ctx, &infisicalPushSecretCRD); err != nil {
logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalPushSecret %s", infisicalPushSecretCRD.Name))
return ctrl.Result{}, err
}
if err := r.DeleteManagedSecrets(ctx, logger, infisicalPushSecretCRD); err != nil {
return ctrl.Result{}, err // Even if this fails, we still want to delete the CRD
}
}
return ctrl.Result{}, nil
}
if infisicalPushSecretCRD.Spec.ResyncInterval != "" {
duration, err := util.ConvertResyncIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
requeueTime = duration
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
} else {
logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime))
}
// Check if the resource is already marked for deletion
if infisicalPushSecretCRD.GetDeletionTimestamp() != nil {
return ctrl.Result{
Requeue: false,
}, nil
}
// Get modified/default config
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
if infisicalPushSecretCRD.Spec.HostAPI == "" {
api.API_HOST_URL = infisicalConfig["hostAPI"]
} else {
api.API_HOST_URL = infisicalPushSecretCRD.Spec.HostAPI
}
if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" {
api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCRD)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
logger.Info("Using custom CA certificate...")
} else {
api.API_CA_CERTIFICATE = ""
}
err = r.ReconcileInfisicalPushSecret(ctx, logger, infisicalPushSecretCRD)
r.SetReconcileStatusCondition(ctx, &infisicalPushSecretCRD, err)
if err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
// Sync again after the specified time
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
// Custom predicate that allows both spec changes and deletions
specChangeOrDelete := predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
// Only reconcile if spec/generation changed
isSpecOrGenerationChange := e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
if isSpecOrGenerationChange {
if infisicalPushSecretResourceVariablesMap != nil {
if rv, ok := infisicalPushSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
rv.CancelCtx()
delete(infisicalPushSecretResourceVariablesMap, string(e.ObjectNew.GetUID()))
}
}
}
return isSpecOrGenerationChange
},
DeleteFunc: func(e event.DeleteEvent) bool {
// Always reconcile on deletion
if infisicalPushSecretResourceVariablesMap != nil {
if rv, ok := infisicalPushSecretResourceVariablesMap[string(e.Object.GetUID())]; ok {
rv.CancelCtx()
delete(infisicalPushSecretResourceVariablesMap, string(e.Object.GetUID()))
}
}
return true
},
CreateFunc: func(e event.CreateEvent) bool {
// Reconcile on creation
return true
},
GenericFunc: func(e event.GenericEvent) bool {
// Ignore generic events
return false
},
}
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalPushSecret{}, builder.WithPredicates(
specChangeOrDelete,
)).
Watches(
&source.Kind{Type: &corev1.Secret{}},
handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
ctx := context.Background()
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
if err := r.List(ctx, pushSecrets); err != nil {
return []reconcile.Request{}
}
requests := []reconcile.Request{}
for _, pushSecret := range pushSecrets.Items {
if pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pushSecret.GetName(),
Namespace: pushSecret.GetNamespace(),
},
})
}
}
return requests
}),
).
Complete(r)
}

@ -0,0 +1,482 @@
package controllers
import (
"context"
"errors"
"fmt"
"strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/constants"
"github.com/Infisical/infisical/k8-operator/packages/util"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
infisicalSdk "github.com/infisical/go-sdk"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
)
func (r *InfisicalPushSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalPushSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) {
authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){
util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth,
util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth,
util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth,
util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth,
util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth,
util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth,
}
for authStrategy, authHandler := range authStrategies {
authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{
Secret: infisicalSecret,
Type: util.SecretCrd.INFISICAL_PUSH_SECRET,
}, infisicalClient)
if err == nil {
return authDetails, nil
}
if !errors.Is(err, util.ErrAuthNotApplicable) {
return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
}
}
return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided")
}
func (r *InfisicalPushSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalPushSecret) (caCertificate string, err error) {
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalSecret.Spec.TLS.CaRef.SecretNamespace,
Name: infisicalSecret.Spec.TLS.CaRef.SecretName,
})
if k8Errors.IsNotFound(err) {
return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err)
}
if err != nil {
return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err)
}
caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalSecret.Spec.TLS.CaRef.SecretKey])
return caCertificateFromSecret, nil
}
func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret) util.ResourceVariables {
var resourceVariables util.ResourceVariables
if _, ok := infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)]; !ok {
ctx, cancel := context.WithCancel(context.Background())
client := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
SiteUrl: api.API_HOST_URL,
CaCertificate: api.API_CA_CERTIFICATE,
UserAgent: api.USER_AGENT_NAME,
})
infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = util.ResourceVariables{
InfisicalClient: client,
CancelCtx: cancel,
AuthDetails: util.AuthenticationDetails{},
}
resourceVariables = infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)]
} else {
resourceVariables = infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)]
}
return resourceVariables
}
func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariables util.ResourceVariables) {
infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables
}
func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error {
resourceVariables := r.getResourceVariables(infisicalPushSecret)
infisicalClient := resourceVariables.InfisicalClient
cancelCtx := resourceVariables.CancelCtx
authDetails := resourceVariables.AuthDetails
var err error
if authDetails.AuthStrategy == "" {
logger.Info("No authentication strategy found. Attempting to authenticate")
authDetails, err = r.handleAuthentication(ctx, infisicalPushSecret, infisicalClient)
r.SetAuthenticatedStatusCondition(ctx, &infisicalPushSecret, err)
if err != nil {
return fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.updateResourceVariables(infisicalPushSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
})
}
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
})
if err != nil {
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
}
var kubeSecrets = make(map[string]string)
for key, value := range kubePushSecret.Data {
kubeSecrets[key] = string(value)
}
destination := infisicalPushSecret.Spec.Destination
existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
IncludeImports: false,
})
getExistingSecretByKey := func(key string) *infisicalSdk.Secret {
for _, secret := range existingSecrets {
if secret.SecretKey == key {
return &secret
}
}
return nil
}
getExistingSecretById := func(id string) *infisicalSdk.Secret {
for _, secret := range existingSecrets {
if secret.ID == id {
return &secret
}
}
return nil
}
updateExistingSecretByKey := func(key string, newSecretValue string) {
for i := range existingSecrets {
if existingSecrets[i].SecretKey == key {
existingSecrets[i].SecretValue = newSecretValue
break
}
}
}
if err != nil {
return fmt.Errorf("unable to list secrets [err=%s]", err)
}
updatePolicy := infisicalPushSecret.Spec.UpdatePolicy
var secretsFailedToCreate []string
var secretsFailedToUpdate []string
var secretsFailedToDelete []string
var secretsFailedToReplaceById []string
// If the ManagedSecrets are nil, we know this is the first time the InfisicalPushSecret is being reconciled.
if infisicalPushSecret.Status.ManagedSecrets == nil {
infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] )
for secretKey, secretValue := range kubeSecrets {
if exists := getExistingSecretByKey(secretKey); exists != nil {
if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
SecretKey: secretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
NewSecretValue: secretValue,
})
if err != nil {
secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey)
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey
}
} else {
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
SecretKey: secretKey,
SecretValue: secretValue,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToCreate = append(secretsFailedToCreate, secretKey)
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", secretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = secretKey
}
}
} else {
// Loop over all the managed secrets, and find the corresponding existingSecret that has the same ID. If the key doesn't match, delete the secret, and re-create it with the correct key/value
for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
existingSecret := getExistingSecretById(managedSecretId)
if existingSecret != nil {
if existingSecret.SecretKey != managedSecretKey {
// Secret key has changed, lets delete the secret and re-create it with the correct key
logger.Info(fmt.Sprintf("Secret with ID [id=%s] has changed key from [%s] to [%s]. Deleting and re-creating secret", managedSecretId, managedSecretKey, existingSecret.SecretKey))
deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
SecretKey: existingSecret.SecretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey)
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
SecretKey: managedSecretKey,
SecretValue: existingSecret.SecretValue,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey)
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID)
infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = managedSecretKey
}
}
}
// We need to check if any of the secrets have been removed in the new kube secret
for _, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
if _, ok := kubeSecrets[managedSecretKey]; !ok {
// Secret has been removed, verify that the secret is managed by the operator
if getExistingSecretByKey(managedSecretKey) != nil {
logger.Info(fmt.Sprintf("Secret with key [key=%s] has been removed from the kube secret. Deleting secret from Infisical", managedSecretKey))
deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
SecretKey: managedSecretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToDelete = append(secretsFailedToDelete, managedSecretKey)
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID)
}
}
}
// We need to check if any new secrets have been added in the kube secret
for currentSecretKey := range kubeSecrets {
if exists := getExistingSecretByKey(currentSecretKey); exists == nil {
// Some secrets has been added, verify that the secret that has been added is not already managed by the operator
if _, ok := infisicalPushSecret.Status.ManagedSecrets[currentSecretKey]; !ok {
// Secret was not managed by the operator, lets add it
logger.Info(fmt.Sprintf("Secret with key [key=%s] has been added to the kube secret. Creating secret in Infisical", currentSecretKey))
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
SecretKey: currentSecretKey,
SecretValue: kubeSecrets[currentSecretKey],
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToCreate = append(secretsFailedToCreate, currentSecretKey)
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", currentSecretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = currentSecretKey
}
} else {
if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
existingSecret := getExistingSecretByKey(currentSecretKey)
if existingSecret != nil && existingSecret.SecretValue != kubeSecrets[currentSecretKey] {
logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey))
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
SecretKey: currentSecretKey,
NewSecretValue: kubeSecrets[currentSecretKey],
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey)
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err))
continue
}
updateExistingSecretByKey(currentSecretKey, kubeSecrets[currentSecretKey])
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey
}
}
}
}
// Check if any of the existing secrets values have changed
for secretKey, secretValue := range kubeSecrets {
existingSecret := getExistingSecretByKey(secretKey)
if existingSecret != nil {
_, managedByOperator := infisicalPushSecret.Status.ManagedSecrets[existingSecret.ID]
if secretValue != existingSecret.SecretValue {
if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", secretKey))
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
SecretKey: secretKey,
NewSecretValue: secretValue,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey)
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey
}
}
}
}
}
var errorMessage string
if len(secretsFailedToCreate) > 0 {
errorMessage = fmt.Sprintf("Failed to create secrets: [%s]", strings.Join(secretsFailedToCreate, ", "))
} else {
errorMessage = ""
}
r.SetFailedToCreateSecretsStatusCondition(ctx, &infisicalPushSecret, fmt.Sprintf("Failed to create secrets: [%s]", errorMessage))
if len(secretsFailedToUpdate) > 0 {
errorMessage = fmt.Sprintf("Failed to update secrets: [%s]", strings.Join(secretsFailedToUpdate, ", "))
} else {
errorMessage = ""
}
r.SetFailedToUpdateSecretsStatusCondition(ctx, &infisicalPushSecret, fmt.Sprintf("Failed to update secrets: [%s]", errorMessage))
if len(secretsFailedToDelete) > 0 {
errorMessage = fmt.Sprintf("Failed to delete secrets: [%s]", strings.Join(secretsFailedToDelete, ", "))
} else {
errorMessage = ""
}
r.SetFailedToDeleteSecretsStatusCondition(ctx, &infisicalPushSecret, errorMessage)
if len(secretsFailedToReplaceById) > 0 {
errorMessage = fmt.Sprintf("Failed to replace secrets: [%s]", strings.Join(secretsFailedToReplaceById, ", "))
} else {
errorMessage = ""
}
r.SetFailedToReplaceSecretsStatusCondition(ctx, &infisicalPushSecret, errorMessage)
// Update the status of the InfisicalPushSecret
if err := r.Client.Status().Update(ctx, &infisicalPushSecret); err != nil {
return fmt.Errorf("unable to update status of InfisicalPushSecret [err=%s]", err)
}
return nil
}
func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error {
if infisicalPushSecret.Spec.DeletionPolicy != string(constants.PUSH_SECRET_DELETE_POLICY_ENABLED) {
return nil
}
resourceVariables := r.getResourceVariables(infisicalPushSecret)
infisicalClient := resourceVariables.InfisicalClient
destination := infisicalPushSecret.Spec.Destination
existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
IncludeImports: false,
})
if err != nil {
return fmt.Errorf("unable to list secrets [err=%s]", err)
}
existingSecretsMappedById := make(map[string]infisicalSdk.Secret)
for _, secret := range existingSecrets {
existingSecretsMappedById[secret.ID] = secret
}
for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
if _, ok := existingSecretsMappedById[managedSecretId]; ok {
logger.Info(fmt.Sprintf("Deleting secret with key [key=%s]", managedSecretKey))
_, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
SecretKey: managedSecretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
}
}
return nil
}

@ -6,6 +6,8 @@ import (
"sync"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/constants"
"github.com/go-logr/logr"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
@ -15,7 +17,7 @@ import (
const DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX = "secrets.infisical.com/managed-secret"
const AUTO_RELOAD_DEPLOYMENT_ANNOTATION = "secrets.infisical.com/auto-reload" // needs to be set to true for a deployment to start auto redeploying
func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (int, error) {
func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) (int, error) {
listOfDeployments := &v1.DeploymentList{}
err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace})
if err != nil {
@ -42,8 +44,8 @@ func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx c
wg.Add(1)
go func(d v1.Deployment, s corev1.Secret) {
defer wg.Done()
if err := r.ReconcileDeployment(ctx, d, s); err != nil {
fmt.Printf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name)
if err := r.ReconcileDeployment(ctx, logger, d, s); err != nil {
logger.Error(err, fmt.Sprintf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name))
}
}(deployment, *managedKubeSecret)
}
@ -80,17 +82,17 @@ func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1
// This function ensures that a deployment is in sync with a Kubernetes secret by comparing their versions.
// If the version of the secret is different from the version annotation on the deployment, the annotation is updated to trigger a restart of the deployment.
func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, deployment v1.Deployment, secret corev1.Secret) error {
func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, logger logr.Logger, deployment v1.Deployment, secret corev1.Secret) error {
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
annotationValue := secret.Annotations[SECRET_VERSION_ANNOTATION]
annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION]
if deployment.Annotations[annotationKey] == annotationValue &&
deployment.Spec.Template.Annotations[annotationKey] == annotationValue {
fmt.Printf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.\n", deployment.ObjectMeta.Name)
logger.Info(fmt.Sprintf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.", deployment.ObjectMeta.Name))
return nil
}
fmt.Printf("deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]\n", deployment.ObjectMeta.Name)
logger.Info(fmt.Sprintf("Deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]", deployment.ObjectMeta.Name))
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = make(map[string]string)

@ -5,6 +5,8 @@ import (
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/util"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
@ -40,7 +42,7 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.
return r.Client.Status().Update(ctx, infisicalSecret)
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, authStrategy AuthStrategyType, errorToConditionOn error) {
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, authStrategy util.AuthStrategyType, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
@ -63,11 +65,11 @@ func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.C
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for LoadedInfisicalToken")
logger.Error(err, "Could not set condition for LoadedInfisicalToken")
}
}
func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) {
func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
@ -90,6 +92,6 @@ func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx contex
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for AutoRedeployReady")
logger.Error(err, "Could not set condition for AutoRedeployReady")
}
}

@ -15,13 +15,24 @@ import (
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
infisicalSdk "github.com/infisical/go-sdk"
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil"
"github.com/Infisical/infisical/k8-operator/packages/util"
"github.com/go-logr/logr"
)
// InfisicalSecretReconciler reconciles a InfisicalSecret object
type InfisicalSecretReconciler struct {
client.Client
Scheme *runtime.Scheme
BaseLogger logr.Logger
Scheme *runtime.Scheme
}
const FINALIZER_NAME = "secrets.finalizers.infisical.com"
var infisicalSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName)
}
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete
@ -37,29 +48,21 @@ type InfisicalSecretReconciler struct {
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
type ResourceVariables struct {
infisicalClient infisicalSdk.InfisicalClientInterface
cancelCtx context.CancelFunc
authDetails AuthenticationDetails
}
const FINALIZER_NAME = "secrets.finalizers.infisical.com"
// Maps the infisicalSecretCR.UID to a infisicalSdk.InfisicalClientInterface and AuthenticationDetails.
var resourceVariablesMap = make(map[string]ResourceVariables)
func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
var infisicalSecretCR secretsv1alpha1.InfisicalSecret
logger := r.GetLogger(req)
var infisicalSecretCRD secretsv1alpha1.InfisicalSecret
requeueTime := time.Minute // seconds
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCRD)
if err != nil {
if errors.IsNotFound(err) {
return ctrl.Result{
Requeue: false,
}, nil
} else {
fmt.Printf("\nUnable to fetch Infisical Secret CRD from cluster because [err=%v]", err)
logger.Error(err, "unable to fetch Infisical Secret CRD from cluster")
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@ -68,80 +71,82 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// Remove finalizers if they exist. This is to support previous InfisicalSecret CRD's that have finalizers on them.
// In order to delete secrets with finalizers, we first remove the finalizers so we can use the simplified and improved deletion process
if !infisicalSecretCR.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCR.ObjectMeta.Finalizers) > 0 {
infisicalSecretCR.ObjectMeta.Finalizers = []string{}
if err := r.Update(ctx, &infisicalSecretCR); err != nil {
fmt.Printf("Error removing finalizers from Infisical Secret %s: %v\n", infisicalSecretCR.Name, err)
if !infisicalSecretCRD.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCRD.ObjectMeta.Finalizers) > 0 {
infisicalSecretCRD.ObjectMeta.Finalizers = []string{}
if err := r.Update(ctx, &infisicalSecretCRD); err != nil {
logger.Error(err, fmt.Sprintf("Error removing finalizers from Infisical Secret %s", infisicalSecretCRD.Name))
return ctrl.Result{}, err
}
// Our finalizers have been removed, so the reconciler can do nothing.
return ctrl.Result{}, nil
}
if infisicalSecretCR.Spec.ResyncInterval != 0 {
requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval)
fmt.Printf("\nManual re-sync interval set. Interval: %v\n", requeueTime)
if infisicalSecretCRD.Spec.ResyncInterval != 0 {
requeueTime = time.Second * time.Duration(infisicalSecretCRD.Spec.ResyncInterval)
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
} else {
fmt.Printf("\nRe-sync interval set. Interval: %v\n", requeueTime)
logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime))
}
// Check if the resource is already marked for deletion
if infisicalSecretCR.GetDeletionTimestamp() != nil {
if infisicalSecretCRD.GetDeletionTimestamp() != nil {
return ctrl.Result{
Requeue: false,
}, nil
}
// Get modified/default config
infisicalConfig, err := r.GetInfisicalConfigMap(ctx)
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client)
if err != nil {
fmt.Printf("unable to fetch infisical-config [err=%s]. Will requeue after [requeueTime=%v]\n", err, requeueTime)
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
if infisicalSecretCR.Spec.HostAPI == "" {
if infisicalSecretCRD.Spec.HostAPI == "" {
api.API_HOST_URL = infisicalConfig["hostAPI"]
} else {
api.API_HOST_URL = infisicalSecretCR.Spec.HostAPI
api.API_HOST_URL = infisicalSecretCRD.Spec.HostAPI
}
if infisicalSecretCR.Spec.TLS.CaRef.SecretName != "" {
api.API_CA_CERTIFICATE, err = r.GetInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCR)
if infisicalSecretCRD.Spec.TLS.CaRef.SecretName != "" {
api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCRD)
if err != nil {
fmt.Printf("unable to fetch CA certificate [err=%s]. Will requeue after [requeueTime=%v]\n", err, requeueTime)
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
fmt.Println("Using custom CA certificate...")
logger.Info("Using custom CA certificate...")
} else {
api.API_CA_CERTIFICATE = ""
}
err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err)
if err != nil {
fmt.Printf("unable to reconcile Infisical Secret because [err=%v]. Will requeue after [requeueTime=%v]\n", err, requeueTime)
logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, infisicalSecretCR)
r.SetInfisicalAutoRedeploymentReady(ctx, &infisicalSecretCR, numDeployments, err)
numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCRD)
r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err)
if err != nil {
fmt.Printf("unable to reconcile auto redeployment because [err=%v]", err)
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
// Sync again after the specified time
fmt.Printf("Operator will requeue after [%v] \n", requeueTime)
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@ -151,16 +156,20 @@ func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{
UpdateFunc: func(e event.UpdateEvent) bool {
if rv, ok := resourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
rv.cancelCtx()
delete(resourceVariablesMap, string(e.ObjectNew.GetUID()))
if infisicalSecretResourceVariablesMap != nil {
if rv, ok := infisicalSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
rv.CancelCtx()
delete(infisicalSecretResourceVariablesMap, string(e.ObjectNew.GetUID()))
}
}
return true
},
DeleteFunc: func(e event.DeleteEvent) bool {
if rv, ok := resourceVariablesMap[string(e.Object.GetUID())]; ok {
rv.cancelCtx()
delete(resourceVariablesMap, string(e.Object.GetUID()))
if infisicalSecretResourceVariablesMap != nil {
if rv, ok := infisicalSecretResourceVariablesMap[string(e.Object.GetUID())]; ok {
rv.CancelCtx()
delete(infisicalSecretResourceVariablesMap, string(e.Object.GetUID()))
}
}
return true
},

@ -10,8 +10,10 @@ import (
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/constants"
"github.com/Infisical/infisical/k8-operator/packages/model"
"github.com/Infisical/infisical/k8-operator/packages/util"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/types"
@ -20,113 +22,61 @@ import (
k8Errors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey"
const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey"
const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey"
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config"
const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
const INFISICAL_DOMAIN = "https://app.infisical.com/api"
func (r *InfisicalSecretReconciler) HandleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
func (r *InfisicalSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) {
// ? Legacy support, service token auth
infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
}
if infisicalToken != "" {
infisicalClient.Auth().SetAccessToken(infisicalToken)
return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_TOKEN}, nil
return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_TOKEN}, nil
}
// ? Legacy support, service account auth
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
}
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey)
return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_ACCOUNT}, nil
return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_ACCOUNT}, nil
}
authStrategies := map[AuthStrategyType]func(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error){
AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: r.handleUniversalAuth,
AuthStrategy.KUBERNETES_MACHINE_IDENTITY: r.handleKubernetesAuth,
AuthStrategy.AWS_IAM_MACHINE_IDENTITY: r.handleAwsIamAuth,
AuthStrategy.AZURE_MACHINE_IDENTITY: r.handleAzureAuth,
AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: r.handleGcpIdTokenAuth,
AuthStrategy.GCP_IAM_MACHINE_IDENTITY: r.handleGcpIamAuth,
authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){
util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth,
util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth,
util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth,
util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth,
util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth,
util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth,
}
for authStrategy, authHandler := range authStrategies {
authDetails, err := authHandler(ctx, infisicalSecret, infisicalClient)
authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{
Secret: infisicalSecret,
Type: util.SecretCrd.INFISICAL_SECRET,
}, infisicalClient)
if err == nil {
return authDetails, nil
}
if !errors.Is(err, ErrAuthNotApplicable) {
return AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
if !errors.Is(err, util.ErrAuthNotApplicable) {
return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
}
}
return AuthenticationDetails{}, fmt.Errorf("no authentication method provided")
return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided")
}
func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) {
// default key values
defaultConfigMapData := make(map[string]string)
defaultConfigMapData["hostAPI"] = INFISICAL_DOMAIN
kubeConfigMap := &corev1.ConfigMap{}
err := r.Client.Get(ctx, types.NamespacedName{
Namespace: OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE,
Name: OPERATOR_SETTINGS_CONFIGMAP_NAME,
}, kubeConfigMap)
if err != nil {
if k8Errors.IsNotFound(err) {
kubeConfigMap = nil
} else {
return nil, fmt.Errorf("GetConfigMapByNamespacedName: unable to fetch config map in [namespacedName=%s] [err=%s]", OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, err)
}
}
if kubeConfigMap == nil {
return defaultConfigMapData, nil
} else {
for key, value := range defaultConfigMapData {
_, exists := kubeConfigMap.Data[key]
if !exists {
kubeConfigMap.Data[key] = value
}
}
return kubeConfigMap.Data, nil
}
}
func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) {
kubeSecret := &corev1.Secret{}
err := r.Client.Get(ctx, namespacedName, kubeSecret)
if err != nil {
kubeSecret = nil
}
return kubeSecret, err
}
func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
func (r *InfisicalSecretReconciler) getInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
// default to new secret ref structure
secretName := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretName
secretNamespace := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretNamespace
@ -139,7 +89,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.
secretNamespace = infisicalSecret.Spec.TokenSecretReference.SecretNamespace
}
tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
tokenSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: secretNamespace,
Name: secretName,
})
@ -152,36 +102,14 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.
return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err)
}
infisicalServiceToken := tokenSecret.Data[INFISICAL_TOKEN_SECRET_KEY_NAME]
infisicalServiceToken := tokenSecret.Data[constants.INFISICAL_TOKEN_SECRET_KEY_NAME]
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
}
func (r *InfisicalSecretReconciler) GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (machineIdentityDetails model.MachineIdentityDetails, err error) {
func (r *InfisicalSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) {
universalAuthCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace,
Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName,
})
if k8Errors.IsNotFound(err) {
return model.MachineIdentityDetails{}, nil
}
if err != nil {
return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
}
clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID]
clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET]
return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
}
func (r *InfisicalSecretReconciler) GetInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) {
caCertificateFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalSecret.Spec.TLS.CaRef.SecretNamespace,
Name: infisicalSecret.Spec.TLS.CaRef.SecretName,
})
@ -197,13 +125,12 @@ func (r *InfisicalSecretReconciler) GetInfisicalCaCertificateFromKubeSecret(ctx
caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalSecret.Spec.TLS.CaRef.SecretKey])
return caCertificateFromSecret, nil
}
// Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object.
// If any keys are missing or an error occurs, returns an empty object or an error object, respectively.
func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
serviceAccountCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
serviceAccountCredsFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName,
})
@ -216,9 +143,9 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err)
}
accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_ACCESS_KEY]
publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_PUBLIC_KEY]
privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_PRIVATE_KEY]
accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_ACCESS_KEY]
publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PUBLIC_KEY]
privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PRIVATE_KEY]
if accessKeyFromSecret == nil || publicKeyFromSecret == nil || privateKeyFromSecret == nil {
return model.ServiceAccountDetails{}, nil
@ -227,7 +154,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
}
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
plainProcessedSecrets := make(map[string][]byte)
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
@ -283,7 +210,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
}
}
annotations[SECRET_VERSION_ANNOTATION] = ETag
annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
// create a new secret as specified by the managed secret spec of CRD
newKubeSecretInstance := &corev1.Secret{
@ -310,11 +237,11 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err)
}
fmt.Printf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s\n", secretType)
logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s", secretType))
return nil
}
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
plainProcessedSecrets := make(map[string][]byte)
@ -354,22 +281,22 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
}
managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag
managedKubeSecret.ObjectMeta.Annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
err := r.Client.Update(ctx, &managedKubeSecret)
if err != nil {
return fmt.Errorf("unable to update Kubernetes secret because [%w]", err)
}
fmt.Println("successfully updated managed Kubernetes secret")
logger.Info("successfully updated managed Kubernetes secret")
return nil
}
func (r *InfisicalSecretReconciler) GetResourceVariables(infisicalSecret v1alpha1.InfisicalSecret) ResourceVariables {
func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha1.InfisicalSecret) util.ResourceVariables {
var resourceVariables ResourceVariables
var resourceVariables util.ResourceVariables
if _, ok := resourceVariablesMap[string(infisicalSecret.UID)]; !ok {
if _, ok := infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)]; !ok {
ctx, cancel := context.WithCancel(context.Background())
@ -379,52 +306,52 @@ func (r *InfisicalSecretReconciler) GetResourceVariables(infisicalSecret v1alpha
UserAgent: api.USER_AGENT_NAME,
})
resourceVariablesMap[string(infisicalSecret.UID)] = ResourceVariables{
infisicalClient: client,
cancelCtx: cancel,
authDetails: AuthenticationDetails{},
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = util.ResourceVariables{
InfisicalClient: client,
CancelCtx: cancel,
AuthDetails: util.AuthenticationDetails{},
}
resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)]
resourceVariables = infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)]
} else {
resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)]
resourceVariables = infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)]
}
return resourceVariables
}
func (r *InfisicalSecretReconciler) UpdateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables ResourceVariables) {
resourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables util.ResourceVariables) {
infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
}
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error {
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) error {
resourceVariables := r.GetResourceVariables(infisicalSecret)
infisicalClient := resourceVariables.infisicalClient
cancelCtx := resourceVariables.cancelCtx
authDetails := resourceVariables.authDetails
resourceVariables := r.getResourceVariables(infisicalSecret)
infisicalClient := resourceVariables.InfisicalClient
cancelCtx := resourceVariables.CancelCtx
authDetails := resourceVariables.AuthDetails
var err error
if authDetails.authStrategy == "" {
fmt.Println("ReconcileInfisicalSecret: No authentication strategy found. Attempting to authenticate")
authDetails, err = r.HandleAuthentication(ctx, infisicalSecret, infisicalClient)
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, authDetails.authStrategy, err)
if authDetails.AuthStrategy == "" {
logger.Info("No authentication strategy found. Attempting to authenticate")
authDetails, err = r.handleAuthentication(ctx, infisicalSecret, infisicalClient)
r.SetInfisicalTokenLoadCondition(ctx, logger, &infisicalSecret, authDetails.AuthStrategy, err)
if err != nil {
return fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.UpdateResourceVariables(infisicalSecret, ResourceVariables{
infisicalClient: infisicalClient,
cancelCtx: cancelCtx,
authDetails: authDetails,
r.updateResourceVariables(infisicalSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
})
}
// Look for managed secret by name and namespace
managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
})
@ -436,14 +363,14 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
// Get exiting Etag if exists
secretVersionBasedOnETag := ""
if managedKubeSecret != nil {
secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION]
secretVersionBasedOnETag = managedKubeSecret.Annotations[constants.SECRET_VERSION_ANNOTATION]
}
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
var updateDetails model.RequestUpdateUpdateDetails
if authDetails.authStrategy == AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
}
@ -453,10 +380,10 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account")
logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
} else if authDetails.authStrategy == AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
} else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
}
@ -470,28 +397,30 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
fmt.Println("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
} else if authDetails.isMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.machineIdentityScope)
logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
} else if authDetails.IsMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.MachineIdentityScope)
if err != nil {
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
fmt.Printf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]\n", authDetails.authStrategy)
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
} else {
return errors.New("no authentication method provided yet. Please configure a authentication method then try again")
}
if !updateDetails.Modified {
fmt.Println("No secrets modified so reconcile not needed")
logger.Info("ReconcileInfisicalSecret: No secrets modified so reconcile not needed")
return nil
}
if managedKubeSecret == nil {
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
return r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
} else {
return r.UpdateInfisicalManagedKubeSecret(ctx, infisicalSecret, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
return r.updateInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
}
}

@ -1,150 +0,0 @@
package controllers
import (
"context"
"errors"
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/util"
infisicalSdk "github.com/infisical/go-sdk"
)
type AuthStrategyType string
var AuthStrategy = struct {
SERVICE_TOKEN AuthStrategyType
SERVICE_ACCOUNT AuthStrategyType
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
KUBERNETES_MACHINE_IDENTITY AuthStrategyType
AWS_IAM_MACHINE_IDENTITY AuthStrategyType
AZURE_MACHINE_IDENTITY AuthStrategyType
GCP_ID_TOKEN_MACHINE_IDENTITY AuthStrategyType
GCP_IAM_MACHINE_IDENTITY AuthStrategyType
}{
SERVICE_TOKEN: "SERVICE_TOKEN",
SERVICE_ACCOUNT: "SERVICE_ACCOUNT",
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
KUBERNETES_MACHINE_IDENTITY: "KUBERNETES_AUTH_MACHINE_IDENTITY",
AWS_IAM_MACHINE_IDENTITY: "AWS_IAM_MACHINE_IDENTITY",
AZURE_MACHINE_IDENTITY: "AZURE_MACHINE_IDENTITY",
GCP_ID_TOKEN_MACHINE_IDENTITY: "GCP_ID_TOKEN_MACHINE_IDENTITY",
GCP_IAM_MACHINE_IDENTITY: "GCP_IAM_MACHINE_IDENTITY",
}
type AuthenticationDetails struct {
authStrategy AuthStrategyType
machineIdentityScope v1alpha1.MachineIdentityScopeInWorkspace // This will only be set if a machine identity auth method is used (e.g. UniversalAuth or KubernetesAuth, etc.)
isMachineIdentityAuth bool
}
var ErrAuthNotApplicable = errors.New("authentication not applicable")
func (r *InfisicalSecretReconciler) handleUniversalAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
// Machine Identities:
universalAuthKubeSecret, err := r.GetInfisicalUniversalAuthFromKubeSecret(ctx, infisicalSecret)
universalAuthSpec := infisicalSecret.Spec.Authentication.UniversalAuth
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
}
if universalAuthKubeSecret.ClientId == "" && universalAuthKubeSecret.ClientSecret == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err = infisicalClient.Auth().UniversalAuthLogin(universalAuthKubeSecret.ClientId, universalAuthKubeSecret.ClientSecret)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err)
}
fmt.Println("Successfully authenticated with machine identity credentials")
return AuthenticationDetails{authStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY, machineIdentityScope: universalAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil
}
func (r *InfisicalSecretReconciler) handleKubernetesAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
kubernetesAuthSpec := infisicalSecret.Spec.Authentication.KubernetesAuth
if kubernetesAuthSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
serviceAccountToken, err := util.GetServiceAccountToken(r.Client, kubernetesAuthSpec.ServiceAccountRef.Namespace, kubernetesAuthSpec.ServiceAccountRef.Name)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err)
}
_, err = infisicalClient.Auth().KubernetesRawServiceAccountTokenLogin(kubernetesAuthSpec.IdentityID, serviceAccountToken)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with Kubernetes native auth [err=%s]", err)
}
return AuthenticationDetails{authStrategy: AuthStrategy.KUBERNETES_MACHINE_IDENTITY, machineIdentityScope: kubernetesAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil
}
func (r *InfisicalSecretReconciler) handleAwsIamAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
awsIamAuthSpec := infisicalSecret.Spec.Authentication.AwsIamAuth
if awsIamAuthSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().AwsIamAuthLogin(awsIamAuthSpec.IdentityID)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with AWS IAM auth [err=%s]", err)
}
return AuthenticationDetails{authStrategy: AuthStrategy.AWS_IAM_MACHINE_IDENTITY, machineIdentityScope: awsIamAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil
}
func (r *InfisicalSecretReconciler) handleAzureAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
azureAuthSpec := infisicalSecret.Spec.Authentication.AzureAuth
if azureAuthSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().AzureAuthLogin(azureAuthSpec.IdentityID, azureAuthSpec.Resource) // If resource is empty(""), it will default to "https://management.azure.com/" in the SDK.
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with Azure auth [err=%s]", err)
}
return AuthenticationDetails{authStrategy: AuthStrategy.AZURE_MACHINE_IDENTITY, machineIdentityScope: azureAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil
}
func (r *InfisicalSecretReconciler) handleGcpIdTokenAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
gcpIdTokenSpec := infisicalSecret.Spec.Authentication.GcpIdTokenAuth
if gcpIdTokenSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().GcpIdTokenAuthLogin(gcpIdTokenSpec.IdentityID)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP Id Token auth [err=%s]", err)
}
return AuthenticationDetails{authStrategy: AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY, machineIdentityScope: gcpIdTokenSpec.SecretsScope, isMachineIdentityAuth: true}, nil
}
func (r *InfisicalSecretReconciler) handleGcpIamAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
gcpIamSpec := infisicalSecret.Spec.Authentication.GcpIamAuth
if gcpIamSpec.IdentityID == "" && gcpIamSpec.ServiceAccountKeyFilePath == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().GcpIamAuthLogin(gcpIamSpec.IdentityID, gcpIamSpec.ServiceAccountKeyFilePath)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP IAM auth [err=%s]", err)
}
return AuthenticationDetails{authStrategy: AuthStrategy.GCP_IAM_MACHINE_IDENTITY, machineIdentityScope: gcpIamSpec.SecretsScope, isMachineIdentityAuth: true}, nil
}

@ -13,6 +13,215 @@ metadata:
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: infisicalpushsecrets.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: InfisicalPushSecret
listKind: InfisicalPushSecretList
plural: infisicalpushsecrets
singular: infisicalpushsecret
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: InfisicalPushSecret is the Schema for the infisicalpushsecrets API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
properties:
authentication:
properties:
awsIamAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
azureAuth:
properties:
identityId:
type: string
resource:
type: string
required:
- identityId
type: object
gcpIamAuth:
properties:
identityId:
type: string
serviceAccountKeyFilePath:
type: string
required:
- identityId
- serviceAccountKeyFilePath
type: object
gcpIdTokenAuth:
properties:
identityId:
type: string
required:
- identityId
type: object
kubernetesAuth:
description: Rest of your types should be defined similarly...
properties:
identityId:
type: string
serviceAccountRef:
properties:
name:
type: string
namespace:
type: string
required:
- name
- namespace
type: object
required:
- identityId
- serviceAccountRef
type: object
universalAuth:
description: PushSecretUniversalAuth defines universal authentication
properties:
credentialsRef:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- credentialsRef
type: object
type: object
deletionPolicy:
type: string
destination:
properties:
envSlug:
type: string
projectId:
type: string
secretsPath:
type: string
required:
- envSlug
- projectId
- secretsPath
type: object
hostAPI:
type: string
push:
properties:
secret:
properties:
secretName:
description: The name of the Kubernetes Secret
type: string
secretNamespace:
description: The name space where the Kubernetes Secret is located
type: string
required:
- secretName
- secretNamespace
type: object
required:
- secret
type: object
resyncInterval:
type: string
updatePolicy:
type: string
required:
- destination
- push
- resyncInterval
type: object
status:
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret
properties:
conditions:
items:
description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }"
properties:
lastTransitionTime:
description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable.
format: date-time
type: string
message:
description: message is a human readable message indicating details about the transition. This may be an empty string.
maxLength: 32768
type: string
observedGeneration:
description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance.
format: int64
minimum: 0
type: integer
reason:
description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty.
maxLength: 1024
minLength: 1
pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$
type: string
status:
description: status of the condition, one of True, False, Unknown.
enum:
- "True"
- "False"
- Unknown
type: string
type:
description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt)
maxLength: 316
pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$
type: string
required:
- lastTransitionTime
- message
- reason
- status
- type
type: object
type: array
managedSecrets:
additionalProperties:
type: string
description: managed secrets is a map where the key is the ID, and the value is the secret key (string[id], string[key] )
type: object
required:
- conditions
- managedSecrets
type: object
type: object
served: true
storage: true
subresources:
status: {}
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0

@ -16,7 +16,8 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log/zap"
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/controllers"
infisicalPushSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalpushsecret"
infisicalSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalsecret"
//+kubebuilder:scaffold:imports
)
@ -81,13 +82,24 @@ func main() {
os.Exit(1)
}
if err = (&controllers.InfisicalSecretReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
if err = (&infisicalSecretController.InfisicalSecretReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
BaseLogger: ctrl.Log,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "InfisicalSecret")
os.Exit(1)
}
if err = (&infisicalPushSecretController.InfisicalPushSecretReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
BaseLogger: ctrl.Log,
}).SetupWithManager(mgr); err != nil {
setupLog.Error(err, "unable to create controller", "controller", "InfisicalPushSecret")
os.Exit(1)
}
//+kubebuilder:scaffold:builder
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {

@ -24,10 +24,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
}
// logging for better debugging and user experience
fmt.Printf("Workspace ID: %v\n", tokenDetailsResponse.Workspace)
fmt.Printf("TokenName: %v\n", tokenDetailsResponse.Name)
return tokenDetailsResponse, nil
}

@ -0,0 +1,24 @@
package constants
const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey"
const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey"
const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey"
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config"
const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
const INFISICAL_DOMAIN = "https://app.infisical.com/api"
const INFISICAL_PUSH_SECRET_FINALIZER_NAME = "pushsecret.secrets.infisical.com/finalizer"
type PushSecretReplacePolicy string
type PushSecretDeletionPolicy string
const (
PUSH_SECRET_REPLACE_POLICY_ENABLED PushSecretReplacePolicy = "Replace"
PUSH_SECRET_DELETE_POLICY_ENABLED PushSecretDeletionPolicy = "Delete"
)

@ -0,0 +1,45 @@
package controllerhelpers
import (
"context"
"fmt"
"github.com/Infisical/infisical/k8-operator/packages/constants"
corev1 "k8s.io/api/core/v1"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
func GetInfisicalConfigMap(ctx context.Context, client client.Client) (configMap map[string]string, errToReturn error) {
// default key values
defaultConfigMapData := make(map[string]string)
defaultConfigMapData["hostAPI"] = constants.INFISICAL_DOMAIN
kubeConfigMap := &corev1.ConfigMap{}
err := client.Get(ctx, types.NamespacedName{
Namespace: constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE,
Name: constants.OPERATOR_SETTINGS_CONFIGMAP_NAME,
}, kubeConfigMap)
if err != nil {
if k8Errors.IsNotFound(err) {
kubeConfigMap = nil
} else {
return nil, fmt.Errorf("GetConfigMapByNamespacedName: unable to fetch config map in [namespacedName=%s] [err=%s]", constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, err)
}
}
if kubeConfigMap == nil {
return defaultConfigMapData, nil
} else {
for key, value := range defaultConfigMapData {
_, exists := kubeConfigMap.Data[key]
if !exists {
kubeConfigMap.Data[key] = value
}
}
return kubeConfigMap.Data, nil
}
}

@ -4,7 +4,12 @@ import (
"context"
"fmt"
"errors"
corev1 "k8s.io/api/core/v1"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
infisicalSdk "github.com/infisical/go-sdk"
"sigs.k8s.io/controller-runtime/pkg/client"
)
@ -32,3 +37,324 @@ func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAc
return string(token), nil
}
type AuthStrategyType string
var AuthStrategy = struct {
SERVICE_TOKEN AuthStrategyType
SERVICE_ACCOUNT AuthStrategyType
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
KUBERNETES_MACHINE_IDENTITY AuthStrategyType
AWS_IAM_MACHINE_IDENTITY AuthStrategyType
AZURE_MACHINE_IDENTITY AuthStrategyType
GCP_ID_TOKEN_MACHINE_IDENTITY AuthStrategyType
GCP_IAM_MACHINE_IDENTITY AuthStrategyType
}{
SERVICE_TOKEN: "SERVICE_TOKEN",
SERVICE_ACCOUNT: "SERVICE_ACCOUNT",
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
KUBERNETES_MACHINE_IDENTITY: "KUBERNETES_AUTH_MACHINE_IDENTITY",
AWS_IAM_MACHINE_IDENTITY: "AWS_IAM_MACHINE_IDENTITY",
AZURE_MACHINE_IDENTITY: "AZURE_MACHINE_IDENTITY",
GCP_ID_TOKEN_MACHINE_IDENTITY: "GCP_ID_TOKEN_MACHINE_IDENTITY",
GCP_IAM_MACHINE_IDENTITY: "GCP_IAM_MACHINE_IDENTITY",
}
type SecretCrdType string
var SecretCrd = struct {
INFISICAL_SECRET SecretCrdType
INFISICAL_PUSH_SECRET SecretCrdType
}{
INFISICAL_SECRET: "INFISICAL_SECRET",
INFISICAL_PUSH_SECRET: "INFISICAL_PUSH_SECRET",
}
type SecretAuthInput struct {
Secret interface{}
Type SecretCrdType
}
type AuthenticationDetails struct {
AuthStrategy AuthStrategyType
MachineIdentityScope v1alpha1.MachineIdentityScopeInWorkspace // This will only be set if a machine identity auth method is used (e.g. UniversalAuth or KubernetesAuth, etc.)
IsMachineIdentityAuth bool
SecretType SecretCrdType
}
var ErrAuthNotApplicable = errors.New("authentication not applicable")
func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
var universalAuthSpec v1alpha1.UniversalAuthDetails
switch secretCrd.Type {
case SecretCrd.INFISICAL_SECRET:
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
}
universalAuthSpec = infisicalSecret.Spec.Authentication.UniversalAuth
case SecretCrd.INFISICAL_PUSH_SECRET:
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
}
universalAuthSpec = v1alpha1.UniversalAuthDetails{
CredentialsRef: infisicalPushSecret.Spec.Authentication.UniversalAuth.CredentialsRef,
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
}
}
universalAuthKubeSecret, err := GetInfisicalUniversalAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{
SecretNamespace: universalAuthSpec.CredentialsRef.SecretNamespace,
SecretName: universalAuthSpec.CredentialsRef.SecretName,
})
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
}
if universalAuthKubeSecret.ClientId == "" && universalAuthKubeSecret.ClientSecret == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err = infisicalClient.Auth().UniversalAuthLogin(universalAuthKubeSecret.ClientId, universalAuthKubeSecret.ClientSecret)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY,
MachineIdentityScope: universalAuthSpec.SecretsScope,
IsMachineIdentityAuth: true,
SecretType: secretCrd.Type,
}, nil
}
func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
var kubernetesAuthSpec v1alpha1.KubernetesAuthDetails
switch secretCrd.Type {
case SecretCrd.INFISICAL_SECRET:
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
}
kubernetesAuthSpec = infisicalSecret.Spec.Authentication.KubernetesAuth
case SecretCrd.INFISICAL_PUSH_SECRET:
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
}
kubernetesAuthSpec = v1alpha1.KubernetesAuthDetails{
IdentityID: infisicalPushSecret.Spec.Authentication.KubernetesAuth.IdentityID,
ServiceAccountRef: v1alpha1.KubernetesServiceAccountRef{
Namespace: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
Name: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
},
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
}
}
if kubernetesAuthSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
serviceAccountToken, err := GetServiceAccountToken(reconcilerClient, kubernetesAuthSpec.ServiceAccountRef.Namespace, kubernetesAuthSpec.ServiceAccountRef.Name)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err)
}
_, err = infisicalClient.Auth().KubernetesRawServiceAccountTokenLogin(kubernetesAuthSpec.IdentityID, serviceAccountToken)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with Kubernetes native auth [err=%s]", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.KUBERNETES_MACHINE_IDENTITY,
MachineIdentityScope: kubernetesAuthSpec.SecretsScope,
IsMachineIdentityAuth: true,
SecretType: secretCrd.Type,
}, nil
}
func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
awsIamAuthSpec := v1alpha1.AWSIamAuthDetails{}
switch secretCrd.Type {
case SecretCrd.INFISICAL_SECRET:
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
}
awsIamAuthSpec = infisicalSecret.Spec.Authentication.AwsIamAuth
case SecretCrd.INFISICAL_PUSH_SECRET:
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
}
awsIamAuthSpec = v1alpha1.AWSIamAuthDetails{
IdentityID: infisicalPushSecret.Spec.Authentication.AwsIamAuth.IdentityID,
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
}
}
if awsIamAuthSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().AwsIamAuthLogin(awsIamAuthSpec.IdentityID)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with AWS IAM auth [err=%s]", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.AWS_IAM_MACHINE_IDENTITY,
MachineIdentityScope: awsIamAuthSpec.SecretsScope,
IsMachineIdentityAuth: true,
SecretType: secretCrd.Type,
}, nil
}
func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
azureAuthSpec := v1alpha1.AzureAuthDetails{}
switch secretCrd.Type {
case SecretCrd.INFISICAL_SECRET:
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
}
azureAuthSpec = infisicalSecret.Spec.Authentication.AzureAuth
case SecretCrd.INFISICAL_PUSH_SECRET:
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
}
azureAuthSpec = v1alpha1.AzureAuthDetails{
IdentityID: infisicalPushSecret.Spec.Authentication.AzureAuth.IdentityID,
Resource: infisicalPushSecret.Spec.Authentication.AzureAuth.Resource,
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
}
}
if azureAuthSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().AzureAuthLogin(azureAuthSpec.IdentityID, azureAuthSpec.Resource) // If resource is empty(""), it will default to "https://management.azure.com/" in the SDK.
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with Azure auth [err=%s]", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.AZURE_MACHINE_IDENTITY,
MachineIdentityScope: azureAuthSpec.SecretsScope,
IsMachineIdentityAuth: true,
SecretType: secretCrd.Type,
}, nil
}
func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
gcpIdTokenSpec := v1alpha1.GCPIdTokenAuthDetails{}
switch secretCrd.Type {
case SecretCrd.INFISICAL_SECRET:
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
}
gcpIdTokenSpec = infisicalSecret.Spec.Authentication.GcpIdTokenAuth
case SecretCrd.INFISICAL_PUSH_SECRET:
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
}
gcpIdTokenSpec = v1alpha1.GCPIdTokenAuthDetails{
IdentityID: infisicalPushSecret.Spec.Authentication.GcpIdTokenAuth.IdentityID,
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
}
}
if gcpIdTokenSpec.IdentityID == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().GcpIdTokenAuthLogin(gcpIdTokenSpec.IdentityID)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP Id Token auth [err=%s]", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY,
MachineIdentityScope: gcpIdTokenSpec.SecretsScope,
IsMachineIdentityAuth: true,
SecretType: secretCrd.Type,
}, nil
}
func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) {
gcpIamSpec := v1alpha1.GcpIamAuthDetails{}
switch secretCrd.Type {
case SecretCrd.INFISICAL_SECRET:
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
}
gcpIamSpec = infisicalSecret.Spec.Authentication.GcpIamAuth
case SecretCrd.INFISICAL_PUSH_SECRET:
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
if !ok {
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
}
gcpIamSpec = v1alpha1.GcpIamAuthDetails{
IdentityID: infisicalPushSecret.Spec.Authentication.GcpIamAuth.IdentityID,
ServiceAccountKeyFilePath: infisicalPushSecret.Spec.Authentication.GcpIamAuth.ServiceAccountKeyFilePath,
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
}
}
if gcpIamSpec.IdentityID == "" && gcpIamSpec.ServiceAccountKeyFilePath == "" {
return AuthenticationDetails{}, ErrAuthNotApplicable
}
_, err := infisicalClient.Auth().GcpIamAuthLogin(gcpIamSpec.IdentityID, gcpIamSpec.ServiceAccountKeyFilePath)
if err != nil {
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP IAM auth [err=%s]", err)
}
return AuthenticationDetails{
AuthStrategy: AuthStrategy.GCP_IAM_MACHINE_IDENTITY,
MachineIdentityScope: gcpIamSpec.SecretsScope,
IsMachineIdentityAuth: true,
SecretType: secretCrd.Type,
}, nil
}

@ -0,0 +1,50 @@
package util
import (
"context"
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/model"
corev1 "k8s.io/api/core/v1"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
func GetKubeSecretByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.Secret, error) {
kubeSecret := &corev1.Secret{}
err := reconcilerClient.Get(ctx, namespacedName, kubeSecret)
if err != nil {
kubeSecret = nil
}
return kubeSecret, err
}
func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference) (machineIdentityDetails model.MachineIdentityDetails, err error) {
universalAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
Namespace: universalAuthRef.SecretNamespace,
Name: universalAuthRef.SecretName,
// Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace,
// Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName,
})
if k8Errors.IsNotFound(err) {
return model.MachineIdentityDetails{}, nil
}
if err != nil {
return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
}
clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID]
clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET]
return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
}

@ -0,0 +1,13 @@
package util
import (
"context"
infisicalSdk "github.com/infisical/go-sdk"
)
type ResourceVariables struct {
InfisicalClient infisicalSdk.InfisicalClientInterface
CancelCtx context.CancelFunc
AuthDetails AuthenticationDetails
}

@ -0,0 +1,40 @@
package util
import (
"fmt"
"strconv"
"time"
)
func ConvertResyncIntervalToDuration(resyncInterval string) (time.Duration, error) {
length := len(resyncInterval)
if length < 2 {
return 0, fmt.Errorf("invalid format")
}
unit := resyncInterval[length-1:]
numberPart := resyncInterval[:length-1]
number, err := strconv.Atoi(numberPart)
if err != nil {
return 0, err
}
switch unit {
case "s":
if number < 5 {
return 0, fmt.Errorf("resync interval must be at least 5 seconds")
}
return time.Duration(number) * time.Second, nil
case "m":
return time.Duration(number) * time.Minute, nil
case "h":
return time.Duration(number) * time.Hour, nil
case "d":
return time.Duration(number) * 24 * time.Hour, nil
case "w":
return time.Duration(number) * 7 * 24 * time.Hour, nil
default:
return 0, fmt.Errorf("invalid time unit")
}
}