Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .bumpy/oidc-workload-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@varlock/akeyless-plugin": minor
"@varlock/aws-secrets-plugin": minor
"@varlock/azure-key-vault-plugin": minor
"@varlock/google-secret-manager-plugin": minor
"@varlock/hashicorp-vault-plugin": minor
"@varlock/infisical-plugin": minor
---

add OIDC workload identity federation support to secret provider plugins
385 changes: 164 additions & 221 deletions bun.lock

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions packages/plugins/akeyless/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ This package is a [Varlock](https://varlock.dev) [plugin](https://varlock.dev/gu
## Features

- **API Key authentication** - Simple access_id + access_key authentication
- **OIDC authentication** - Authenticate from Vercel, GitHub Actions, and other platforms without long-lived credentials
- **Static secrets** - Fetch static (key/value) secrets
- **Dynamic secrets** - Fetch on-demand generated credentials (database, cloud, etc.)
- **Rotated secrets** - Fetch auto-rotated credentials
Expand Down Expand Up @@ -70,6 +71,19 @@ If you are running a self-hosted [Akeyless Gateway](https://docs.akeyless.io/doc
# )
```

### OIDC authentication (For Vercel, GitHub Actions, etc.)

If you're deploying on a platform that supports OIDC, you can authenticate without an API key:

```env-spec
# @plugin(@varlock/akeyless-plugin)
# @initAkeyless(oidcAccessId="p-your-oidc-access-id")
```

The plugin auto-detects the OIDC token from your platform and authenticates with Akeyless using the OIDC access type. You need to create an OIDC auth method in the Akeyless Console and configure it with your platform's issuer URL.

See the [OIDC Workload Identity guide](https://varlock.dev/guides/oidc/) for full setup instructions.

### Multiple instances

If you need to connect to multiple Akeyless instances, register named instances:
Expand Down Expand Up @@ -161,8 +175,10 @@ Initialize an Akeyless plugin instance.

**Parameters:**

- `accessId: string` (required) - Akeyless Access ID (starts with `p-` for API Key auth)
- `accessKey: string` (required) - Akeyless Access Key
- `accessId?: string` - Akeyless Access ID (starts with `p-` for API Key auth). Required if not using OIDC.
- `accessKey?: string` - Akeyless Access Key. Required if `accessId` is provided.
- `oidcAccessId?: string` - Akeyless Access ID for OIDC auth method (alternative to accessId/accessKey)
- `oidcToken?: string` - Explicit OIDC JWT token (auto-detected from platform if not provided)
- `apiUrl?: string` - Akeyless API URL (defaults to `https://api.akeyless.io`). Use this for self-hosted Akeyless Gateway.
- `pathPrefix?: string` - Prefix automatically prepended to all secret paths
- `id?: string` - Instance identifier for multiple instances (defaults to `_default`)
Expand Down
90 changes: 68 additions & 22 deletions packages/plugins/akeyless/src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { type Resolver, plugin } from 'varlock/plugin-lib';
import ky from 'ky';
import { getOidcToken } from '@env-spec/utils/oidc-tokens';

const { SchemaError, ResolutionError } = plugin.ERRORS;

Expand All @@ -19,8 +20,8 @@ const DEFAULT_API_URL = 'https://api.akeyless.io';
const FIX_AUTH_TIP = [
'Verify your Akeyless credentials are configured correctly:',
' 1. Provide an API Key via @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY)',
' 2. Ensure the Access ID starts with "p-" (API Key auth)',
' 3. Verify the Access Key matches the Access ID in the Akeyless Console',
' 2. Use OIDC via @initAkeyless(oidcAccessId=...) for deployed environments',
' 3. Ensure the Access ID starts with "p-" (API Key auth) or matches your OIDC auth method',
].join('\n');

interface CachedToken {
Expand Down Expand Up @@ -48,6 +49,8 @@ function extractJsonKey(
class AkeylessPluginInstance {
private accessId?: string;
private accessKey?: string;
private oidcAccessId?: string;
private oidcToken?: string;
private apiUrl: string = DEFAULT_API_URL;
private pathPrefix?: string;
private cachedToken?: CachedToken;
Expand All @@ -62,9 +65,13 @@ class AkeylessPluginInstance {
accessKey?: any,
apiUrl?: any,
pathPrefix?: any,
oidcAccessId?: any,
oidcToken?: any,
) {
this.accessId = accessId ? String(accessId) : undefined;
this.accessKey = accessKey ? String(accessKey) : undefined;
this.oidcAccessId = oidcAccessId ? String(oidcAccessId) : undefined;
this.oidcToken = oidcToken ? String(oidcToken) : undefined;
if (apiUrl) this.apiUrl = String(apiUrl).replace(/\/+$/, '');
this.pathPrefix = pathPrefix ? String(pathPrefix) : undefined;
debug(
Expand All @@ -76,6 +83,8 @@ class AkeylessPluginInstance {
!!this.accessId,
'hasAccessKey:',
!!this.accessKey,
'oidcAccessId:',
this.oidcAccessId,
'pathPrefix:',
this.pathPrefix,
);
Expand All @@ -97,21 +106,50 @@ class AkeylessPluginInstance {
return this.cachedToken.token;
}

if (!this.accessId || !this.accessKey) {
throw new SchemaError('Akeyless Access ID and Access Key are required', {
// Determine auth method
let authPayload: Record<string, string>;

if (this.accessId && this.accessKey) {
// API Key auth
debug('Authenticating with Akeyless API Key');
authPayload = {
'access-id': this.accessId,
'access-key': this.accessKey,
'access-type': 'access_key',
};
} else if (this.oidcAccessId) {
// OIDC auth
let jwt: string | undefined = this.oidcToken;
if (!jwt) {
const result = await getOidcToken();
jwt = result?.token;
}

if (!jwt) {
throw new SchemaError('OIDC auth configured but no token available', {
tip: [
'Ensure you are running in an environment that provides OIDC tokens:',
' - Vercel, GitHub Actions, Fly.io, GCP Cloud Run, GitLab CI',
'Or provide an explicit token via @initAkeyless(oidcToken=...)',
].join('\n'),
});
}

debug('Authenticating with Akeyless OIDC');
authPayload = {
'access-id': this.oidcAccessId,
'access-type': 'oidc',
'oidc-token': jwt,
};
} else {
throw new SchemaError('Akeyless authentication is required', {
tip: FIX_AUTH_TIP,
});
}

try {
debug('Authenticating with Akeyless API Key');

const response = await ky.post(`${this.apiUrl}/auth`, {
json: {
'access-id': this.accessId,
'access-key': this.accessKey,
'access-type': 'access_key',
},
json: authPayload,
}).json<{ token: string; expiry?: number }>();

const token = response.token;
Expand Down Expand Up @@ -312,15 +350,18 @@ plugin.registerRootDecorator({
throw new SchemaError(`Instance with id "${id}" already initialized`);
}

// accessId is required
if (!objArgs.accessId) {
throw new SchemaError('accessId parameter is required', {
tip: 'Provide your Akeyless Access ID: @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY)',
// Either accessId+accessKey OR oidcAccessId is required
if (!objArgs.accessId && !objArgs.oidcAccessId) {
throw new SchemaError('Authentication is required: provide accessId+accessKey or oidcAccessId for OIDC', {
tip: [
'Option 1 - API Key: @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY)',
'Option 2 - OIDC: @initAkeyless(oidcAccessId=your-access-id)',
].join('\n'),
});
}
// accessKey is required
if (!objArgs.accessKey) {
throw new SchemaError('accessKey parameter is required', {

if (objArgs.accessId && !objArgs.accessKey) {
throw new SchemaError('accessKey is required when using accessId', {
tip: 'Provide your Akeyless Access Key: @initAkeyless(accessId=$AKEYLESS_ACCESS_ID, accessKey=$AKEYLESS_ACCESS_KEY)',
});
}
Expand All @@ -333,16 +374,21 @@ plugin.registerRootDecorator({
accessKeyResolver: objArgs.accessKey,
apiUrlResolver: objArgs.apiUrl,
pathPrefixResolver: objArgs.pathPrefix,
oidcAccessIdResolver: objArgs.oidcAccessId,
oidcTokenResolver: objArgs.oidcToken,
};
},
async execute({
id, accessIdResolver, accessKeyResolver, apiUrlResolver, pathPrefixResolver,
id, accessIdResolver, accessKeyResolver, apiUrlResolver,
pathPrefixResolver, oidcAccessIdResolver, oidcTokenResolver,
}) {
const accessId = await accessIdResolver.resolve();
const accessKey = await accessKeyResolver.resolve();
const accessId = await accessIdResolver?.resolve();
const accessKey = await accessKeyResolver?.resolve();
const apiUrl = await apiUrlResolver?.resolve();
const pathPrefix = await pathPrefixResolver?.resolve();
pluginInstances[id].setAuth(accessId, accessKey, apiUrl, pathPrefix);
const oidcAccessId = await oidcAccessIdResolver?.resolve();
const oidcToken = await oidcTokenResolver?.resolve();
pluginInstances[id].setAuth(accessId, accessKey, apiUrl, pathPrefix, oidcAccessId, oidcToken);
},
});

Expand Down
22 changes: 20 additions & 2 deletions packages/plugins/aws-secrets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ This package is a [Varlock](https://varlock.dev) [plugin](https://varlock.dev/gu
- **Auto-infer secret/parameter names** from environment variable names (uses name as-is)
- **JSON key extraction** from secrets/parameters using `#` syntax or named `key` parameter
- **Name prefixing** with `namePrefix` option for organized secret management
- **OIDC workload identity** - Authenticate from Vercel, GitHub Actions, and other platforms without long-lived credentials
- Support for named AWS profiles
- Support for explicit credentials (access key/secret key)
- Support for temporary credentials (with session tokens)
Expand Down Expand Up @@ -77,12 +78,26 @@ AWS_SECRET_ACCESS_KEY=

You would then need to inject these env vars using your CI/CD system.

### OIDC workload identity (For Vercel, GitHub Actions, etc.)

If you're deploying on a platform that supports OIDC, you can authenticate without any long-lived credentials:

```env-spec
# @plugin(@varlock/aws-secrets-plugin)
# @initAws(region=us-east-1, oidcRoleArn="arn:aws:iam::123456789012:role/varlock-oidc-role")
```

The plugin auto-detects the OIDC token from your platform and exchanges it for temporary AWS credentials via STS. You just need to create an IAM role with a trust policy for your platform's OIDC provider.

See the [OIDC Workload Identity guide](https://varlock.dev/guides/oidc/) for full setup instructions.

### Authentication Priority

The plugin tries authentication methods in this order:
1. **Explicit credentials** - If `accessKeyId` and `secretAccessKey` are provided
2. **Named profile** - If `profile` is specified, uses credentials from `~/.aws/credentials`
3. **Default AWS credential chain** - Environment variables → `~/.aws/credentials` → IAM roles
2. **OIDC** - If `oidcRoleArn` is provided, exchanges a platform OIDC token for temporary AWS credentials
3. **Named profile** - If `profile` is specified, uses credentials from `~/.aws/credentials`
4. **Default AWS credential chain** - Environment variables → `~/.aws/credentials` → IAM roles

### Using Named Profiles

Expand Down Expand Up @@ -185,6 +200,9 @@ Initialize an AWS plugin instance.
- `secretAccessKey?: string` - AWS secret access key for explicit authentication
- `sessionToken?: string` - AWS session token for temporary credentials
- `profile?: string` - Named profile from `~/.aws/credentials`
- `oidcRoleArn?: string` - ARN of IAM role to assume via OIDC (enables workload identity federation)
- `oidcSessionName?: string` - Session name for the OIDC assumed role (defaults to `varlock-session`)
- `oidcToken?: string` - Explicit OIDC JWT token (auto-detected from platform if not provided)
- `id?: string` - Instance identifier for multiple instances (defaults to `_default`)

### Functions
Expand Down
1 change: 1 addition & 0 deletions packages/plugins/aws-secrets/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"devDependencies": {
"@aws-sdk/client-secrets-manager": "^3.700.0",
"@aws-sdk/client-ssm": "^3.700.0",
"@aws-sdk/client-sts": "^3.700.0",
"@aws-sdk/credential-providers": "^3.700.0",
"@env-spec/utils": "workspace:^",
"@types/node": "catalog:",
Expand Down
Loading
Loading