env-secrets 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +12 -3
- package/README.md +52 -8
- package/__e2e__/README.md +2 -5
- package/__e2e__/index.test.ts +341 -2
- package/__e2e__/utils/test-utils.ts +61 -1
- package/__tests__/cli/helpers.test.ts +217 -0
- package/__tests__/vaults/aws-config.test.ts +85 -0
- package/__tests__/vaults/secretsmanager-admin.test.ts +355 -0
- package/dist/cli/helpers.js +172 -0
- package/dist/index.js +456 -2
- package/dist/vaults/aws-config.js +29 -0
- package/dist/vaults/secretsmanager-admin.js +273 -0
- package/dist/vaults/secretsmanager.js +8 -16
- package/docs/AWS.md +129 -2
- package/jest.e2e.config.js +1 -0
- package/package.json +5 -5
- package/src/cli/helpers.ts +239 -0
- package/src/index.ts +595 -2
- package/src/vaults/aws-config.ts +51 -0
- package/src/vaults/secretsmanager-admin.ts +397 -0
- package/src/vaults/secretsmanager.ts +8 -21
- package/website/docs/cli-reference.mdx +101 -0
- package/website/docs/providers/aws-secrets-manager.mdx +59 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CreateSecretCommand,
|
|
3
|
+
DeleteSecretCommand,
|
|
4
|
+
DescribeSecretCommand,
|
|
5
|
+
GetSecretValueCommand,
|
|
6
|
+
ListSecretsCommand,
|
|
7
|
+
SecretsManagerClient,
|
|
8
|
+
Tag,
|
|
9
|
+
UpdateSecretCommand
|
|
10
|
+
} from '@aws-sdk/client-secrets-manager';
|
|
11
|
+
import { GetCallerIdentityCommand, STSClient } from '@aws-sdk/client-sts';
|
|
12
|
+
import Debug from 'debug';
|
|
13
|
+
import { buildAwsClientConfig } from './aws-config';
|
|
14
|
+
|
|
15
|
+
const debug = Debug('env-secrets:secretsmanager-admin');
|
|
16
|
+
|
|
17
|
+
export interface AwsSecretCommandOptions {
|
|
18
|
+
profile?: string;
|
|
19
|
+
region?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface SecretCreateOptions extends AwsSecretCommandOptions {
|
|
23
|
+
name: string;
|
|
24
|
+
value: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
kmsKeyId?: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SecretUpdateOptions extends AwsSecretCommandOptions {
|
|
31
|
+
name: string;
|
|
32
|
+
value?: string;
|
|
33
|
+
description?: string;
|
|
34
|
+
kmsKeyId?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SecretListOptions extends AwsSecretCommandOptions {
|
|
38
|
+
prefix?: string;
|
|
39
|
+
tags?: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface SecretDeleteOptions extends AwsSecretCommandOptions {
|
|
43
|
+
name: string;
|
|
44
|
+
recoveryDays?: number;
|
|
45
|
+
forceDeleteWithoutRecovery?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface SecretSummary {
|
|
49
|
+
name: string;
|
|
50
|
+
arn?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
lastChangedDate?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SecretMetadata {
|
|
56
|
+
name?: string;
|
|
57
|
+
arn?: string;
|
|
58
|
+
description?: string;
|
|
59
|
+
kmsKeyId?: string;
|
|
60
|
+
deletedDate?: string;
|
|
61
|
+
lastChangedDate?: string;
|
|
62
|
+
lastAccessedDate?: string;
|
|
63
|
+
createdDate?: string;
|
|
64
|
+
versionIdsToStages?: Record<string, string[]>;
|
|
65
|
+
tags?: Record<string, string>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface AWSLikeError {
|
|
69
|
+
name?: string;
|
|
70
|
+
message?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Allowed characters are documented by AWS Secrets Manager naming rules.
|
|
74
|
+
// See: https://docs.aws.amazon.com/secretsmanager/latest/userguide/reference_limits.html
|
|
75
|
+
const SECRET_NAME_PATTERN = /^[A-Za-z0-9/_+=.@-]+$/;
|
|
76
|
+
|
|
77
|
+
const formatDate = (value?: Date): string | undefined => {
|
|
78
|
+
if (!value) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return value.toISOString();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const parseTags = (tags?: string[]): Tag[] | undefined => {
|
|
86
|
+
if (!tags || tags.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return tags.map((tag) => {
|
|
91
|
+
const parts = tag.split('=');
|
|
92
|
+
if (parts.length < 2) {
|
|
93
|
+
throw new Error(`Invalid tag format: ${tag}. Use key=value.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const key = parts[0].trim();
|
|
97
|
+
const value = parts.slice(1).join('=').trim();
|
|
98
|
+
|
|
99
|
+
if (!key || !value) {
|
|
100
|
+
throw new Error(`Invalid tag format: ${tag}. Use key=value.`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { Key: key, Value: value };
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const tagsToRecord = (tags?: Tag[]): Record<string, string> | undefined => {
|
|
108
|
+
if (!tags || tags.length === 0) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const result: Record<string, string> = {};
|
|
113
|
+
for (const tag of tags) {
|
|
114
|
+
if (tag.Key && tag.Value) {
|
|
115
|
+
result[tag.Key] = tag.Value;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const mapAwsError = (error: unknown, secretName?: string): never => {
|
|
123
|
+
const awsError = error as AWSLikeError;
|
|
124
|
+
const secretLabel = secretName ? ` for "${secretName}"` : '';
|
|
125
|
+
|
|
126
|
+
if (awsError?.name === 'AlreadyExistsException') {
|
|
127
|
+
throw new Error(`Secret${secretLabel} already exists.`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (awsError?.name === 'ResourceNotFoundException') {
|
|
131
|
+
throw new Error(`Secret${secretLabel} was not found.`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (awsError?.name === 'InvalidRequestException') {
|
|
135
|
+
throw new Error(
|
|
136
|
+
awsError.message || 'Invalid request to AWS Secrets Manager.'
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (awsError?.name === 'AccessDeniedException') {
|
|
141
|
+
throw new Error(
|
|
142
|
+
awsError.message ||
|
|
143
|
+
'Access denied while calling AWS Secrets Manager. Verify IAM permissions.'
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (awsError?.message) {
|
|
148
|
+
throw new Error(awsError.message);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
throw new Error(String(error));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const ensureConnected = async (
|
|
155
|
+
clientConfig: ReturnType<typeof buildAwsClientConfig>
|
|
156
|
+
) => {
|
|
157
|
+
const stsClient = new STSClient(clientConfig);
|
|
158
|
+
await stsClient.send(new GetCallerIdentityCommand({}));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const createClient = async (options: AwsSecretCommandOptions) => {
|
|
162
|
+
const config = buildAwsClientConfig(options);
|
|
163
|
+
debug('Creating AWS clients', {
|
|
164
|
+
hasProfile: Boolean(options.profile),
|
|
165
|
+
region: options.region,
|
|
166
|
+
hasEndpoint: Boolean(config.endpoint)
|
|
167
|
+
});
|
|
168
|
+
await ensureConnected(config);
|
|
169
|
+
return new SecretsManagerClient(config);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const validateSecretName = (name: string) => {
|
|
173
|
+
if (!SECRET_NAME_PATTERN.test(name)) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
`Invalid secret name "${name}". Use only letters, numbers, and /_+=.@- characters.`
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export const createSecret = async (
|
|
181
|
+
options: SecretCreateOptions
|
|
182
|
+
): Promise<{ arn?: string; name?: string; versionId?: string }> => {
|
|
183
|
+
validateSecretName(options.name);
|
|
184
|
+
debug('createSecret called', {
|
|
185
|
+
name: options.name,
|
|
186
|
+
hasTags: !!options.tags?.length
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const client = await createClient(options);
|
|
190
|
+
const tags = parseTags(options.tags);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const result = await client.send(
|
|
194
|
+
new CreateSecretCommand({
|
|
195
|
+
Name: options.name,
|
|
196
|
+
Description: options.description,
|
|
197
|
+
SecretString: options.value,
|
|
198
|
+
KmsKeyId: options.kmsKeyId,
|
|
199
|
+
Tags: tags
|
|
200
|
+
})
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
arn: result.ARN,
|
|
205
|
+
name: result.Name,
|
|
206
|
+
versionId: result.VersionId
|
|
207
|
+
};
|
|
208
|
+
} catch (error: unknown) {
|
|
209
|
+
return mapAwsError(error, options.name);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const updateSecret = async (
|
|
214
|
+
options: SecretUpdateOptions
|
|
215
|
+
): Promise<{ arn?: string; name?: string; versionId?: string }> => {
|
|
216
|
+
validateSecretName(options.name);
|
|
217
|
+
debug('updateSecret called', { name: options.name });
|
|
218
|
+
|
|
219
|
+
const client = await createClient(options);
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const result = await client.send(
|
|
223
|
+
new UpdateSecretCommand({
|
|
224
|
+
SecretId: options.name,
|
|
225
|
+
Description: options.description,
|
|
226
|
+
SecretString: options.value,
|
|
227
|
+
KmsKeyId: options.kmsKeyId
|
|
228
|
+
})
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
arn: result.ARN,
|
|
233
|
+
name: result.Name,
|
|
234
|
+
versionId: result.VersionId
|
|
235
|
+
};
|
|
236
|
+
} catch (error: unknown) {
|
|
237
|
+
return mapAwsError(error, options.name);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
export const listSecrets = async (
|
|
242
|
+
options: SecretListOptions
|
|
243
|
+
): Promise<SecretSummary[]> => {
|
|
244
|
+
debug('listSecrets called', {
|
|
245
|
+
prefix: options.prefix,
|
|
246
|
+
hasTags: !!options.tags?.length
|
|
247
|
+
});
|
|
248
|
+
const client = await createClient(options);
|
|
249
|
+
const requiredTags = parseTags(options.tags);
|
|
250
|
+
const secrets: SecretSummary[] = [];
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
let nextToken: string | undefined;
|
|
254
|
+
|
|
255
|
+
do {
|
|
256
|
+
const result = await client.send(
|
|
257
|
+
new ListSecretsCommand({ NextToken: nextToken })
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
for (const secret of result.SecretList || []) {
|
|
261
|
+
if (
|
|
262
|
+
options.prefix &&
|
|
263
|
+
secret.Name &&
|
|
264
|
+
!secret.Name.startsWith(options.prefix)
|
|
265
|
+
) {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (requiredTags && requiredTags.length > 0) {
|
|
270
|
+
const available = tagsToRecord(secret.Tags);
|
|
271
|
+
const matchesAll = requiredTags.every(
|
|
272
|
+
(tag) => tag.Key && tag.Value && available?.[tag.Key] === tag.Value
|
|
273
|
+
);
|
|
274
|
+
if (!matchesAll) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
secrets.push({
|
|
280
|
+
name: secret.Name || '',
|
|
281
|
+
arn: secret.ARN,
|
|
282
|
+
description: secret.Description,
|
|
283
|
+
lastChangedDate: formatDate(secret.LastChangedDate)
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
nextToken = result.NextToken;
|
|
288
|
+
} while (nextToken);
|
|
289
|
+
} catch (error: unknown) {
|
|
290
|
+
return mapAwsError(error);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return secrets;
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
export const getSecretMetadata = async (
|
|
297
|
+
options: AwsSecretCommandOptions & { name: string }
|
|
298
|
+
): Promise<SecretMetadata> => {
|
|
299
|
+
validateSecretName(options.name);
|
|
300
|
+
debug('getSecretMetadata called', { name: options.name });
|
|
301
|
+
const client = await createClient(options);
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
const result = await client.send(
|
|
305
|
+
new DescribeSecretCommand({ SecretId: options.name })
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
name: result.Name,
|
|
310
|
+
arn: result.ARN,
|
|
311
|
+
description: result.Description,
|
|
312
|
+
kmsKeyId: result.KmsKeyId,
|
|
313
|
+
deletedDate: formatDate(result.DeletedDate),
|
|
314
|
+
lastChangedDate: formatDate(result.LastChangedDate),
|
|
315
|
+
lastAccessedDate: formatDate(result.LastAccessedDate),
|
|
316
|
+
createdDate: formatDate(result.CreatedDate),
|
|
317
|
+
versionIdsToStages: result.VersionIdsToStages,
|
|
318
|
+
tags: tagsToRecord(result.Tags)
|
|
319
|
+
};
|
|
320
|
+
} catch (error: unknown) {
|
|
321
|
+
return mapAwsError(error, options.name);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const secretExists = async (
|
|
326
|
+
options: AwsSecretCommandOptions & { name: string }
|
|
327
|
+
): Promise<boolean> => {
|
|
328
|
+
validateSecretName(options.name);
|
|
329
|
+
debug('secretExists called', { name: options.name });
|
|
330
|
+
const client = await createClient(options);
|
|
331
|
+
|
|
332
|
+
try {
|
|
333
|
+
await client.send(new DescribeSecretCommand({ SecretId: options.name }));
|
|
334
|
+
return true;
|
|
335
|
+
} catch (error: unknown) {
|
|
336
|
+
const awsError = error as AWSLikeError;
|
|
337
|
+
if (awsError?.name === 'ResourceNotFoundException') {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return mapAwsError(error, options.name);
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
export const getSecretString = async (
|
|
346
|
+
options: AwsSecretCommandOptions & { name: string }
|
|
347
|
+
): Promise<string> => {
|
|
348
|
+
validateSecretName(options.name);
|
|
349
|
+
debug('getSecretString called', { name: options.name });
|
|
350
|
+
const client = await createClient(options);
|
|
351
|
+
|
|
352
|
+
try {
|
|
353
|
+
const result = await client.send(
|
|
354
|
+
new GetSecretValueCommand({ SecretId: options.name })
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
if (typeof result.SecretString !== 'string') {
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Secret "${options.name}" is not stored as a string value and cannot be edited with append/remove.`
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return result.SecretString;
|
|
364
|
+
} catch (error: unknown) {
|
|
365
|
+
return mapAwsError(error, options.name);
|
|
366
|
+
}
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export const deleteSecret = async (
|
|
370
|
+
options: SecretDeleteOptions
|
|
371
|
+
): Promise<{ arn?: string; name?: string; deletedDate?: string }> => {
|
|
372
|
+
validateSecretName(options.name);
|
|
373
|
+
debug('deleteSecret called', {
|
|
374
|
+
name: options.name,
|
|
375
|
+
recoveryDays: options.recoveryDays,
|
|
376
|
+
forceDeleteWithoutRecovery: options.forceDeleteWithoutRecovery
|
|
377
|
+
});
|
|
378
|
+
const client = await createClient(options);
|
|
379
|
+
|
|
380
|
+
try {
|
|
381
|
+
const result = await client.send(
|
|
382
|
+
new DeleteSecretCommand({
|
|
383
|
+
SecretId: options.name,
|
|
384
|
+
RecoveryWindowInDays: options.recoveryDays,
|
|
385
|
+
ForceDeleteWithoutRecovery: options.forceDeleteWithoutRecovery
|
|
386
|
+
})
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
return {
|
|
390
|
+
arn: result.ARN,
|
|
391
|
+
name: result.Name,
|
|
392
|
+
deletedDate: formatDate(result.DeletionDate)
|
|
393
|
+
};
|
|
394
|
+
} catch (error: unknown) {
|
|
395
|
+
return mapAwsError(error, options.name);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
GetSecretValueCommand
|
|
4
4
|
} from '@aws-sdk/client-secrets-manager';
|
|
5
5
|
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts';
|
|
6
|
-
import { fromIni } from '@aws-sdk/credential-providers';
|
|
7
6
|
import Debug from 'debug';
|
|
7
|
+
import { buildAwsClientConfig } from './aws-config';
|
|
8
8
|
|
|
9
9
|
const debug = Debug('env-secrets:secretsmanager');
|
|
10
10
|
|
|
@@ -31,10 +31,9 @@ const isCredentialsError = (error: unknown): error is AWSLikeError => {
|
|
|
31
31
|
};
|
|
32
32
|
|
|
33
33
|
const checkConnection = async (
|
|
34
|
-
|
|
35
|
-
credentials?: ReturnType<typeof fromIni>
|
|
34
|
+
config: ReturnType<typeof buildAwsClientConfig>
|
|
36
35
|
) => {
|
|
37
|
-
const stsClient = new STSClient(
|
|
36
|
+
const stsClient = new STSClient(config);
|
|
38
37
|
const command = new GetCallerIdentityCommand({});
|
|
39
38
|
|
|
40
39
|
try {
|
|
@@ -56,33 +55,21 @@ const checkConnection = async (
|
|
|
56
55
|
|
|
57
56
|
export const secretsmanager = async (options: secretsmanagerType) => {
|
|
58
57
|
const { secret, profile, region } = options;
|
|
59
|
-
const {
|
|
60
|
-
AWS_ACCESS_KEY_ID: awsAccessKeyId,
|
|
61
|
-
AWS_SECRET_ACCESS_KEY: awsSecretAccessKey
|
|
62
|
-
} = process.env;
|
|
58
|
+
const config = buildAwsClientConfig({ profile, region });
|
|
63
59
|
|
|
64
|
-
let credentials;
|
|
65
60
|
if (profile) {
|
|
66
61
|
debug(`Using profile: ${profile}`);
|
|
67
|
-
|
|
68
|
-
} else if (awsAccessKeyId && awsSecretAccessKey) {
|
|
69
|
-
debug('Using environment variables');
|
|
70
|
-
credentials = undefined; // Will use environment variables automatically
|
|
71
|
-
} else {
|
|
62
|
+
} else if (config.credentials) {
|
|
72
63
|
debug('Using profile: default');
|
|
73
|
-
|
|
64
|
+
} else {
|
|
65
|
+
debug('Using environment variables');
|
|
74
66
|
}
|
|
75
67
|
|
|
76
|
-
const config = {
|
|
77
|
-
region,
|
|
78
|
-
credentials
|
|
79
|
-
};
|
|
80
|
-
|
|
81
68
|
if (!config.region) {
|
|
82
69
|
debug('no region set');
|
|
83
70
|
}
|
|
84
71
|
|
|
85
|
-
const connected = await checkConnection(
|
|
72
|
+
const connected = await checkConnection(config);
|
|
86
73
|
|
|
87
74
|
if (connected) {
|
|
88
75
|
const client = new SecretsManagerClient(config);
|
|
@@ -28,6 +28,67 @@ Currently supported provider: `aws`
|
|
|
28
28
|
- `-p, --profile <profile>` - AWS profile to use (defaults to environment variables or IAM role)
|
|
29
29
|
- `-o, --output <file>` - Output secrets to a file instead of injecting into environment variables. File will be created with 0400 permissions and will not overwrite existing files
|
|
30
30
|
|
|
31
|
+
## AWS Secret Management Commands
|
|
32
|
+
|
|
33
|
+
Use `env-secrets aws secret <command>` to manage AWS Secrets Manager secrets directly.
|
|
34
|
+
|
|
35
|
+
Use `env-secrets aws -s <secret-name>` together with either `-- <program-to-run>` to inject secret values into a spawned child process, or with `-o <file>` to write exports to a file.
|
|
36
|
+
`env-secrets aws secret <command>` is the lifecycle-management path and does not inject values by itself.
|
|
37
|
+
|
|
38
|
+
### Commands
|
|
39
|
+
|
|
40
|
+
- `create` - Create a new secret
|
|
41
|
+
- `update` - Update secret value or metadata
|
|
42
|
+
- `append` - Add or overwrite one key in an existing JSON secret
|
|
43
|
+
- `remove` - Remove one or more keys from an existing JSON secret
|
|
44
|
+
- `upsert` (`import`) - Create or update one JSON secret from an env file
|
|
45
|
+
- `list` - List secrets
|
|
46
|
+
- `get` - Get secret metadata/version info
|
|
47
|
+
- `delete` - Delete a secret
|
|
48
|
+
|
|
49
|
+
### Shared Options
|
|
50
|
+
|
|
51
|
+
- `-r, --region <region>` - AWS region
|
|
52
|
+
- `-p, --profile <profile>` - AWS profile
|
|
53
|
+
- `--output <format>` - Output format: `json` or `table` (default: `table`)
|
|
54
|
+
|
|
55
|
+
### Command-Specific Options
|
|
56
|
+
|
|
57
|
+
- `create`
|
|
58
|
+
- `-n, --name <name>` (required)
|
|
59
|
+
- `-v, --value <value>` or `--value-stdin` or `-f, --file <path>`
|
|
60
|
+
- `-d, --description <description>`
|
|
61
|
+
- `-k, --kms-key-id <kmsKeyId>`
|
|
62
|
+
- `-t, --tag <key=value...>`
|
|
63
|
+
- `update`
|
|
64
|
+
- `-n, --name <name>` (required)
|
|
65
|
+
- `-v, --value <value>` or `--value-stdin` or `-f, --file <path>`
|
|
66
|
+
- `-d, --description <description>`
|
|
67
|
+
- `-k, --kms-key-id <kmsKeyId>`
|
|
68
|
+
- `upsert` / `import`
|
|
69
|
+
- `-f, --file <path>` (required)
|
|
70
|
+
- `-n, --name <name>` (required)
|
|
71
|
+
- `-d, --description <description>`
|
|
72
|
+
- `-k, --kms-key-id <kmsKeyId>`
|
|
73
|
+
- `-t, --tag <key=value...>` (applies on create)
|
|
74
|
+
- `append`
|
|
75
|
+
- `-n, --name <name>` (required)
|
|
76
|
+
- `--key <key>` (required)
|
|
77
|
+
- `-v, --value <value>` or `--value-stdin` or `-f, --file <path>`
|
|
78
|
+
- `remove`
|
|
79
|
+
- `-n, --name <name>` (required)
|
|
80
|
+
- `--key <key...>` (required, repeatable)
|
|
81
|
+
- `list`
|
|
82
|
+
- `--prefix <prefix>`
|
|
83
|
+
- `-t, --tag <key=value...>`
|
|
84
|
+
- `get`
|
|
85
|
+
- `-n, --name <name>` (required)
|
|
86
|
+
- `delete`
|
|
87
|
+
- `-n, --name <name>` (required)
|
|
88
|
+
- `-y, --yes` (required for delete)
|
|
89
|
+
- `--recovery-days <7-30>`
|
|
90
|
+
- `--force-delete-without-recovery`
|
|
91
|
+
|
|
31
92
|
### Global Options
|
|
32
93
|
|
|
33
94
|
- `--help` - Show help information
|
|
@@ -131,6 +192,46 @@ env-secrets aws -s docker-secrets -r us-east-1 -o .env
|
|
|
131
192
|
docker run --env-file .env my-app:latest
|
|
132
193
|
```
|
|
133
194
|
|
|
195
|
+
This is also the supported way to load values into your current shell session.
|
|
196
|
+
`env-secrets aws -s ... -- <command>` only affects the spawned child process.
|
|
197
|
+
|
|
198
|
+
### Secret Management
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Create
|
|
202
|
+
env-secrets aws secret create -n app/dev/api -v '{"API_KEY":"abc123"}' --output json
|
|
203
|
+
|
|
204
|
+
# Create from stdin (recommended for sensitive values)
|
|
205
|
+
echo -n 'super-secret-value' | env-secrets aws secret create -n app/dev/raw --value-stdin
|
|
206
|
+
|
|
207
|
+
# Update value
|
|
208
|
+
env-secrets aws secret update -n app/dev/api -v '{"API_KEY":"xyz789"}'
|
|
209
|
+
|
|
210
|
+
# Update from stdin
|
|
211
|
+
echo -n 'rotated-value' | env-secrets aws secret update -n app/dev/raw --value-stdin
|
|
212
|
+
|
|
213
|
+
# Upsert/import from env file
|
|
214
|
+
env-secrets aws secret upsert --file .env --name app/dev --output json
|
|
215
|
+
# alias
|
|
216
|
+
env-secrets aws secret import --file .env --name app/dev --output json
|
|
217
|
+
|
|
218
|
+
# Result: one secret named app/dev with SecretString like:
|
|
219
|
+
# {"API_KEY":"abc123","DATABASE_URL":"postgres://..."}
|
|
220
|
+
|
|
221
|
+
# Append/remove keys (JSON secret only)
|
|
222
|
+
env-secrets aws secret append -n app/dev --key JIRA_EMAIL_TOKEN -v blah --output json
|
|
223
|
+
env-secrets aws secret remove -n app/dev --key OLD_TOKEN --output json
|
|
224
|
+
|
|
225
|
+
# List
|
|
226
|
+
env-secrets aws secret list --prefix app/dev --output table
|
|
227
|
+
|
|
228
|
+
# Get metadata (does not print secret value)
|
|
229
|
+
env-secrets aws secret get -n app/dev/api --output json
|
|
230
|
+
|
|
231
|
+
# Delete with confirmation
|
|
232
|
+
env-secrets aws secret delete -n app/dev/raw --recovery-days 7 --yes
|
|
233
|
+
```
|
|
234
|
+
|
|
134
235
|
## Environment Variable Behavior
|
|
135
236
|
|
|
136
237
|
### JSON Secret Parsing
|
|
@@ -4,6 +4,13 @@ title: AWS Secrets Manager
|
|
|
4
4
|
|
|
5
5
|
`env-secrets` supports pulling a single JSON secret from AWS Secrets Manager, mapping each top-level key to an environment variable.
|
|
6
6
|
|
|
7
|
+
It also supports secret lifecycle operations with `env-secrets aws secret <command>`.
|
|
8
|
+
|
|
9
|
+
Use:
|
|
10
|
+
|
|
11
|
+
- `env-secrets aws -s <secret-name> -- <command>` to fetch/inject secret values while running a command (or use `-o <file>` to write env vars to a file).
|
|
12
|
+
- `env-secrets aws secret <command>` for lifecycle operations (`create`, `update`, `append`, `remove`, `upsert/import`, `list`, `get`, `delete`).
|
|
13
|
+
|
|
7
14
|
### Create a secret (JSON)
|
|
8
15
|
|
|
9
16
|
```bash
|
|
@@ -22,6 +29,58 @@ env-secrets aws -s local/sample -r us-east-1 -- echo $user/$password
|
|
|
22
29
|
- `-r, --region` — AWS region (or `AWS_DEFAULT_REGION`)
|
|
23
30
|
- `-p, --profile` — AWS profile to use
|
|
24
31
|
|
|
32
|
+
### Inject into current shell
|
|
33
|
+
|
|
34
|
+
By default, variables are injected into the spawned child process only.
|
|
35
|
+
To load variables into your current shell session:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
env-secrets aws -s local/sample -r us-east-1 -o secrets.env
|
|
39
|
+
source secrets.env
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Secret Management Commands
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Create
|
|
46
|
+
env-secrets aws secret create -n app/dev/api -v '{"API_KEY":"abc123"}' --output json
|
|
47
|
+
|
|
48
|
+
# Create from stdin
|
|
49
|
+
echo -n 'super-secret-value' | env-secrets aws secret create -n app/dev/raw --value-stdin
|
|
50
|
+
|
|
51
|
+
# Update
|
|
52
|
+
env-secrets aws secret update -n app/dev/api -v '{"API_KEY":"rotated"}'
|
|
53
|
+
|
|
54
|
+
# Append/remove keys on JSON secret
|
|
55
|
+
env-secrets aws secret append -n app/dev/api --key JIRA_EMAIL_TOKEN -v blah
|
|
56
|
+
env-secrets aws secret remove -n app/dev/api --key OLD_TOKEN
|
|
57
|
+
|
|
58
|
+
# Upsert/import from env file
|
|
59
|
+
env-secrets aws secret upsert --file .env --name app/dev --output json
|
|
60
|
+
|
|
61
|
+
# Result: one secret named app/dev with SecretString JSON:
|
|
62
|
+
# {"API_KEY":"abc123","DATABASE_URL":"postgres://..."}
|
|
63
|
+
|
|
64
|
+
# List
|
|
65
|
+
env-secrets aws secret list --prefix app/dev --output table
|
|
66
|
+
|
|
67
|
+
# Get metadata (does not print secret value)
|
|
68
|
+
env-secrets aws secret get -n app/dev/api --output json
|
|
69
|
+
|
|
70
|
+
# Delete (requires --yes)
|
|
71
|
+
env-secrets aws secret delete -n app/dev/raw --recovery-days 7 --yes
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Supported commands:
|
|
75
|
+
|
|
76
|
+
- `create` with `--value`, `--value-stdin`, or `--file`
|
|
77
|
+
- `update` with value and/or metadata changes
|
|
78
|
+
- `append` / `remove` for key-level edits on JSON object secrets
|
|
79
|
+
- `upsert/import` from env files containing `export KEY=value` or `KEY=value`, stored as one JSON secret via `--name`
|
|
80
|
+
- `list` with optional prefix/tag filters
|
|
81
|
+
- `get` for metadata/version details
|
|
82
|
+
- `delete` with recovery window or force-delete flags
|
|
83
|
+
|
|
25
84
|
### Tips
|
|
26
85
|
|
|
27
86
|
- Use `DEBUG=env-secrets,env-secrets:secretsmanager` for verbose logs.
|