diff --git a/backend/spec.json b/backend/spec.json index 31ef07373..0b0e275b6 100644 --- a/backend/spec.json +++ b/backend/spec.json @@ -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", diff --git a/backend/swagger/index.ts b/backend/swagger/index.ts index d7a87357f..9f4d4732c 100644 --- a/backend/swagger/index.ts +++ b/backend/swagger/index.ts @@ -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: '', diff --git a/backend/swagger/schemas/index.ts b/backend/swagger/schemas/index.ts deleted file mode 100644 index b0b427fce..000000000 --- a/backend/swagger/schemas/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable @typescript-eslint/no-var-requires */ -const secretSchema = require('./secretSchema.ts'); - -module.exports = { - secretSchema -} \ No newline at end of file diff --git a/backend/swagger/schemas/secretSchema.ts b/backend/swagger/schemas/secretSchema.ts deleted file mode 100644 index 85c6ce017..000000000 --- a/backend/swagger/schemas/secretSchema.ts +++ /dev/null @@ -1,11 +0,0 @@ -const secretSchema = { - _id: { - type: 'string', - format: 'objectId' - }, - version: { - type: 'number' - } -} - -module.exports = secretSchema; \ No newline at end of file diff --git a/docs/api-reference/endpoints/secrets/create.mdx b/docs/api-reference/endpoints/secrets/create.mdx index 27c167f76..2fba642a7 100644 --- a/docs/api-reference/endpoints/secrets/create.mdx +++ b/docs/api-reference/endpoints/secrets/create.mdx @@ -2,3 +2,10 @@ title: "Create" openapi: "POST /api/v2/secrets/" --- + + + 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). + diff --git a/docs/api-reference/endpoints/secrets/read.mdx b/docs/api-reference/endpoints/secrets/read.mdx index c88fb0ec7..bc30ec8f2 100644 --- a/docs/api-reference/endpoints/secrets/read.mdx +++ b/docs/api-reference/endpoints/secrets/read.mdx @@ -2,3 +2,10 @@ title: "Retrieve" openapi: "GET /api/v2/secrets/" --- + + + 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). + diff --git a/docs/api-reference/endpoints/secrets/update.mdx b/docs/api-reference/endpoints/secrets/update.mdx index 2193fccc3..c77a9de12 100644 --- a/docs/api-reference/endpoints/secrets/update.mdx +++ b/docs/api-reference/endpoints/secrets/update.mdx @@ -2,3 +2,10 @@ title: "Update" openapi: "PATCH /api/v2/secrets/" --- + + + 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). + diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index 8cc218253..9c155a96a 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -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) 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. diff --git a/docs/api-reference/overview/examples/create-secrets.mdx b/docs/api-reference/overview/examples/create-secrets.mdx index b2afe1467..319bd9d46 100644 --- a/docs/api-reference/overview/examples/create-secrets.mdx +++ b/docs/api-reference/overview/examples/create-secrets.mdx @@ -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 diff --git a/docs/api-reference/overview/examples/retrieve-secrets.mdx b/docs/api-reference/overview/examples/retrieve-secrets.mdx index 2f34339c5..19d2d777b 100644 --- a/docs/api-reference/overview/examples/retrieve-secrets.mdx +++ b/docs/api-reference/overview/examples/retrieve-secrets.mdx @@ -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 diff --git a/docs/api-reference/overview/examples/update-secrets.mdx b/docs/api-reference/overview/examples/update-secrets.mdx index b8566d9b3..99e3530ee 100644 --- a/docs/api-reference/overview/examples/update-secrets.mdx +++ b/docs/api-reference/overview/examples/update-secrets.mdx @@ -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 diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 7babef955..9865a644a 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -2,11 +2,6 @@ title: "Introduction" --- - - Infisical's REST API is currently unavailable and scheduled to go live on Jan - 16! - - 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. - 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. diff --git a/docs/images/api-key-add.png b/docs/images/api-key-add.png new file mode 100644 index 000000000..a59e59cbe Binary files /dev/null and b/docs/images/api-key-add.png differ diff --git a/docs/images/api-key-dashboard.png b/docs/images/api-key-dashboard.png new file mode 100644 index 000000000..29fb26519 Binary files /dev/null and b/docs/images/api-key-dashboard.png differ diff --git a/docs/images/api-key-settings.png b/docs/images/api-key-settings.png new file mode 100644 index 000000000..847a1e7b3 Binary files /dev/null and b/docs/images/api-key-settings.png differ diff --git a/docs/spec.yaml b/docs/spec.yaml index ced05d577..f715c5d7b 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -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: '' diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index 26a402da3..9ab8e4898 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -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. diff --git a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml index c5b82fa14..e3e3517d5 100644 --- a/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicalsecret-crd.yaml @@ -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 diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index 7940474f9..8f8fbbed9 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -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"` diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml index c885fbdde..2f99ef443 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalsecrets.yaml @@ -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 diff --git a/k8-operator/config/samples/deployment.yaml b/k8-operator/config/samples/deployment.yaml new file mode 100644 index 000000000..c43a9e544 --- /dev/null +++ b/k8-operator/config/samples/deployment.yaml @@ -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 \ No newline at end of file diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/sample.yaml index ad352d3e7..236e55baf 100644 --- a/k8-operator/config/samples/sample.yaml +++ b/k8-operator/config/samples/sample.yaml @@ -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 diff --git a/k8-operator/config/samples/serviceTokenSecret.yaml b/k8-operator/config/samples/serviceTokenSecret.yaml new file mode 100644 index 000000000..3fd2e1b8a --- /dev/null +++ b/k8-operator/config/samples/serviceTokenSecret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: service-token +type: Opaque +data: + infisicalToken: \ No newline at end of file diff --git a/k8-operator/controllers/auto_redeployment.go b/k8-operator/controllers/auto_redeployment.go new file mode 100644 index 000000000..468b6e75d --- /dev/null +++ b/k8-operator/controllers/auto_redeployment.go @@ -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 +} diff --git a/k8-operator/controllers/conditions.go b/k8-operator/controllers/conditions.go new file mode 100644 index 000000000..dea822088 --- /dev/null +++ b/k8-operator/controllers/conditions.go @@ -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") + } +} diff --git a/k8-operator/controllers/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret_controller.go index f6422e177..f32cd0a2e 100644 --- a/k8-operator/controllers/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret_controller.go @@ -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) } diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret_helper.go index be4e35dd5..8428cbec1 100644 --- a/k8-operator/controllers/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret_helper.go @@ -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") - } -} diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml index 6a3cb8e6d..7862fe7c3 100644 --- a/k8-operator/kubectl-install/install-secrets-operator.yaml +++ b/k8-operator/kubectl-install/install-secrets-operator.yaml @@ -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 diff --git a/k8-operator/packages/api/api.go b/k8-operator/packages/api/api.go index 64f8add46..46969e320 100644 --- a/k8-operator/packages/api/api.go +++ b/k8-operator/packages/api/api.go @@ -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 } diff --git a/k8-operator/packages/api/models.go b/k8-operator/packages/api/models.go new file mode 100644 index 000000000..a03f9fee7 --- /dev/null +++ b/k8-operator/packages/api/models.go @@ -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"` +} diff --git a/k8-operator/packages/api/variables.go b/k8-operator/packages/api/variables.go new file mode 100644 index 000000000..214b7e1d8 --- /dev/null +++ b/k8-operator/packages/api/variables.go @@ -0,0 +1,3 @@ +package api + +var API_HOST_URL string = "https://app.infisical.com/api" diff --git a/k8-operator/packages/models/api.go b/k8-operator/packages/models/api.go deleted file mode 100644 index 57420dac5..000000000 --- a/k8-operator/packages/models/api.go +++ /dev/null @@ -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"` -} diff --git a/k8-operator/packages/util/secrets.go b/k8-operator/packages/util/secrets.go new file mode 100644 index 000000000..c68f76250 --- /dev/null +++ b/k8-operator/packages/util/secrets.go @@ -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 +}