feat: allow to override vault secret engine in entity level

Signed-off-by: Ilya Katlinski <ilya.katlinsky@gmail.com>
This commit is contained in:
Ilya Katlinski
2023-08-08 08:59:15 +02:00
parent 7b8c703602
commit 49c3657ac6
7 changed files with 84 additions and 18 deletions
+16 -1
View File
@@ -66,7 +66,7 @@ To get started, first you need a running instance of Vault. You can follow [this
baseUrl: http://your-internal-vault-url.svc
publicUrl: https://your-vault-url.example.com
token: <VAULT_TOKEN>
secretEngine: 'customSecretEngine' # Optional. By default it uses 'secrets'
secretEngine: 'customSecretEngine' # Optional. By default it uses 'secrets'. Can be overwritten by the annotation of the entity
kvVersion: <kv-version> # Optional. The K/V version that your instance is using. The available options are '1' or '2'
```
@@ -106,6 +106,20 @@ The path is relative to your secrets engine folder. So if you want to get the se
You will set the `vault.io/secret-path` to `test/backstage`. If the folder `backstage` contains other sub-folders, the plugin will fetch the secrets inside them and adapt the **View** and **Edit** URLs to point to the correct place.
In case you need to support different secret engines for entities of the catalog you can proivde optional annotion to the entity in `catalog-info.yaml`:
```diff
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
# ...
annotations:
vault.io/secrets-path: path/to/secrets
+ vault.io/secret-engine: customSecretEngine # Optional. By default it uses 'secertEngine' value from configuration.
```
That will overwrite the default secret engine from the configuration.
## Renew token
In a secure Vault instance, it's usual that the tokens are refreshed after some time. In order to always have a valid token to fetch the secrets, it might be necessary to execute a renew action after some time. By default this is deactivated, but it can be easily activated and configured to be executed periodically (hourly by default, but customizable by the user). In order to do that, modify your `src/plugins/vault.ts` file to look like this one:
@@ -138,6 +152,7 @@ export default async function createPlugin(
## Features
- List the secrets present in a certain path
- Use different secret engines for different components
- Open a link to view the secret
- Open a link to edit the secret
- Renew the token automatically with a defined periodicity
@@ -140,11 +140,16 @@ export class VaultBuilder {
router.get('/v1/secrets/:path', async (req, res) => {
const { path } = req.params;
const { engine } = req.query;
if (typeof path !== 'string') {
throw new InputError(`Invalid path: ${path}`);
}
if (engine && typeof engine !== 'string') {
throw new InputError(`Invalid engine: ${engine}`);
}
const secrets = await vaultApi.listSecrets(path);
const secrets = await vaultApi.listSecrets(path, engine);
res.json({ items: secrets });
});
+18 -7
View File
@@ -59,11 +59,17 @@ export interface VaultApi {
* Returns the URL to access the Vault UI with the defined config.
*/
getFrontendSecretsUrl(): string;
/**
* Returns a list of secrets used to show in a table.
* @param secretPath - The path where the secrets are stored in Vault
* @param secretMount - The mount point of the secrets engine, optional, overrides default secret engine
*/
listSecrets(secretPath: string): Promise<VaultSecret[]>;
listSecrets(
secretPath: string,
secretMount?: string | undefined,
): Promise<VaultSecret[]>;
/**
* Optional, to renew the token used to list the secrets. Throws an
* error if the token renewal went wrong.
@@ -115,11 +121,16 @@ export class VaultClient implements VaultApi {
return `${this.vaultConfig.baseUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}`;
}
async listSecrets(secretPath: string): Promise<VaultSecret[]> {
async listSecrets(
secretPath: string,
secretEngine?: string | undefined,
): Promise<VaultSecret[]> {
const mount = secretEngine || this.vaultConfig.secretEngine;
const listUrl =
this.vaultConfig.kvVersion === 2
? `v1/${this.vaultConfig.secretEngine}/metadata/${secretPath}`
: `v1/${this.vaultConfig.secretEngine}/${secretPath}`;
? `v1/${mount}/metadata/${secretPath}`
: `v1/${mount}/${secretPath}`;
const result = await this.limit(() =>
this.callApi<VaultSecretList>(listUrl, { list: true }),
);
@@ -131,7 +142,7 @@ export class VaultClient implements VaultApi {
if (secret.endsWith('/')) {
secrets.push(
...(await this.limit(() =>
this.listSecrets(`${secretPath}/${secret.slice(0, -1)}`),
this.listSecrets(`${secretPath}/${secret.slice(0, -1)}`, mount),
)),
);
} else {
@@ -140,8 +151,8 @@ export class VaultClient implements VaultApi {
secrets.push({
name: secret,
path: secretPath,
editUrl: `${vaultUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/edit/${secretPath}/${secret}`,
showUrl: `${vaultUrl}/ui/vault/secrets/${this.vaultConfig.secretEngine}/show/${secretPath}/${secret}`,
editUrl: `${vaultUrl}/ui/vault/secrets/${mount}/edit/${secretPath}/${secret}`,
showUrl: `${vaultUrl}/ui/vault/secrets/${mount}/show/${secretPath}/${secret}`,
});
}
}),
+17 -1
View File
@@ -42,7 +42,7 @@ To get started, first you need a running instance of Vault. You can follow [this
vault:
baseUrl: http://your-vault-url
token: <VAULT_TOKEN>
secretEngine: 'customSecretEngine' # Optional. By default it uses 'secrets'
secretEngine: 'customSecretEngine' # Optional. By default it uses 'secrets'. Can be overwritten by the annotation of the entity
kvVersion: <kv-version> # Optional. The K/V version that your instance is using. The available options are '1' or '2'
```
@@ -67,6 +67,7 @@ metadata:
# ...
annotations:
vault.io/secrets-path: path/to/secrets
vault.io/secret-engine: customSecretEngine # Optional. By default it uses 'secertEngine' value from configuration.
```
The path is relative to your secrets engine folder. So if you want to get the secrets for backstage and you have the following directory structure:
@@ -86,9 +87,24 @@ If the annotation is missing for a certain component, then the card will show so
![Screenshot of the vault plugin with missing annotation](images/annotation-missing.png)
In case you need to support different secret engines for entities of the catalog you can proivde optional annotion to the entity in `catalog-info.yaml`:
```diff
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
# ...
annotations:
vault.io/secrets-path: path/to/secrets
+ vault.io/secret-engine: customSecretEngine # Optional. By default it uses 'secertEngine' value from configuration.
```
That will overwrite the default secret engine from the configuration.
## Features
- List the secrets present in a certain path
- Use different secret engines for different components
- Open a link to view the secret
- Open a link to edit the secret
- Renew the token automatically with a defined periodicity
+15 -3
View File
@@ -46,8 +46,12 @@ export interface VaultApi {
/**
* Returns a list of secrets used to show in a table.
* @param secretPath - The path where the secrets are stored in Vault
* @param secretMount - The mount point of the secrets engine, optional, overrides default secret engine
*/
listSecrets(secretPath: string): Promise<VaultSecret[]>;
listSecrets(
secretPath: string,
secretMount?: string | undefined,
): Promise<VaultSecret[]>;
}
/**
@@ -90,10 +94,18 @@ export class VaultClient implements VaultApi {
throw await ResponseError.fromResponse(response);
}
async listSecrets(secretPath: string): Promise<VaultSecret[]> {
async listSecrets(
secretPath: string,
secretMount?: string | undefined,
): Promise<VaultSecret[]> {
const query: { [key in string]: any } = {};
if (secretMount) {
query.engine = secretMount;
}
const result = await this.callApi<{ items: VaultSecret[] }>(
`v1/secrets/${encodeURIComponent(secretPath)}`,
{},
query,
);
return result.items;
}
@@ -23,27 +23,33 @@ import Visibility from '@material-ui/icons/Visibility';
import Alert from '@material-ui/lab/Alert';
import useAsync from 'react-use/lib/useAsync';
import { VaultSecret, vaultApiRef } from '../../api';
import { VAULT_SECRET_PATH_ANNOTATION } from '../../constants';
import {
VAULT_SECRET_ENGINE_ANNOTATION,
VAULT_SECRET_PATH_ANNOTATION,
} from '../../constants';
export const vaultSecretPath = (entity: Entity) => {
export const vaultSecretConfig = (entity: Entity) => {
const secretPath =
entity.metadata.annotations?.[VAULT_SECRET_PATH_ANNOTATION];
const secretEngine =
entity.metadata.annotations?.[VAULT_SECRET_ENGINE_ANNOTATION];
return { secretPath };
return { secretPath, secretEngine };
};
export const EntityVaultTable = ({ entity }: { entity: Entity }) => {
const vaultApi = useApi(vaultApiRef);
const { secretPath } = vaultSecretPath(entity);
const { secretPath, secretEngine } = vaultSecretConfig(entity);
if (!secretPath) {
throw Error(
`The secret path is undefined. Please, define the annotation ${VAULT_SECRET_PATH_ANNOTATION}`,
);
}
const { value, loading, error } = useAsync(async (): Promise<
VaultSecret[]
> => {
return vaultApi.listSecrets(secretPath);
return vaultApi.listSecrets(secretPath, secretEngine);
}, []);
const columns: TableColumn[] = [
+1
View File
@@ -17,4 +17,5 @@
/**
* @public
*/
export const VAULT_SECRET_ENGINE_ANNOTATION = 'vault.io/secrets-engine';
export const VAULT_SECRET_PATH_ANNOTATION = 'vault.io/secrets-path';