This commit is contained in:
Tuan Dang
2023-01-16 10:53:16 +07:00
33 changed files with 770 additions and 456 deletions
+32 -32
View File
@@ -3582,39 +3582,39 @@
}, },
"email": { "email": {
"type": "string", "type": "string",
"example": "" "example": "[email protected]"
}, },
"firstName": { "firstName": {
"type": "string", "type": "string",
"example": "" "example": "John"
}, },
"lastName": { "lastName": {
"type": "string", "type": "string",
"example": "" "example": "Doe"
}, },
"publicKey": { "publicKey": {
"type": "string", "type": "string",
"example": "" "example": "johns_nacl_public_key"
}, },
"encryptedPrivateKey": { "encryptedPrivateKey": {
"type": "string", "type": "string",
"example": "" "example": "johns_enc_nacl_private_key"
}, },
"iv": { "iv": {
"type": "string", "type": "string",
"example": "" "example": "iv_of_enc_nacl_private_key"
}, },
"tag": { "tag": {
"type": "string", "type": "string",
"example": "" "example": "tag_of_enc_nacl_private_key"
}, },
"updatedAt": { "updatedAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
}, },
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
} }
} }
}, },
@@ -3630,27 +3630,27 @@
}, },
"email": { "email": {
"type": "string", "type": "string",
"example": "" "example": "[email protected]"
}, },
"firstName": { "firstName": {
"type": "string", "type": "string",
"example": "" "example": "John"
}, },
"lastName": { "lastName": {
"type": "string", "type": "string",
"example": "" "example": "Doe"
}, },
"publicKey": { "publicKey": {
"type": "string", "type": "string",
"example": "" "example": "johns_nacl_public_key"
}, },
"updatedAt": { "updatedAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
}, },
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
} }
} }
}, },
@@ -3676,27 +3676,27 @@
}, },
"email": { "email": {
"type": "string", "type": "string",
"example": "" "example": "[email protected]"
}, },
"firstName": { "firstName": {
"type": "string", "type": "string",
"example": "" "example": "John"
}, },
"lastName": { "lastName": {
"type": "string", "type": "string",
"example": "" "example": "Doe"
}, },
"publicKey": { "publicKey": {
"type": "string", "type": "string",
"example": "" "example": "johns_nacl_public_key"
}, },
"updatedAt": { "updatedAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
}, },
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
} }
} }
}, },
@@ -3723,7 +3723,7 @@
}, },
"name": { "name": {
"type": "string", "type": "string",
"example": "" "example": "Acme Corp."
}, },
"customerId": { "customerId": {
"type": "string", "type": "string",
@@ -3776,7 +3776,7 @@
"properties": { "properties": {
"publicKey": { "publicKey": {
"type": "string", "type": "string",
"example": "" "example": "senders_nacl_public_key"
} }
} }
}, },
@@ -3938,11 +3938,11 @@
}, },
"updatedAt": { "updatedAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
}, },
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
} }
} }
}, },
@@ -3962,15 +3962,15 @@
}, },
"email": { "email": {
"type": "string", "type": "string",
"example": "" "example": "[email protected]"
}, },
"firstName": { "firstName": {
"type": "string", "type": "string",
"example": "" "example": "John"
}, },
"lastName": { "lastName": {
"type": "string", "type": "string",
"example": "" "example": "Doe"
} }
} }
}, },
@@ -4033,11 +4033,11 @@
}, },
"updatedAt": { "updatedAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
}, },
"createdAt": { "createdAt": {
"type": "string", "type": "string",
"example": "" "example": "2023-01-13T14:16:12.210Z"
} }
} }
}, },
@@ -4087,7 +4087,7 @@
}, },
"type": { "type": {
"type": "string", "type": "string",
"example": "" "example": "shared"
}, },
"user": { "user": {
"type": "string", "type": "string",
@@ -4095,7 +4095,7 @@
}, },
"environment": { "environment": {
"type": "string", "type": "string",
"example": "" "example": "dev"
}, },
"isDeleted": { "isDeleted": {
"type": "string", "type": "string",
+32 -33
View File
@@ -2,7 +2,6 @@
const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' });
const fs = require('fs').promises; const fs = require('fs').promises;
const yaml = require('js-yaml'); const yaml = require('js-yaml');
const { secretSchema } = require('./schemas/index.ts');
/** /**
* Generates OpenAPI specs for all Infisical API endpoints: * Generates OpenAPI specs for all Infisical API endpoints:
@@ -43,25 +42,25 @@ const generateOpenAPISpec = async () => {
definitions: { definitions: {
CurrentUser: { CurrentUser: {
_id: '', _id: '',
email: '', email: '[email protected]',
firstName: '', firstName: 'John',
lastName: '', lastName: 'Doe',
publicKey: '', publicKey: 'johns_nacl_public_key',
encryptedPrivateKey: '', encryptedPrivateKey: 'johns_enc_nacl_private_key',
iv: '', iv: 'iv_of_enc_nacl_private_key',
tag: '', tag: 'tag_of_enc_nacl_private_key',
updatedAt: '', updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '' createdAt: '2023-01-13T14:16:12.210Z'
}, },
Membership: { Membership: {
user: { user: {
_id: '', _id: '',
email: '', email: '[email protected]',
firstName: '', firstName: 'John',
lastName: '', lastName: 'Doe',
publicKey: '', publicKey: 'johns_nacl_public_key',
updatedAt: '', updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '' createdAt: '2023-01-13T14:16:12.210Z'
}, },
workspace: '', workspace: '',
role: 'admin' role: 'admin'
@@ -69,12 +68,12 @@ const generateOpenAPISpec = async () => {
MembershipOrg: { MembershipOrg: {
user: { user: {
_id: '', _id: '',
email: '', email: '[email protected]',
firstName: '', firstName: 'John',
lastName: '', lastName: 'Doe',
publicKey: '', publicKey: 'johns_nacl_public_key',
updatedAt: '', updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '' createdAt: '2023-01-13T14:16:12.210Z'
}, },
organization: '', organization: '',
role: 'owner', role: 'owner',
@@ -82,7 +81,7 @@ const generateOpenAPISpec = async () => {
}, },
Organization: { Organization: {
_id: '', _id: '',
name: '', name: 'Acme Corp.',
customerId: '' customerId: ''
}, },
Project: { Project: {
@@ -97,7 +96,7 @@ const generateOpenAPISpec = async () => {
encryptedkey: '', encryptedkey: '',
nonce: '', nonce: '',
sender: { sender: {
publicKey: '' publicKey: 'senders_nacl_public_key'
}, },
receiver: '', receiver: '',
workspace: '' workspace: ''
@@ -141,16 +140,16 @@ const generateOpenAPISpec = async () => {
secretCommentCiphertext: '', secretCommentCiphertext: '',
secretCommentIV: '', secretCommentIV: '',
secretCommentTag: '', secretCommentTag: '',
updatedAt: '', updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '' createdAt: '2023-01-13T14:16:12.210Z'
}, },
Log: { Log: {
_id: '', _id: '',
user: { user: {
_id: '', _id: '',
email: '', email: '[email protected]',
firstName: '', firstName: 'John',
lastName: '' lastName: 'Doe'
}, },
workspace: '', workspace: '',
actionNames: [ actionNames: [
@@ -171,8 +170,8 @@ const generateOpenAPISpec = async () => {
], ],
channel: 'cli', channel: 'cli',
ipAddress: '192.168.0.1', ipAddress: '192.168.0.1',
updatedAt: '', updatedAt: '2023-01-13T14:16:12.210Z',
createdAt: '' createdAt: '2023-01-13T14:16:12.210Z'
}, },
SecretSnapshot: { SecretSnapshot: {
workspace: '', workspace: '',
@@ -188,9 +187,9 @@ const generateOpenAPISpec = async () => {
secret: '', secret: '',
version: 1, version: 1,
workspace: '', workspace: '',
type: '', type: 'shared',
user: '', user: '',
environment: '', environment: 'dev',
isDeleted: '', isDeleted: '',
secretKeyCiphertext: '', secretKeyCiphertext: '',
secretKeyIV: '', secretKeyIV: '',
-6
View File
@@ -1,6 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const secretSchema = require('./secretSchema.ts');
module.exports = {
secretSchema
}
-11
View File
@@ -1,11 +0,0 @@
const secretSchema = {
_id: {
type: 'string',
format: 'objectId'
},
version: {
type: 'number'
}
}
module.exports = secretSchema;
@@ -2,3 +2,10 @@
title: "Create" title: "Create"
openapi: "POST /api/v2/secrets/" 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>
@@ -2,3 +2,10 @@
title: "Retrieve" title: "Retrieve"
openapi: "GET /api/v2/secrets/" 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>
@@ -2,3 +2,10 @@
title: "Update" title: "Update"
openapi: "PATCH /api/v2/secrets/" 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>
@@ -2,10 +2,14 @@
title: "Authentication" 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> <Info>
It's important to keep your API key secure, as it grants access to your 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 secrets in Infisical. For added security, set a reasonable expiration time and
regular basis. rotate your API key on a regular basis.
</Info> </Info>
@@ -11,12 +11,12 @@ Prerequisites:
## Flow ## 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. 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. 4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project 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 ## Example
@@ -11,11 +11,11 @@ Prerequisites:
## Flow ## 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. 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. 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 6. Decrypt the (encrypted) secrets
## Example ## Example
@@ -11,12 +11,12 @@ Prerequisites:
## Flow ## 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. 2. Decrypt your (encrypted) private key with your password.
3. Get the project key for the project. 3. [Get the (encrypted) project key for the project.](/api-reference/endpoints/workspaces/workspace-key)
4. Decrypt the project key with your private key. 4. Decrypt the (encrypted) project key with your private key.
5. Encrypt your secret(s) with the project 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 ## Example
+5 -9
View File
@@ -2,11 +2,6 @@
title: "Introduction" 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 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 secrets via HTTPS requests. This can be useful for automating tasks, such as
rotating credentials, or for integrating secret management into a larger system. 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 uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations.
<Info> <Info>
Infisical's system ensures greater security such that secrets are Infisical's system requires that secrets be encrypted/decrypted on the
encrypted/decrypted on the client-side but requires users to properly client-side to maintain E2EE. We strongly recommend you read up on the system
implement cryptographic operations to maintain end-to-end encryption (E2EE). prior to using the Infisical API. The (opt-in) ability to retrieve secrets
We're back in decrypted format if you choose to share secrets with Infisical is on
our roadmap.
</Info> </Info>
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

+32 -32
View File
@@ -2208,31 +2208,31 @@ components:
example: '' example: ''
email: email:
type: string type: string
example: '' example: [email protected]
firstName: firstName:
type: string type: string
example: '' example: John
lastName: lastName:
type: string type: string
example: '' example: Doe
publicKey: publicKey:
type: string type: string
example: '' example: johns_nacl_public_key
encryptedPrivateKey: encryptedPrivateKey:
type: string type: string
example: '' example: johns_enc_nacl_private_key
iv: iv:
type: string type: string
example: '' example: iv_of_enc_nacl_private_key
tag: tag:
type: string type: string
example: '' example: tag_of_enc_nacl_private_key
updatedAt: updatedAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
createdAt: createdAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
Membership: Membership:
type: object type: object
properties: properties:
@@ -2244,22 +2244,22 @@ components:
example: '' example: ''
email: email:
type: string type: string
example: '' example: [email protected]
firstName: firstName:
type: string type: string
example: '' example: John
lastName: lastName:
type: string type: string
example: '' example: Doe
publicKey: publicKey:
type: string type: string
example: '' example: johns_nacl_public_key
updatedAt: updatedAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
createdAt: createdAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
workspace: workspace:
type: string type: string
example: '' example: ''
@@ -2277,22 +2277,22 @@ components:
example: '' example: ''
email: email:
type: string type: string
example: '' example: [email protected]
firstName: firstName:
type: string type: string
example: '' example: John
lastName: lastName:
type: string type: string
example: '' example: Doe
publicKey: publicKey:
type: string type: string
example: '' example: johns_nacl_public_key
updatedAt: updatedAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
createdAt: createdAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
organization: organization:
type: string type: string
example: '' example: ''
@@ -2310,7 +2310,7 @@ components:
example: '' example: ''
name: name:
type: string type: string
example: '' example: Acme Corp.
customerId: customerId:
type: string type: string
example: '' example: ''
@@ -2348,7 +2348,7 @@ components:
properties: properties:
publicKey: publicKey:
type: string type: string
example: '' example: senders_nacl_public_key
receiver: receiver:
type: string type: string
example: '' example: ''
@@ -2466,10 +2466,10 @@ components:
example: '' example: ''
updatedAt: updatedAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
createdAt: createdAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
Log: Log:
type: object type: object
properties: properties:
@@ -2484,13 +2484,13 @@ components:
example: '' example: ''
email: email:
type: string type: string
example: '' example: [email protected]
firstName: firstName:
type: string type: string
example: '' example: John
lastName: lastName:
type: string type: string
example: '' example: Doe
workspace: workspace:
type: string type: string
example: '' example: ''
@@ -2533,10 +2533,10 @@ components:
example: 192.168.0.1 example: 192.168.0.1
updatedAt: updatedAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
createdAt: createdAt:
type: string type: string
example: '' example: '2023-01-13T14:16:12.210Z'
SecretSnapshot: SecretSnapshot:
type: object type: object
properties: properties:
@@ -2571,13 +2571,13 @@ components:
example: '' example: ''
type: type:
type: string type: string
example: '' example: shared
user: user:
type: string type: string
example: '' example: ''
environment: environment:
type: string type: string
example: '' example: dev
isDeleted: isDeleted:
type: string type: string
example: '' example: ''
+1 -1
View File
@@ -13,7 +13,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes # 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. # to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/) # 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 # 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 # 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. # follow Semantic Versioning. They should reflect the version the application is using.
@@ -35,9 +35,6 @@ spec:
spec: spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties: properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI: hostAPI:
default: https://app.infisical.com/api default: https://app.infisical.com/api
description: Infisical host to pull secrets from description: Infisical host to pull secrets from
@@ -54,9 +51,6 @@ spec:
- secretName - secretName
- secretNamespace - secretNamespace
type: object type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference: tokenSecretReference:
properties: properties:
secretName: secretName:
@@ -69,9 +63,6 @@ spec:
- secretName - secretName
- secretNamespace - secretNamespace
type: object type: object
required:
- environment
- projectId
type: object type: object
status: status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret description: InfisicalSecretStatus defines the observed state of InfisicalSecret
@@ -16,17 +16,11 @@ type KubeSecretReference struct {
// InfisicalSecretSpec defines the desired state of InfisicalSecret // InfisicalSecretSpec defines the desired state of InfisicalSecret
type InfisicalSecretSpec struct { type InfisicalSecretSpec struct {
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"` // +kubebuilder:validation:Required
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
// +kubebuilder:validation:Required
ManagedSecretReference KubeSecretReference `json:"managedSecretReference,omitempty"` 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 // Infisical host to pull secrets from
// +kubebuilder:default="https://app.infisical.com/api" // +kubebuilder:default="https://app.infisical.com/api"
HostAPI string `json:"hostAPI,omitempty"` HostAPI string `json:"hostAPI,omitempty"`
@@ -35,9 +35,6 @@ spec:
spec: spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties: properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI: hostAPI:
default: https://app.infisical.com/api default: https://app.infisical.com/api
description: Infisical host to pull secrets from description: Infisical host to pull secrets from
@@ -54,9 +51,6 @@ spec:
- secretName - secretName
- secretNamespace - secretNamespace
type: object type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference: tokenSecretReference:
properties: properties:
secretName: secretName:
@@ -69,9 +63,6 @@ spec:
- secretName - secretName
- secretNamespace - secretNamespace
type: object type: object
required:
- environment
- projectId
type: object type: object
status: status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret description: InfisicalSecretStatus defines the observed state of InfisicalSecret
@@ -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
+3 -4
View File
@@ -3,11 +3,10 @@ kind: InfisicalSecret
metadata: metadata:
name: infisicalsecret-sample name: infisicalsecret-sample
spec: spec:
projectId: 62faf98ae0b05e8529b5da46 hostAPI: https://app.infisical.com/api
environment: dev
tokenSecretReference: tokenSecretReference:
secretName: service-token secretName: service-token
secretNamespace: first-project secretNamespace: default
managedSecretReference: managedSecretReference:
secretName: managed-secret secretName: managed-secret
secretNamespace: first-project secretNamespace: default
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: service-token
type: Opaque
data:
infisicalToken: <base64 infisical token here>
@@ -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
}
+95
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")
}
}
@@ -2,16 +2,17 @@ package controllers
import ( import (
"context" "context"
"fmt"
"time" "time"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime" ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
secretsv1alpha1 "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 // InfisicalSecretReconciler reconciles a InfisicalSecret object
@@ -31,19 +32,18 @@ type InfisicalSecretReconciler struct {
// For more details, check Reconcile and its Result here: // For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile // - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { 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 var infisicalSecretCR v1alpha1.InfisicalSecret
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR) err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
requeueTime := time.Minute * 5
if err != nil { if err != nil {
if errors.IsNotFound(err) { if errors.IsNotFound(err) {
log.Info("Infisical Secret not found") fmt.Printf("Infisical Secret CRD not found [err=%v]", err)
return ctrl.Result{}, nil return ctrl.Result{
Requeue: false,
}, nil
} else { } 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{ return ctrl.Result{
RequeueAfter: requeueTime, RequeueAfter: requeueTime,
}, nil }, nil
@@ -52,13 +52,28 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// Check if the resource is already marked for deletion // Check if the resource is already marked for deletion
if infisicalSecretCR.GetDeletionTimestamp() != nil { 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) err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err) r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
if err != nil { 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{ return ctrl.Result{
RequeueAfter: requeueTime, RequeueAfter: requeueTime,
}, nil }, nil
@@ -73,6 +88,6 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// SetupWithManager sets up the controller with the Manager. // SetupWithManager sets up the controller with the Manager.
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr). return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}). // TODO we should also be watching secrets with the name specifed For(&secretsv1alpha1.InfisicalSecret{}).
Complete(r) Complete(r)
} }
@@ -6,16 +6,16 @@ import (
"strings" "strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
api "github.com/Infisical/infisical/k8-operator/packages/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/util"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
) )
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" 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) { func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) {
kubeSecret := &corev1.Secret{} kubeSecret := &corev1.Secret{}
@@ -27,14 +27,14 @@ func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Co
return kubeSecret, err 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{ tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace, Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.TokenSecretReference.SecretName, Name: infisicalSecret.Spec.TokenSecretReference.SecretName,
}) })
if err != nil { 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] 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 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) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
@@ -56,6 +56,9 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName, Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Annotations: map[string]string{
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
},
}, },
Type: "Opaque", Type: "Opaque",
Data: plainProcessedSecrets, Data: plainProcessedSecrets,
@@ -70,13 +73,17 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil 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) plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI { for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) plainProcessedSecrets[secret.Key] = []byte(secret.Value)
} }
managedKubeSecret.Data = plainProcessedSecrets managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations = map[string]string{
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
}
err := r.Client.Update(ctx, &managedKubeSecret) err := r.Client.Update(ctx, &managedKubeSecret)
if err != nil { if err != nil {
return fmt.Errorf("unable to update Kubernetes secret because [%w]", err) 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 { 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) r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
if err != nil { if err != nil {
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err) 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{ managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName, Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, 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) 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 { if managedKubeSecret != nil {
return err 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 { if managedKubeSecret == nil {
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, secretsFromApi) return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
} else { } 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")
}
}
@@ -43,9 +43,6 @@ spec:
spec: spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties: properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI: hostAPI:
default: https://app.infisical.com/api default: https://app.infisical.com/api
description: Infisical host to pull secrets from description: Infisical host to pull secrets from
@@ -62,9 +59,6 @@ spec:
- secretName - secretName
- secretNamespace - secretNamespace
type: object type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference: tokenSecretReference:
properties: properties:
secretName: secretName:
@@ -77,9 +71,6 @@ spec:
- secretName - secretName
- secretNamespace - secretNamespace
type: object type: object
required:
- environment
- projectId
type: object type: object
status: status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret description: InfisicalSecretStatus defines the observed state of InfisicalSecret
+50 -147
View File
@@ -1,177 +1,80 @@
package api package api
import ( import (
"encoding/base64"
"errors"
"fmt" "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" "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) { const USER_AGENT_NAME = "k8-operator"
envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId, hostAPI)
if err != nil {
return nil, err
}
return SubstituteSecrets(envsFromApi), nil 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
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
response, err := httpClient. response, err := httpClient.
R(). R().
SetQueryParam("environment", envName). SetResult(&result).
SetQueryParam("channel", "cli"). SetHeader("User-Agent", USER_AGENT_NAME).
SetResult(&pullSecretsByInfisicalTokenResponse). Get(endpoint)
Get(fmt.Sprintf("%v/v1/secret/%v/service-token", hostAPI, projectId))
if err != nil { if err != nil {
return nil, err return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
} }
if response.StatusCode() > 299 { if response.StatusCode() > 299 {
return nil, fmt.Errorf(response.Status()) return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
} }
// Get workspace key return result, nil
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
} }
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string { func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found { var tokenDetailsResponse GetServiceTokenDetailsResponse
return value 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 response.IsError() {
if secret.Key == variableWeAreLookingFor { return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
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
}
} }
return "${" + variableWeAreLookingFor + "}" return tokenDetailsResponse, nil
} }
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable { func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
hashMapOfCompleteVariables := make(map[string]string) var secretsResponse GetEncryptedSecretsV2Response = GetEncryptedSecretsV2Response{}
hashMapOfSelfRefs := make(map[string]string) createHttpRequest := httpClient.
expandedSecrets := []models.SingleEnvironmentVariable{} R().
SetResult(&secretsResponse.Secrets).
for _, secret := range secrets { SetQueryParam("environment", request.EnvironmentName).
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs) SetHeader("User-Agent", USER_AGENT_NAME)
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
Key: secret.Key,
Value: expandedVariable,
})
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
} }
+75
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"`
}
+3
View File
@@ -0,0 +1,3 @@
package api
var API_HOST_URL string = "https://app.infisical.com/api"
-51
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"`
}
+187
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
}