Documentation

AppRole example with VaultClient

This example mirrors the AppRole authentication flow with the original v1 client. It intentionally uses KV v1 at the secret mount so the code can show the legacy client's native write and read calls.

This example mirrors the AppRole authentication flow with the original v1 client. It intentionally uses KV v1 at the secret mount so the code can show the legacy client’s native write and read calls.

What the workflow demonstrates

  • Prepare Vault by enabling a KV v1 mount at secret.
  • Write an application secret at /secret/mysql/webapp.
  • Enable the approle auth method.
  • Create a read-only jenkins policy for that one secret path.
  • Register an AppRole with short-lived tokens.
  • Generate role_id and secret_id credentials.
  • Log in as an app with those AppRole credentials.
  • Read the secret with the app token and assert the returned data.

This example uses the shared decorator-based runner and personas described in examples/README.md.

Some AppRole operations use apiRequest() with a custom POST 200 command spec inside the v1 personas because the original client does not expose dedicated AppRole helpers.

Local Vault

From the repository root, start only the plain Vault service:

docker compose up -d vault

One Vault instance is enough. You do not need vault_tls or vault_mtls unless you are specifically testing TLS.

For a fresh Vault state:

docker compose down --volumes --remove-orphans
docker compose up -d vault

Run

Install dependencies from the repository root:

npm install

Then run the example:

NANVC_VAULT_CLUSTER_ADDRESS=http://127.0.0.1:8200 npx tsx examples/app-role-v1/main.ts

The helper defaults to http://vault.local:8200. Use the environment variable above when vault.local is not mapped on your machine.

Environment

For an existing Vault server, set:

export NANVC_VAULT_CLUSTER_ADDRESS=http://127.0.0.1:8200
export TEST_NANVC_VAULT_AUTH_TOKEN=<root-or-admin-token>
export TEST_NANVC_VAULT_UNSEAL_KEY=<unseal-key>

If the local Vault server is initialized by any example or integration helper, the helper writes a shared cache file under your OS temp directory with:

  • TEST_NANVC_VAULT_AUTH_TOKEN
  • TEST_NANVC_VAULT_UNSEAL_KEY

Shell-exported TEST_NANVC_* variables take precedence over cached values. If Vault reports invalid token, the cached credentials probably belong to another Vault instance or an older Docker volume. Export valid TEST_NANVC_* values, or reset local Vault with the fresh-state commands above.

import assert from 'node:assert';

import type { AdminPersona } from '../common/personas/admin.js';
import type { AppPersona } from '../common/personas/app.js';
import type { OperatorPersona } from '../common/personas/operator.js';
import type { VaultResponseData, AppRoleCredentials } from '../common/personas/types.js';
import { expectSuccess } from '../common/personas/helpers.js';
import { example, runAs, runExample, workflow } from '../common/workflow/decorators.js';

const secretData = {
    db_name: 'users',
    username: 'admin',
    password: 'passw0rd',
};

@example('AppRole authentication example')
class AppRoleExample {
    private credentials!: AppRoleCredentials;

    @workflow('operator', 'Prerequisites: ensure Vault is prepared and kv secrets engine is enabled')
    @runAs({ persona: 'operator', version: 'v1' })
    public async prepareVault(operator: OperatorPersona<'v1'>): Promise<void> {
        await operator.ensureKvMountAvailable('credentials', 1);
    }

    @workflow('admin', 'Configure AppRole and create credentials')
    @runAs({ persona: 'admin', version: 'v1' })
    public async adminWorkflow(admin: AdminPersona<'v1'>): Promise<void> {
        await expectSuccess(admin.vault.write('/credentials/mysql/webapp', secretData), 'Vault KV v1 write failed');

        await admin.enableAppRoleAuth();

        const jenkinsPolicy = [
            "# Read-only permission on secrets stored at 'credentials/mysql/webapp'",
            'path "credentials/mysql/webapp" {',
            '  capabilities = ["read"]',
            '}',
        ].join('\n');
        await admin.createPolicy('jenkins', jenkinsPolicy);

        await admin.registerAppRole('jenkins', {
            token_policies: ['jenkins'],
            token_ttl: '20m',
            token_max_ttl: '30m',
        });

        this.credentials = await admin.createAppRoleCredentials('jenkins');
    }

    @workflow('app', 'Log in with AppRole credentials and check policy permissions')
    @runAs({ persona: 'app', version: 'v1' })
    public async appWorkflow(app: AppPersona<'v1'>): Promise<void> {
        await app.loginWithAppRole(this.credentials);

        const secretResponse = await expectSuccess(
            app.vault.read('/credentials/mysql/webapp'),
            'Vault KV v1 read failed',
        );
        const secret = secretResponse.apiResponse as
            | VaultResponseData<{
                  db_name: string;
                  username: string;
                  password: string;
              }>
            | undefined;

        assert.deepStrictEqual(secret?.data, secretData, 'Retrieved secret data does not match the expected value');
    }
}

runExample(AppRoleExample).catch((error) => {
    console.error(error);
    process.exitCode = 1;
});

Source Files

  • README source: examples/app-role-v1/README.md
  • Runnable source: examples/app-role-v1/main.ts

This page is generated from the example README. Edit the source README and run npm run generate:docs to update it.