diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go
index 83732b64a..15f75a57d 100644
--- a/cli/packages/api/api.go
+++ b/cli/packages/api/api.go
@@ -631,8 +631,8 @@ func CallGatewayHeartBeatV1(httpClient *resty.Client) error {
return nil
}
-func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (map[string]interface{}, error) {
- var resBody map[string]interface{}
+func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRequest) (BootstrapInstanceResponse, error) {
+ var resBody BootstrapInstanceResponse
response, err := httpClient.
R().
SetResult(&resBody).
@@ -641,11 +641,11 @@ func CallBootstrapInstance(httpClient *resty.Client, request BootstrapInstanceRe
Post(fmt.Sprintf("%v/v1/admin/bootstrap", request.Domain))
if err != nil {
- return nil, NewGenericRequestError(operationCallBootstrapInstance, err)
+ return BootstrapInstanceResponse{}, NewGenericRequestError(operationCallBootstrapInstance, err)
}
if response.IsError() {
- return nil, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil)
+ return BootstrapInstanceResponse{}, NewAPIErrorWithResponse(operationCallBootstrapInstance, response, nil)
}
return resBody, nil
diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go
index a7a797a0b..9bf666e44 100644
--- a/cli/packages/api/model.go
+++ b/cli/packages/api/model.go
@@ -655,3 +655,35 @@ type BootstrapInstanceRequest struct {
Organization string `json:"organization"`
Domain string `json:"domain"`
}
+
+type BootstrapInstanceResponse struct {
+ Message string `json:"message"`
+ Identity BootstrapIdentity `json:"identity"`
+ Organization BootstrapOrganization `json:"organization"`
+ User BootstrapUser `json:"user"`
+}
+
+type BootstrapIdentity struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Credentials BootstrapIdentityCredentials `json:"credentials"`
+}
+
+type BootstrapIdentityCredentials struct {
+ Token string `json:"token"`
+}
+
+type BootstrapOrganization struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Slug string `json:"slug"`
+}
+
+type BootstrapUser struct {
+ ID string `json:"id"`
+ Email string `json:"email"`
+ FirstName string `json:"firstName"`
+ LastName string `json:"lastName"`
+ Username string `json:"username"`
+ SuperAdmin bool `json:"superAdmin"`
+}
diff --git a/cli/packages/cmd/bootstrap.go b/cli/packages/cmd/bootstrap.go
index 4582cb001..51e8b076e 100644
--- a/cli/packages/cmd/bootstrap.go
+++ b/cli/packages/cmd/bootstrap.go
@@ -4,9 +4,14 @@ Copyright (c) 2023 Infisical Inc.
package cmd
import (
+ "bytes"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/base64"
"encoding/json"
"fmt"
"os"
+ "text/template"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/util"
@@ -14,6 +19,133 @@ import (
"github.com/spf13/cobra"
)
+// handleK8SecretOutput processes the k8-secret output type by creating a Kubernetes secret
+func handleK8SecretOutput(bootstrapResponse api.BootstrapInstanceResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace string) error {
+ // Read Kubernetes service account credentials from the pod
+ k8sToken, err := os.ReadFile(util.KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH)
+ if err != nil {
+ return fmt.Errorf("failed to read Kubernetes service account token: %v", err)
+ }
+
+ k8sCaCert, err := os.ReadFile(util.KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH)
+ if err != nil {
+ return fmt.Errorf("failed to read Kubernetes CA certificate: %v", err)
+ }
+
+ // Get Kubernetes API server URL from environment variables
+ k8sHost := os.Getenv(util.KUBERNETES_SERVICE_HOST_ENV_NAME)
+ k8sPort := os.Getenv(util.KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME)
+ if k8sHost == "" || k8sPort == "" {
+ return fmt.Errorf("failed to get Kubernetes API server address from environment variables")
+ }
+
+ k8sApiUrl := fmt.Sprintf("https://%s:%s", k8sHost, k8sPort)
+
+ // Parse and execute the template to render only the data/stringData section
+ tmpl, err := template.New("k8-secret-template").Funcs(template.FuncMap{
+ "b64enc": func(s string) string {
+ return base64.StdEncoding.EncodeToString([]byte(s))
+ },
+ }).Parse(k8SecretTemplate)
+
+ if err != nil {
+ return fmt.Errorf("failed to parse output template: %v", err)
+ }
+
+ var renderedDataSection bytes.Buffer
+ err = tmpl.Execute(&renderedDataSection, bootstrapResponse)
+ if err != nil {
+ return fmt.Errorf("failed to execute output template: %v", err)
+ }
+
+ // Parse the rendered template as JSON to validate it's valid
+ var dataSection map[string]interface{}
+ if err := json.Unmarshal(renderedDataSection.Bytes(), &dataSection); err != nil {
+ return fmt.Errorf("template output is not valid JSON: %v", err)
+ }
+
+ // Construct the complete Kubernetes secret object
+ k8sSecret := map[string]interface{}{
+ "apiVersion": "v1",
+ "kind": "Secret",
+ "metadata": map[string]interface{}{
+ "name": k8SecretName,
+ "namespace": k8SecretNamespace,
+ },
+ "type": "Opaque",
+ }
+
+ // Merge the rendered data section into the secret
+ for key, value := range dataSection {
+ k8sSecret[key] = value
+ }
+
+ // Prepare the HTTP client with TLS configuration
+ caCertPool := x509.NewCertPool()
+ if !caCertPool.AppendCertsFromPEM(k8sCaCert) {
+ return fmt.Errorf("failed to parse Kubernetes CA certificate")
+ }
+
+ tlsConfig := &tls.Config{
+ RootCAs: caCertPool,
+ }
+
+ // Create a new HTTP client for Kubernetes API
+ k8sHttpClient, err := util.GetRestyClientWithCustomHeaders()
+ if err != nil {
+ return fmt.Errorf("failed to create Kubernetes HTTP client: %v", err)
+ }
+
+ k8sHttpClient.SetTLSClientConfig(tlsConfig)
+ k8sHttpClient.SetHeader("Authorization", fmt.Sprintf("Bearer %s", string(k8sToken)))
+ k8sHttpClient.SetHeader("Content-Type", "application/json")
+
+ // Check if secret already exists first
+ checkUrl := fmt.Sprintf("%s/api/v1/namespaces/%s/secrets/%s", k8sApiUrl, k8SecretNamespace, k8SecretName)
+ checkResponse, err := k8sHttpClient.R().Get(checkUrl)
+
+ if err != nil {
+ return fmt.Errorf("failed to check if Kubernetes secret exists: %v", err)
+ }
+
+ secretUrl := fmt.Sprintf("%s/api/v1/namespaces/%s/secrets", k8sApiUrl, k8SecretNamespace)
+
+ if checkResponse.StatusCode() == 200 {
+ // Secret exists, update it
+ secretUrl = fmt.Sprintf("%s/%s", secretUrl, k8SecretName)
+ response, err := k8sHttpClient.R().
+ SetBody(k8sSecret).
+ Put(secretUrl)
+
+ if err != nil {
+ return fmt.Errorf("failed to update Kubernetes secret: %v", err)
+ }
+
+ if response.IsError() {
+ return fmt.Errorf("kubernetes API returned error when updating secret: %s", response.String())
+ }
+
+ log.Info().Msgf("Successfully updated Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace)
+ } else {
+ // Secret doesn't exist, create it
+ response, err := k8sHttpClient.R().
+ SetBody(k8sSecret).
+ Post(secretUrl)
+
+ if err != nil {
+ return fmt.Errorf("failed to create Kubernetes secret: %v", err)
+ }
+
+ if response.IsError() {
+ return fmt.Errorf("kubernetes API returned error when creating secret: %s", response.String())
+ }
+
+ log.Info().Msgf("Successfully created Kubernetes secret '%s' in namespace '%s'", k8SecretName, k8SecretNamespace)
+ }
+
+ return nil
+}
+
var bootstrapCmd = &cobra.Command{
Use: "bootstrap",
Short: "Used to bootstrap your Infisical instance",
@@ -23,7 +155,7 @@ var bootstrapCmd = &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
email, _ := cmd.Flags().GetString("email")
if email == "" {
- if envEmail, ok := os.LookupEnv("INFISICAL_ADMIN_EMAIL"); ok {
+ if envEmail, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_EMAIL_NAME); ok {
email = envEmail
}
}
@@ -35,7 +167,7 @@ var bootstrapCmd = &cobra.Command{
password, _ := cmd.Flags().GetString("password")
if password == "" {
- if envPassword, ok := os.LookupEnv("INFISICAL_ADMIN_PASSWORD"); ok {
+ if envPassword, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_PASSWORD_NAME); ok {
password = envPassword
}
}
@@ -47,7 +179,7 @@ var bootstrapCmd = &cobra.Command{
organization, _ := cmd.Flags().GetString("organization")
if organization == "" {
- if envOrganization, ok := os.LookupEnv("INFISICAL_ADMIN_ORGANIZATION"); ok {
+ if envOrganization, ok := os.LookupEnv(util.INFISICAL_BOOTSTRAP_ORGANIZATION_NAME); ok {
organization = envOrganization
}
}
@@ -69,11 +201,55 @@ var bootstrapCmd = &cobra.Command{
return
}
+ outputType, err := cmd.Flags().GetString("output")
+ if err != nil {
+ log.Error().Msgf("Failed to get output type: %v", err)
+ return
+ }
+
+ k8SecretTemplate, err := cmd.Flags().GetString("k8-secret-template")
+ if err != nil {
+ log.Error().Msgf("Failed to get output template: %v", err)
+ }
+
+ k8SecretName, err := cmd.Flags().GetString("k8-secret-name")
+ if err != nil {
+ log.Error().Msgf("Failed to get k8-secret-name: %v", err)
+ }
+
+ k8SecretNamespace, err := cmd.Flags().GetString("k8-secret-namespace")
+ if err != nil {
+ log.Error().Msgf("Failed to get k8-secret-namespace: %v", err)
+ }
+
+ if outputType == "k8-secret" {
+ if k8SecretTemplate == "" {
+ log.Error().Msg("k8-secret-template is required when using k8-secret output type")
+ return
+ }
+
+ if k8SecretName == "" {
+ log.Error().Msg("k8-secret-name is required when using k8-secret output type")
+ return
+ }
+
+ if k8SecretNamespace == "" {
+ log.Error().Msg("k8-secret-namespace is required when using k8-secret output type")
+ return
+ }
+ }
+
httpClient, err := util.GetRestyClientWithCustomHeaders()
if err != nil {
log.Error().Msgf("Failed to get resty client with custom headers: %v", err)
return
}
+
+ ignoreIfBootstrapped, err := cmd.Flags().GetBool("ignore-if-bootstrapped")
+ if err != nil {
+ log.Error().Msgf("Failed to get ignore-if-bootstrapped flag: %v", err)
+ }
+
httpClient.SetHeader("Accept", "application/json")
bootstrapResponse, err := api.CallBootstrapInstance(httpClient, api.BootstrapInstanceRequest{
@@ -84,16 +260,26 @@ var bootstrapCmd = &cobra.Command{
})
if err != nil {
- log.Error().Msgf("Failed to bootstrap instance: %v", err)
+ if !ignoreIfBootstrapped {
+ log.Error().Msgf("Failed to bootstrap instance: %v", err)
+ }
return
}
- responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
- if err != nil {
- log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
- return
+ if outputType == "k8-secret" {
+ if err := handleK8SecretOutput(bootstrapResponse, k8SecretTemplate, k8SecretName, k8SecretNamespace); err != nil {
+ log.Error().Msgf("Failed to handle k8-secret output: %v", err)
+ return
+ }
+ } else {
+ responseJSON, err := json.MarshalIndent(bootstrapResponse, "", " ")
+ if err != nil {
+ log.Fatal().Msgf("Failed to convert response to JSON: %v", err)
+ return
+ }
+
+ fmt.Println(string(responseJSON))
}
- fmt.Println(string(responseJSON))
},
}
@@ -102,6 +288,10 @@ func init() {
bootstrapCmd.Flags().String("email", "", "The desired email address of the instance admin")
bootstrapCmd.Flags().String("password", "", "The desired password of the instance admin")
bootstrapCmd.Flags().String("organization", "", "The name of the organization to create for the instance")
-
+ bootstrapCmd.Flags().String("output", "", "The type of output to use for the bootstrap command (json or k8-secret)")
+ bootstrapCmd.Flags().Bool("ignore-if-bootstrapped", false, "Whether to continue on error if the instance has already been bootstrapped")
+ bootstrapCmd.Flags().String("k8-secret-template", "", "The template to use for rendering the Kubernetes secret (entire secret YAML)")
+ bootstrapCmd.Flags().String("k8-secret-namespace", "", "The namespace to use for the Kubernetes secret")
+ bootstrapCmd.Flags().String("k8-secret-name", "", "The name of the Kubernetes secret to create")
rootCmd.AddCommand(bootstrapCmd)
}
diff --git a/cli/packages/util/constants.go b/cli/packages/util/constants.go
index 126e5a5d0..383c7fc4c 100644
--- a/cli/packages/util/constants.go
+++ b/cli/packages/util/constants.go
@@ -10,6 +10,10 @@ const (
INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN_NAME = "INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN"
INFISICAL_VAULT_FILE_PASSPHRASE_ENV_NAME = "INFISICAL_VAULT_FILE_PASSPHRASE" // This works because we've forked the keyring package and added support for this env variable. This explains why you won't find any occurrences of it in the CLI codebase.
+ INFISICAL_BOOTSTRAP_EMAIL_NAME = "INFISICAL_ADMIN_EMAIL"
+ INFISICAL_BOOTSTRAP_PASSWORD_NAME = "INFISICAL_ADMIN_PASSWORD"
+ INFISICAL_BOOTSTRAP_ORGANIZATION_NAME = "INFISICAL_ADMIN_ORGANIZATION"
+
VAULT_BACKEND_AUTO_MODE = "auto"
VAULT_BACKEND_FILE_MODE = "file"
@@ -47,6 +51,11 @@ const (
INFISICAL_BACKUP_SECRET = "infisical-backup-secrets" // akhilmhdh: @depreciated remove in version v0.30
INFISICAL_BACKUP_SECRET_ENCRYPTION_KEY = "infisical-backup-secret-encryption-key"
+
+ KUBERNETES_SERVICE_HOST_ENV_NAME = "KUBERNETES_SERVICE_HOST"
+ KUBERNETES_SERVICE_PORT_HTTPS_ENV_NAME = "KUBERNETES_SERVICE_PORT_HTTPS"
+ KUBERNETES_SERVICE_ACCOUNT_CA_CERT_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
+ KUBERNETES_SERVICE_ACCOUNT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token"
)
var (
diff --git a/docs/cli/commands/bootstrap.mdx b/docs/cli/commands/bootstrap.mdx
index 77f8b38f1..f85c4167b 100644
--- a/docs/cli/commands/bootstrap.mdx
+++ b/docs/cli/commands/bootstrap.mdx
@@ -75,8 +75,90 @@ This flag is required.
+
+ Whether to continue without error if the instance has already been bootstrapped. Useful for idempotent automation scripts.
+
+```bash
+# Example
+infisical bootstrap --ignore-if-bootstrapped
+```
+
+This flag is optional and defaults to `false`.
+
+
+
+
+ The type of output format for the bootstrap command. Supports `k8-secret` for Kubernetes secret integration. This flag is optional and defaults to "".
+
+```bash
+# Kubernetes secret output
+infisical bootstrap --output=k8-secret --k8-secret-template='{"data":{"token":"{{.Identity.Credentials.Token}}"}}' --k8-secret-name=infisical-bootstrap --k8-secret-namespace=default
+```
+
+When using `k8-secret`, the command will create or update a Kubernetes secret directly in your cluster.
+
+
+
+
+ The template to use for rendering the Kubernetes secret data/stringData section. Required when using `--output=k8-secret`. The template uses Go template syntax and has access to the bootstrap response data.
+
+```bash
+# Example template that stores the token
+infisical bootstrap --k8-secret-template='{"data":{"token":"{{.Identity.Credentials.Token | b64enc}}"}}'
+
+# Example template with multiple fields
+infisical bootstrap --k8-secret-template='{"stringData":{"token":"{{.Identity.Credentials.Token}}","org-id":"{{.Organization.Id}}","user-email":"{{.User.Email}}"}}'
+```
+
+Available template functions:
+
+- `b64enc`: Base64 encode a string
+
+Available data fields:
+
+- `.Identity.Credentials.Token`: The machine identity token
+- `.Identity.Id`: The identity ID
+- `.Identity.Name`: The identity name
+- `.Organization.Id`: The organization ID
+- `.Organization.Name`: The organization name
+- `.Organization.Slug`: The organization slug
+- `.User.Email`: The admin user email
+- `.User.Id`: The admin user ID
+- `.User.FirstName`: The admin user first name
+- `.User.LastName`: The admin user last name
+
+This flag is required when using `k8-secret` output.
+
+
+
+
+ The name of the Kubernetes secret to create or update. Required when using `--output=k8-secret`.
+
+```bash
+# Example
+infisical bootstrap --k8-secret-name=infisical-bootstrap-credentials
+```
+
+This flag is required when using `k8-secret` output.
+
+
+
+
+ The namespace where the Kubernetes secret should be created or updated. Required when using `--output=k8-secret`.
+
+```bash
+# Example
+infisical bootstrap --k8-secret-namespace=infisical-system
+```
+
+This flag is required when using `k8-secret` output.
+
+
+
## Response
+### JSON Output (Default)
+
The command returns a JSON response with details about the created user, organization, and machine identity:
```json
@@ -105,6 +187,47 @@ The command returns a JSON response with details about the created user, organiz
}
```
+### Kubernetes Secret Output
+
+When using `--output=k8-secret`, the command creates or updates a Kubernetes secret in your cluster and logs the operation result.
+
+## Kubernetes Integration
+
+### Prerequisites for k8-secret Output
+
+When running with `--output=k8-secret`, the command must be executed from within a Kubernetes pod with proper service account permissions. The command automatically:
+
+1. Reads the service account token from `/var/run/secrets/kubernetes.io/serviceaccount/token`
+2. Reads the CA certificate from `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`
+3. Gets the Kubernetes API server URL from environment variables (`KUBERNETES_SERVICE_HOST` and `KUBERNETES_SERVICE_PORT_HTTPS`)
+
+### Required RBAC Permissions
+
+Your service account needs the following permissions:
+
+```yaml
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+ name: infisical-bootstrap
+rules:
+ - apiGroups: [""]
+ resources: ["secrets"]
+ verbs: ["get", "create", "update"]
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+ name: infisical-bootstrap
+subjects:
+ - kind: ServiceAccount
+ name: your-service-account
+roleRef:
+ kind: Role
+ name: infisical-bootstrap
+ apiGroup: rbac.authorization.k8s.io
+```
+
## Usage with Automation
For automation purposes, you can extract just the machine identity token from the response:
@@ -127,6 +250,8 @@ echo "Token has been captured and can be used for authentication"
## Notes
- The bootstrap process can only be performed once on a fresh Infisical instance
-- All flags are required for the bootstrap process to complete successfully
+- All core flags (domain, email, password, organization) are required for the bootstrap process to complete successfully
- Security controls prevent privilege escalation: instance admin identities cannot be managed by non-instance admin users and identities
- The generated admin user account can be used to log in via the UI if needed
+- When using `k8-secret` output, the command must run within a Kubernetes pod with proper service account permissions
+- The `--ignore-if-bootstrapped` flag is useful for making bootstrap scripts idempotent