This commit is contained in:
Tuan Dang
2023-01-16 10:53:16 +07:00
33 changed files with 770 additions and 456 deletions

View File

@ -3582,39 +3582,39 @@
},
"email": {
"type": "string",
"example": ""
"example": "johndoe@gmail.com"
},
"firstName": {
"type": "string",
"example": ""
"example": "John"
},
"lastName": {
"type": "string",
"example": ""
"example": "Doe"
},
"publicKey": {
"type": "string",
"example": ""
"example": "johns_nacl_public_key"
},
"encryptedPrivateKey": {
"type": "string",
"example": ""
"example": "johns_enc_nacl_private_key"
},
"iv": {
"type": "string",
"example": ""
"example": "iv_of_enc_nacl_private_key"
},
"tag": {
"type": "string",
"example": ""
"example": "tag_of_enc_nacl_private_key"
},
"updatedAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
},
"createdAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
}
}
},
@ -3630,27 +3630,27 @@
},
"email": {
"type": "string",
"example": ""
"example": "johndoe@gmail.com"
},
"firstName": {
"type": "string",
"example": ""
"example": "John"
},
"lastName": {
"type": "string",
"example": ""
"example": "Doe"
},
"publicKey": {
"type": "string",
"example": ""
"example": "johns_nacl_public_key"
},
"updatedAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
},
"createdAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
}
}
},
@ -3676,27 +3676,27 @@
},
"email": {
"type": "string",
"example": ""
"example": "johndoe@gmail.com"
},
"firstName": {
"type": "string",
"example": ""
"example": "John"
},
"lastName": {
"type": "string",
"example": ""
"example": "Doe"
},
"publicKey": {
"type": "string",
"example": ""
"example": "johns_nacl_public_key"
},
"updatedAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
},
"createdAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
}
}
},
@ -3723,7 +3723,7 @@
},
"name": {
"type": "string",
"example": ""
"example": "Acme Corp."
},
"customerId": {
"type": "string",
@ -3776,7 +3776,7 @@
"properties": {
"publicKey": {
"type": "string",
"example": ""
"example": "senders_nacl_public_key"
}
}
},
@ -3938,11 +3938,11 @@
},
"updatedAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
},
"createdAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
}
}
},
@ -3962,15 +3962,15 @@
},
"email": {
"type": "string",
"example": ""
"example": "johndoe@gmail.com"
},
"firstName": {
"type": "string",
"example": ""
"example": "John"
},
"lastName": {
"type": "string",
"example": ""
"example": "Doe"
}
}
},
@ -4033,11 +4033,11 @@
},
"updatedAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
},
"createdAt": {
"type": "string",
"example": ""
"example": "2023-01-13T14:16:12.210Z"
}
}
},
@ -4087,7 +4087,7 @@
},
"type": {
"type": "string",
"example": ""
"example": "shared"
},
"user": {
"type": "string",
@ -4095,7 +4095,7 @@
},
"environment": {
"type": "string",
"example": ""
"example": "dev"
},
"isDeleted": {
"type": "string",

View File

@ -2,7 +2,6 @@
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
const fs = require('fs').promises;
const yaml = require('js-yaml');
const { secretSchema } = require('./schemas/index.ts');
/**
* Generates OpenAPI specs for all Infisical API endpoints:
@ -43,25 +42,25 @@ const generateOpenAPISpec = async () => {
definitions: {
CurrentUser: {
_id: '',
email: '',
firstName: '',
lastName: '',
publicKey: '',
encryptedPrivateKey: '',
iv: '',
tag: '',
updatedAt: '',
createdAt: ''
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe',
publicKey: 'johns_nacl_public_key',
encryptedPrivateKey: 'johns_enc_nacl_private_key',
iv: 'iv_of_enc_nacl_private_key',
tag: 'tag_of_enc_nacl_private_key',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
Membership: {
user: {
_id: '',
email: '',
firstName: '',
lastName: '',
publicKey: '',
updatedAt: '',
createdAt: ''
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe',
publicKey: 'johns_nacl_public_key',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
workspace: '',
role: 'admin'
@ -69,12 +68,12 @@ const generateOpenAPISpec = async () => {
MembershipOrg: {
user: {
_id: '',
email: '',
firstName: '',
lastName: '',
publicKey: '',
updatedAt: '',
createdAt: ''
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe',
publicKey: 'johns_nacl_public_key',
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
organization: '',
role: 'owner',
@ -82,7 +81,7 @@ const generateOpenAPISpec = async () => {
},
Organization: {
_id: '',
name: '',
name: 'Acme Corp.',
customerId: ''
},
Project: {
@ -97,7 +96,7 @@ const generateOpenAPISpec = async () => {
encryptedkey: '',
nonce: '',
sender: {
publicKey: ''
publicKey: 'senders_nacl_public_key'
},
receiver: '',
workspace: ''
@ -141,16 +140,16 @@ const generateOpenAPISpec = async () => {
secretCommentCiphertext: '',
secretCommentIV: '',
secretCommentTag: '',
updatedAt: '',
createdAt: ''
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
Log: {
_id: '',
user: {
_id: '',
email: '',
firstName: '',
lastName: ''
email: 'johndoe@gmail.com',
firstName: 'John',
lastName: 'Doe'
},
workspace: '',
actionNames: [
@ -171,8 +170,8 @@ const generateOpenAPISpec = async () => {
],
channel: 'cli',
ipAddress: '192.168.0.1',
updatedAt: '',
createdAt: ''
updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '2023-01-13T14:16:12.210Z'
},
SecretSnapshot: {
workspace: '',
@ -188,9 +187,9 @@ const generateOpenAPISpec = async () => {
secret: '',
version: 1,
workspace: '',
type: '',
type: 'shared',
user: '',
environment: '',
environment: 'dev',
isDeleted: '',
secretKeyCiphertext: '',
secretKeyIV: '',

View File

@ -1,6 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const secretSchema = require('./secretSchema.ts');
module.exports = {
secretSchema
}

View File

@ -1,11 +0,0 @@
const secretSchema = {
_id: {
type: 'string',
format: 'objectId'
},
version: {
type: 'number'
}
}
module.exports = secretSchema;

View File

@ -2,3 +2,10 @@
title: "Create"
openapi: "POST /api/v2/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for creating
secrets](/api-reference/overview/examples/create-secrets).
</Tip>

View File

@ -2,3 +2,10 @@
title: "Retrieve"
openapi: "GET /api/v2/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for retrieving
secrets](/api-reference/overview/examples/retrieve-secrets).
</Tip>

View File

@ -2,3 +2,10 @@
title: "Update"
openapi: "PATCH /api/v2/secrets/"
---
<Tip>
Using this route requires understanding Infisical's system and cryptography.
It may be helpful to read through the
[introduction](/api-reference/overview/introduction) and [guide for updating
secrets](/api-reference/overview/examples/update-secrets).
</Tip>

View File

@ -2,10 +2,14 @@
title: "Authentication"
---
To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key from your user settings.
To authenticate requests with Infisical, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. You can obtain an API key in User Settings > API Keys
![API key dashboard](../../images/api-key-dashboard.png)
![API key in personal settings](../../images/api-key-settings.png)
![Adding an API key](../../images/api-key-add.png)
<Info>
It's important to keep your API key secure, as it grants access to your
secrets in Infisical. For added security, consider rotating your API key on a
regular basis.
secrets in Infisical. For added security, set a reasonable expiration time and
rotate your API key on a regular basis.
</Info>

View File

@ -11,12 +11,12 @@ Prerequisites:
## Flow
1. Get your (encrypted) private key.
1. [Get your (encrypted) private key](/api-reference/endpoints/users/me).
2. Decrypt your (encrypted) private key with your password.
3. Get the (encrypted) project key for the project.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project key.
6. Send (encrypted) secret(s) to the Infical API
6. [Send (encrypted) secret(s) to the Infical API](/api-reference/endpoints/secrets/create)
## Example

View File

@ -11,11 +11,11 @@ Prerequisites:
## Flow
1. Get your (encrypted) private key.
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
2. Decrypt your (encrypted) private key with your password.
3. Get the (encrypted) project key for the project.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Get secrets for a project and environment.
5. [Get secrets for a project and environment.](/api-reference/endpoints/secrets/read)
6. Decrypt the (encrypted) secrets
## Example

View File

@ -11,12 +11,12 @@ Prerequisites:
## Flow
1. Get your (encrypted) private key.
1. [Get your (encrypted) private key.](/api-reference/endpoints/users/me)
2. Decrypt your (encrypted) private key with your password.
3. Get the project key for the project.
4. Decrypt the project key with your private key.
3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project key.
6. Send (encrypted) updated secret(s) to the Infical API
6. [Send (encrypted) updated secret(s) to the Infical API.](/api-reference/endpoints/secrets/update)
## Example

View File

@ -2,11 +2,6 @@
title: "Introduction"
---
<Warning>
Infisical's REST API is currently unavailable and scheduled to go live on Jan
16!
</Warning>
Infisical's REST API provides users an alternative way to programmatically access and manage
secrets via HTTPS requests. This can be useful for automating tasks, such as
rotating credentials, or for integrating secret management into a larger system.
@ -24,8 +19,9 @@ Using Infisical's API to manage secrets requires a basic understanding of the sy
- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
<Info>
Infisical's system ensures greater security such that secrets are
encrypted/decrypted on the client-side but requires users to properly
implement cryptographic operations to maintain end-to-end encryption (E2EE).
We're
Infisical's system requires that secrets be encrypted/decrypted on the
client-side to maintain E2EE. We strongly recommend you read up on the system
prior to using the Infisical API. The (opt-in) ability to retrieve secrets
back in decrypted format if you choose to share secrets with Infisical is on
our roadmap.
</Info>

BIN
docs/images/api-key-add.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

View File

@ -2208,31 +2208,31 @@ components:
example: ''
email:
type: string
example: ''
example: johndoe@gmail.com
firstName:
type: string
example: ''
example: John
lastName:
type: string
example: ''
example: Doe
publicKey:
type: string
example: ''
example: johns_nacl_public_key
encryptedPrivateKey:
type: string
example: ''
example: johns_enc_nacl_private_key
iv:
type: string
example: ''
example: iv_of_enc_nacl_private_key
tag:
type: string
example: ''
example: tag_of_enc_nacl_private_key
updatedAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
createdAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
Membership:
type: object
properties:
@ -2244,22 +2244,22 @@ components:
example: ''
email:
type: string
example: ''
example: johndoe@gmail.com
firstName:
type: string
example: ''
example: John
lastName:
type: string
example: ''
example: Doe
publicKey:
type: string
example: ''
example: johns_nacl_public_key
updatedAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
createdAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
workspace:
type: string
example: ''
@ -2277,22 +2277,22 @@ components:
example: ''
email:
type: string
example: ''
example: johndoe@gmail.com
firstName:
type: string
example: ''
example: John
lastName:
type: string
example: ''
example: Doe
publicKey:
type: string
example: ''
example: johns_nacl_public_key
updatedAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
createdAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
organization:
type: string
example: ''
@ -2310,7 +2310,7 @@ components:
example: ''
name:
type: string
example: ''
example: Acme Corp.
customerId:
type: string
example: ''
@ -2348,7 +2348,7 @@ components:
properties:
publicKey:
type: string
example: ''
example: senders_nacl_public_key
receiver:
type: string
example: ''
@ -2466,10 +2466,10 @@ components:
example: ''
updatedAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
createdAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
Log:
type: object
properties:
@ -2484,13 +2484,13 @@ components:
example: ''
email:
type: string
example: ''
example: johndoe@gmail.com
firstName:
type: string
example: ''
example: John
lastName:
type: string
example: ''
example: Doe
workspace:
type: string
example: ''
@ -2533,10 +2533,10 @@ components:
example: 192.168.0.1
updatedAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
createdAt:
type: string
example: ''
example: '2023-01-13T14:16:12.210Z'
SecretSnapshot:
type: object
properties:
@ -2571,13 +2571,13 @@ components:
example: ''
type:
type: string
example: ''
example: shared
user:
type: string
example: ''
environment:
type: string
example: ''
example: dev
isDeleted:
type: string
example: ''

View File

@ -13,7 +13,7 @@ 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: 0.1.0
version: 0.1.1
# 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.

View File

@ -35,9 +35,6 @@ spec:
spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI:
default: https://app.infisical.com/api
description: Infisical host to pull secrets from
@ -54,9 +51,6 @@ spec:
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference:
properties:
secretName:
@ -69,9 +63,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret

View File

@ -16,17 +16,11 @@ type KubeSecretReference struct {
// InfisicalSecretSpec defines the desired state of InfisicalSecret
type InfisicalSecretSpec struct {
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
// +kubebuilder:validation:Required
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
// +kubebuilder:validation:Required
ManagedSecretReference KubeSecretReference `json:"managedSecretReference,omitempty"`
// The Infisical project id
// +kubebuilder:validation:Required
ProjectId string `json:"projectId"`
// The Infisical environment such as dev, prod, testing
// +kubebuilder:validation:Required
Environment string `json:"environment"`
// Infisical host to pull secrets from
// +kubebuilder:default="https://app.infisical.com/api"
HostAPI string `json:"hostAPI,omitempty"`

View File

@ -35,9 +35,6 @@ spec:
spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI:
default: https://app.infisical.com/api
description: Infisical host to pull secrets from
@ -54,9 +51,6 @@ spec:
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference:
properties:
secretName:
@ -69,9 +63,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret

View File

@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-2
labels:
app: nginx
annotations:
secrets.infisical.com/auto-reload: "true"
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
envFrom:
- secretRef:
name: managed-secret
ports:
- containerPort: 80

View File

@ -3,11 +3,10 @@ kind: InfisicalSecret
metadata:
name: infisicalsecret-sample
spec:
projectId: 62faf98ae0b05e8529b5da46
environment: dev
hostAPI: https://app.infisical.com/api
tokenSecretReference:
secretName: service-token
secretNamespace: first-project
secretNamespace: default
managedSecretReference:
secretName: managed-secret
secretNamespace: first-project
secretNamespace: default

View File

@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: service-token
type: Opaque
data:
infisicalToken: <base64 infisical token here>

View File

@ -0,0 +1,121 @@
package controllers
import (
"context"
"fmt"
"sync"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
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) {
listOfDeployments := &v1.DeploymentList{}
err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace})
if err != nil {
return 0, fmt.Errorf("unable to get deployments in the [namespace=%v] [err=%v]", infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err)
}
managedKubeSecretNameAndNamespace := types.NamespacedName{
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
}
managedKubeSecret := &corev1.Secret{}
err = r.Client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret)
if err != nil {
return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err)
}
// Create a channel to receive errors from goroutines
errChan := make(chan error, len(listOfDeployments.Items))
wg := sync.WaitGroup{}
wg.Add(len(listOfDeployments.Items))
go func() {
wg.Wait()
close(errChan)
}()
// Iterate over the deployments and check if they use the managed secret
for _, deployment := range listOfDeployments.Items {
if deployment.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && r.IsDeploymentUsingManagedSecret(deployment, infisicalSecret) {
// Start a goroutine to reconcile the deployment
go func(d v1.Deployment, s corev1.Secret) {
defer wg.Done()
if err := r.ReconcileDeployment(ctx, d, s); err != nil {
errChan <- err
}
}(deployment, *managedKubeSecret)
}
}
// Collect any errors that were sent through the channel
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return 0, fmt.Errorf("unable to reconcile some deployments: %v", errs)
}
return len(listOfDeployments.Items), nil
}
// Check if the deployment uses managed secrets
func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1.Deployment, infisicalSecret v1alpha1.InfisicalSecret) bool {
managedSecretName := infisicalSecret.Spec.ManagedSecretReference.SecretName
for _, container := range deployment.Spec.Template.Spec.Containers {
for _, envFrom := range container.EnvFrom {
if envFrom.SecretRef != nil && envFrom.SecretRef.LocalObjectReference.Name == managedSecretName {
return true
}
}
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.LocalObjectReference.Name == managedSecretName {
return true
}
}
}
for _, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == managedSecretName {
return true
}
}
return false
}
// 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 {
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
annotationValue := secret.Annotations[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)
return nil
}
fmt.Printf("deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]\n", deployment.ObjectMeta.Name)
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = make(map[string]string)
}
deployment.Annotations[annotationKey] = annotationValue
deployment.Spec.Template.Annotations[annotationKey] = annotationValue
if err := r.Client.Update(ctx, &deployment); err != nil {
return fmt.Errorf("failed to update deployment annotation: %v", err)
}
return nil
}

View File

@ -0,0 +1,95 @@
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 *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) error {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn != nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: "Failed to sync secrets. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info",
})
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/AutoRedeployReady",
Status: metav1.ConditionFalse,
Reason: "Stopped",
Message: "Auto redeployment has been stopped because the operator failed to sync secrets",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has started syncing your secrets",
})
}
return r.Client.Status().Update(ctx, infisicalSecret)
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LoadedInfisicalToken",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has located the Infisical token in provided Kubernetes secret",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LoadedInfisicalToken",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to load Infisical Token from the provided Kubernetes secret because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for LoadedInfisicalToken")
}
}
func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/AutoRedeployReady",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: fmt.Sprintf("Infisical has found %v deployments which are ready to be auto redeployed when secrets change", numDeployments),
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/AutoRedeployReady",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed reconcile deployments because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for AutoRedeployReady")
}
}

View File

@ -2,16 +2,17 @@ package controllers
import (
"context"
"fmt"
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
)
// InfisicalSecretReconciler reconciles a InfisicalSecret object
@ -31,19 +32,18 @@ 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
func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
requeueTime := time.Minute * 1
var infisicalSecretCR v1alpha1.InfisicalSecret
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
requeueTime := time.Minute * 5
if err != nil {
if errors.IsNotFound(err) {
log.Info("Infisical Secret not found")
return ctrl.Result{}, nil
fmt.Printf("Infisical Secret CRD not found [err=%v]", err)
return ctrl.Result{
Requeue: false,
}, nil
} else {
log.Error(err, "Unable to fetch Infisical Secret from cluster. Will retry")
fmt.Printf("Unable to fetch Infisical Secret CRD from cluster because [err=%v]", err)
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@ -52,13 +52,28 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// Check if the resource is already marked for deletion
if infisicalSecretCR.GetDeletionTimestamp() != nil {
return ctrl.Result{}, nil
return ctrl.Result{
Requeue: false,
}, nil
}
// set the api url based on the CRD
api.API_HOST_URL = infisicalSecretCR.Spec.HostAPI
err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
if err != nil {
log.Error(err, "Unable to reconcile Infisical Secret and will try again")
fmt.Printf("unable to reconcile Infisical Secret because [err=%v]. Will requeue after [requeueTime=%v]\n", err, requeueTime)
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, infisicalSecretCR)
r.SetInfisicalAutoRedeploymentReady(ctx, &infisicalSecretCR, numDeployments, err)
if err != nil {
fmt.Printf("unable to reconcile auto redeployment because [err=%v]", err)
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@ -73,6 +88,6 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// SetupWithManager sets up the controller with the Manager.
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}). // TODO we should also be watching secrets with the name specifed
For(&secretsv1alpha1.InfisicalSecret{}).
Complete(r)
}

View File

@ -6,16 +6,16 @@ import (
"strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
api "github.com/Infisical/infisical/k8-operator/packages/api"
models "github.com/Infisical/infisical/k8-operator/packages/models"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) {
kubeSecret := &corev1.Secret{}
@ -27,14 +27,14 @@ func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Co
return kubeSecret, err
}
func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.TokenSecretReference.SecretName,
})
if err != nil {
return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.ManagedSecretReference.SecretName, infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err)
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]
@ -45,7 +45,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infis
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
}
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []models.SingleEnvironmentVariable) error {
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []util.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error {
plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
@ -56,6 +56,9 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
ObjectMeta: metav1.ObjectMeta{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Annotations: map[string]string{
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
},
},
Type: "Opaque",
Data: plainProcessedSecrets,
@ -70,13 +73,17 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil
}
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []models.SingleEnvironmentVariable) error {
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []util.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error {
plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
}
managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations = map[string]string{
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
}
err := r.Client.Update(ctx, &managedKubeSecret)
if err != nil {
return fmt.Errorf("unable to update Kubernetes secret because [%w]", err)
@ -87,12 +94,13 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
}
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error {
infisicalToken, err := r.GetInfisicalToken(ctx, infisicalSecret)
infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
if err != nil {
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
}
// Look for managed secret by name and namespace
managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
@ -102,72 +110,28 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
}
secretsFromApi, err := api.GetAllEnvironmentVariables(infisicalSecret.Spec.ProjectId, infisicalSecret.Spec.Environment, infisicalToken, infisicalSecret.Spec.HostAPI)
secretVersionBasedOnETag := ""
if err != nil {
return err
if managedKubeSecret != nil {
secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION]
}
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err := util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag)
if err != nil {
return fmt.Errorf("failed to get secrets because [err=%v]\n", err)
}
if !fullEncryptedSecretsResponse.Modified {
fmt.Println("No secrets modified so reconcile not needed", "Etag:", fullEncryptedSecretsResponse.ETag, "Modified:", fullEncryptedSecretsResponse.Modified)
return nil
}
fmt.Println("secret is modified so it needs to be created or updated")
if managedKubeSecret == nil {
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, secretsFromApi)
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
} else {
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, secretsFromApi)
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
}
}
// Conditions
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has started syncing your secrets",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to update secret because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition", err)
}
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LoadedInfisicalToken",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has located the Infisical token in provided Kubernetes secret",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LoadedInfisicalToken",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to load Infisical Token because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for LoadedInfisicalToken")
}
}

View File

@ -43,9 +43,6 @@ spec:
spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI:
default: https://app.infisical.com/api
description: Infisical host to pull secrets from
@ -62,9 +59,6 @@ spec:
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference:
properties:
secretName:
@ -77,9 +71,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret

View File

@ -1,177 +1,80 @@
package api
import (
"encoding/base64"
"errors"
"fmt"
"regexp"
"strings"
"github.com/Infisical/infisical/k8-operator/packages/crypto"
"github.com/Infisical/infisical/k8-operator/packages/models"
"github.com/go-resty/resty/v2"
"golang.org/x/crypto/nacl/box"
)
func GetAllEnvironmentVariables(projectId string, envName string, infisicalToken string, hostAPI string) ([]models.SingleEnvironmentVariable, error) {
envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId, hostAPI)
if err != nil {
return nil, err
}
const USER_AGENT_NAME = "k8-operator"
return SubstituteSecrets(envsFromApi), nil
}
func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string, hostAPI string) ([]models.SingleEnvironmentVariable, error) {
if infisicalToken == "" || projectId == "" || envName == "" {
return nil, errors.New("infisical token, project id and or environment name cannot be empty")
}
splitToken := strings.Split(infisicalToken, ",")
JTWToken := splitToken[0]
temPrivateKey := splitToken[1]
// create http client
httpClient := resty.New().
SetAuthToken(JTWToken).
SetHeader("Accept", "application/json")
var pullSecretsByInfisicalTokenResponse models.PullSecretsByInfisicalTokenResponse
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", API_HOST_URL, request.WorkspaceId)
var result GetEncryptedWorkspaceKeyResponse
response, err := httpClient.
R().
SetQueryParam("environment", envName).
SetQueryParam("channel", "cli").
SetResult(&pullSecretsByInfisicalTokenResponse).
Get(fmt.Sprintf("%v/v1/secret/%v/service-token", hostAPI, projectId))
SetResult(&result).
SetHeader("User-Agent", USER_AGENT_NAME).
Get(endpoint)
if err != nil {
return nil, err
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
}
if response.StatusCode() > 299 {
return nil, fmt.Errorf(response.Status())
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
}
// Get workspace key
workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.EncryptedKey)
if err != nil {
return nil, err
}
nonce, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Nonce)
if err != nil {
return nil, err
}
senderPublicKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Sender.PublicKey)
if err != nil {
return nil, err
}
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(temPrivateKey)
if err != nil {
return nil, err
}
workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
var listOfEnv []models.SingleEnvironmentVariable
for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets {
key_iv, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Iv)
key_tag, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Tag)
key_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Ciphertext)
plainTextKey, err := crypto.DecryptSymmetric(workspaceKeyInBytes, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, err
}
value_iv, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Iv)
value_tag, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Tag)
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Ciphertext)
plainTextValue, err := crypto.DecryptSymmetric(workspaceKeyInBytes, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, err
}
env := models.SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
}
listOfEnv = append(listOfEnv, env)
}
return listOfEnv, nil
return result, nil
}
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
return value
func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
var tokenDetailsResponse GetServiceTokenDetailsResponse
response, err := httpClient.
R().
SetResult(&tokenDetailsResponse).
SetHeader("User-Agent", USER_AGENT_NAME).
Get(fmt.Sprintf("%v/v2/service-token", API_HOST_URL))
if err != nil {
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err)
}
for _, secret := range secrets {
if secret.Key == variableWeAreLookingFor {
regex := regexp.MustCompile(`\${([^\}]*)}`)
variablesToPopulate := regex.FindAllString(secret.Value, -1)
// case: variable is a constant so return its value
if len(variablesToPopulate) == 0 {
return secret.Value
}
valueToEdit := secret.Value
for _, variableWithSign := range variablesToPopulate {
variableWithoutSign := strings.Trim(variableWithSign, "}")
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
// case: reference to self
if variableWithoutSign == secret.Key {
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
continue
} else {
var expandedVariableValue string
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
expandedVariableValue = preComputedVariable
} else {
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
}
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
continue
} else {
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
}
}
}
return valueToEdit
} else {
continue
}
if response.IsError() {
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
}
return "${" + variableWeAreLookingFor + "}"
return tokenDetailsResponse, nil
}
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
hashMapOfCompleteVariables := make(map[string]string)
hashMapOfSelfRefs := make(map[string]string)
expandedSecrets := []models.SingleEnvironmentVariable{}
for _, secret := range secrets {
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key,
Value: expandedVariable,
})
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
var secretsResponse GetEncryptedSecretsV2Response = GetEncryptedSecretsV2Response{}
createHttpRequest := httpClient.
R().
SetResult(&secretsResponse.Secrets).
SetQueryParam("environment", request.EnvironmentName).
SetHeader("User-Agent", USER_AGENT_NAME)
if request.ETag != "" {
createHttpRequest.SetHeader("If-None-Match", request.ETag)
}
return expandedSecrets
response, err := createHttpRequest.Get(fmt.Sprintf("%v/v2/secret/workspace/%v", API_HOST_URL, request.WorkspaceId))
if err != nil {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
}
if response.StatusCode() == 304 {
secretsResponse.Modified = false
} else {
secretsResponse.Modified = true
}
secretsResponse.ETag = response.Header().Get("etag")
return secretsResponse, nil
}

View File

@ -0,0 +1,75 @@
package api
import "time"
type GetEncryptedWorkspaceKeyRequest struct {
WorkspaceId string `json:"workspaceId"`
}
type GetEncryptedWorkspaceKeyResponse struct {
ID string `json:"_id"`
EncryptedKey string `json:"encryptedKey"`
Nonce string `json:"nonce"`
Sender struct {
ID string `json:"_id"`
Email string `json:"email"`
RefreshVersion int `json:"refreshVersion"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
PublicKey string `json:"publicKey"`
} `json:"sender"`
Receiver string `json:"receiver"`
Workspace string `json:"workspace"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type GetEncryptedSecretsV2Request struct {
EnvironmentName string `json:"environmentName"`
WorkspaceId string `json:"workspaceId"`
ETag string `json:"etag,omitempty"`
}
type GetEncryptedSecretsV2Response struct {
Secrets []struct {
ID string `json:"_id"`
Version int `json:"version"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
SecretKeyIV string `json:"secretKeyIV"`
SecretKeyTag string `json:"secretKeyTag"`
SecretKeyHash string `json:"secretKeyHash"`
SecretValueCiphertext string `json:"secretValueCiphertext"`
SecretValueIV string `json:"secretValueIV"`
SecretValueTag string `json:"secretValueTag"`
SecretValueHash string `json:"secretValueHash"`
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
SecretCommentIV string `json:"secretCommentIV"`
SecretCommentTag string `json:"secretCommentTag"`
SecretCommentHash string `json:"secretCommentHash"`
V int `json:"__v"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
User string `json:"user,omitempty"`
}
Modified bool `json:"modified,omitempty"`
ETag string `json:"ETag,omitempty"`
}
type GetServiceTokenDetailsResponse struct {
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Environment string `json:"environment"`
User string `json:"user"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
}

View File

@ -0,0 +1,3 @@
package api
var API_HOST_URL string = "https://app.infisical.com/api"

View File

@ -1,51 +0,0 @@
package models
import "time"
type PullSecretsByInfisicalTokenResponse struct {
Secrets []struct {
ID string `json:"_id"`
Workspace string `json:"workspace"`
Type string `json:"type"`
Environment string `json:"environment"`
SecretKey struct {
Workspace string `json:"workspace"`
Ciphertext string `json:"ciphertext"`
Iv string `json:"iv"`
Tag string `json:"tag"`
Hash string `json:"hash"`
} `json:"secretKey"`
SecretValue struct {
Workspace string `json:"workspace"`
Ciphertext string `json:"ciphertext"`
Iv string `json:"iv"`
Tag string `json:"tag"`
Hash string `json:"hash"`
} `json:"secretValue"`
} `json:"secrets"`
Key struct {
EncryptedKey string `json:"encryptedKey"`
Nonce string `json:"nonce"`
Sender struct {
PublicKey string `json:"publicKey"`
} `json:"sender"`
Receiver struct {
RefreshVersion int `json:"refreshVersion"`
ID string `json:"_id"`
Email string `json:"email"`
CustomerID string `json:"customerId"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
V int `json:"__v"`
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
PublicKey string `json:"publicKey"`
} `json:"receiver"`
Workspace string `json:"workspace"`
} `json:"key"`
}
type SingleEnvironmentVariable struct {
Key string `json:"key"`
Value string `json:"value"`
}

View File

@ -0,0 +1,187 @@
package util
import (
"encoding/base64"
"fmt"
"strings"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/crypto"
"github.com/go-resty/resty/v2"
)
type SingleEnvironmentVariable struct {
Key string `json:"key"`
Value string `json:"value"`
Type string `json:"type"`
ID string `json:"_id"`
}
type DecodedSymmetricEncryptionDetails = struct {
Cipher []byte
IV []byte
Tag []byte
Key []byte
}
func VerifyServiceToken(serviceToken string) (string, error) {
serviceTokenParts := strings.SplitN(serviceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return "", fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken = fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
return serviceToken, nil
}
func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsResponse, error) {
serviceTokenParts := strings.SplitN(infisicalToken, ".", 4)
if len(serviceTokenParts) < 4 {
return api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
httpClient := resty.New()
httpClient.SetAuthToken(serviceToken).
SetHeader("Accept", "application/json")
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil {
return api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
return serviceTokenDetails, nil
}
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) {
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
if len(serviceTokenParts) < 4 {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
}
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
httpClient := resty.New()
httpClient.SetAuthToken(serviceToken).
SetHeader("Accept", "application/json")
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
}
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
WorkspaceId: serviceTokenDetails.Workspace,
EnvironmentName: serviceTokenDetails.Environment,
ETag: etag,
})
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, err
}
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
}
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt the required workspace key")
}
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
if err != nil {
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
}
return plainTextSecrets, encryptedSecretsResponse, nil
}
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
cipherx, err := base64.StdEncoding.DecodeString(cipher)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err)
}
keyx, err := base64.StdEncoding.DecodeString(key)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err)
}
IVx, err := base64.StdEncoding.DecodeString(IV)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err)
}
tagx, err := base64.StdEncoding.DecodeString(tag)
if err != nil {
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err)
}
return DecodedSymmetricEncryptionDetails{
Key: keyx,
Cipher: cipherx,
IV: IVx,
Tag: tagx,
}, nil
}
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]SingleEnvironmentVariable, error) {
plainTextSecrets := []SingleEnvironmentVariable{}
for _, secret := range encryptedSecretsResponse.Secrets {
// Decrypt key
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret key")
}
key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret key")
}
key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextKey, err := crypto.DecryptSymmetric(key, key_ciphertext, key_tag, key_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret key")
}
// Decrypt value
value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV)
if err != nil {
return nil, fmt.Errorf("unable to decode secret IV for secret value")
}
value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag)
if err != nil {
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
}
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext)
if err != nil {
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
}
plainTextValue, err := crypto.DecryptSymmetric(key, value_ciphertext, value_tag, value_iv)
if err != nil {
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
}
plainTextSecret := SingleEnvironmentVariable{
Key: string(plainTextKey),
Value: string(plainTextValue),
Type: string(secret.Type),
ID: secret.ID,
}
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
}
return plainTextSecrets, nil
}