Resolve secrets securely

When building workflows that integrate with external systems, you inevitably need to handle API keys, passwords, and tokens.

Quarkus Flow strictly separates secret declarations from secret values. Your workflow definitions (Java DSL or YAML) only ever reference a secret by a string "handle" (e.g., mySecret). At runtime, Quarkus Flow resolves that handle into actual credentials using the Quarkus CredentialsProvider SPI, securely pulling the values from HashiCorp Vault, Kubernetes Secrets, or a local file.

This guide shows how to:

  • Reference secrets securely in your workflow definitions.

  • Provide secret values using local application.properties (for dev/test).

  • Integrate with a production-grade CredentialsProvider (like Vault).

1. Referencing Secrets in the Workflow

You never hardcode credentials in your workflow DSL. Instead, you reference a "handle" (a logical name for the secret).

There are two primary ways to consume a secret in the Java DSL:

  1. HTTP Authentication: Use the fluent AuthenticationConfigurer to attach a secret to an HTTP task (e.g., auth → auth.use("mySecret")).

  2. Workflow Expressions: Inject the secret directly into a payload or task argument using a jq expression: ${ $secret.mySecret.password }.

Here is how you declare and consume a secret in the Java DSL:

package org.acme.secrets;

import static io.serverlessworkflow.fluent.func.FuncWorkflowBuilder.workflow;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkiverse.flow.Flow;
import io.serverlessworkflow.api.types.Workflow;

@ApplicationScoped
public class SecretEchoFlow extends Flow {
    @Override
    public Workflow descriptor() {
        return workflow()
                .use(u -> u.secrets("mySecret")) // declare the handle
                .tasks(t -> t.set("${ $secret.mySecret.password }")) // -> "s3cr3t!"
                .build();
    }
}

Typical keys expected by the engine (per the specification):

  • Basic auth: requires username and password keys.

  • Bearer token: requires a token or access_token key.

2. Providing Secrets (Dev & Test)

When developing locally or running unit tests, you likely don’t have a full HashiCorp Vault instance running.

If Quarkus Flow detects that zero CredentialsProvider beans are present in your application, it automatically falls back to reading secrets directly from MicroProfile Config (e.g., your application.properties file).

To fulfill a secret handle named mySecret, simply define dotted properties:

application.properties
# Syntax: <secretHandle>.<key>=<value>

# Resolving 'mySecret' for Basic Auth
mySecret.username=alice
mySecret.password=s3cr3t!

# Resolving an API key
my-api-key.token=eyJhbGciOi...

This fallback mechanism ensures you can test workflows immediately without complex infrastructure.

3. Providing Secrets (Production)

In production, you should use a secure secret store. Quarkus provides excellent integrations for this via the CredentialsProvider SPI.

For example, if you add the quarkus-vault extension, Quarkus will automatically create a CredentialsProvider bean that talks to HashiCorp Vault.

Once a CredentialsProvider bean is present on the classpath, Quarkus Flow disables the application.properties fallback and routes all secret resolutions through the provider.

3.1 Managing Multiple Providers

If you have multiple CredentialsProvider beans in your application (e.g., one for Vault and one for a custom legacy database), you must tell Quarkus Flow which one to use via their @Named identifiers.

# Set a global default provider for all workflows
quarkus.flow.secrets.credentials-provider-name=vault

# Override the provider for specific secret handles
quarkus.flow.secrets.credentials-provider-names.mySecret=file
quarkus.flow.secrets.credentials-provider-names.legacy-db-creds=custom-db-provider

If multiple providers exist and you do not specify which one to use, the engine will fail fast at startup and log a list of the available @Named beans to help you configure the routing.

4. Error Handling and Security

Missing Secrets

Per the Serverless Workflow Specification, if a workflow attempts to execute a task and the referenced secret handle cannot be resolved (or is missing the required keys like username), the engine treats this as an authorization failure.

The task will immediately fail with a WorkflowError indicating an authorization problem (often surfacing as an HTTP 401 Unauthorized or 403 Forbidden if it was an HTTP task).

Security Notes

  • Never log secrets: Quarkus Flow is designed to never log the values of your secrets. Debug logs and error messages will only ever include the provider name and the secret handle.

  • Never hardcode credentials: Always use handles in your DSL.

5. Unit Testing with Secrets

If you are writing unit tests using @QuarkusTest or QuarkusUnitTest, you can easily test both the provider and fallback behaviors.

Using MicroProfile Config fallback (no provider beans)

@RegisterExtension
static final QuarkusUnitTest unit = new QuarkusUnitTest()
    .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
        .addClass(SecretEchoFlow.class)
        // no CredentialsProvider classes added → fallback kicks in
        .addAsResource(new StringAsset(
                "mySecret.username=alice\n" +
                "mySecret.password=s3cr3t!\n"),
            "application.properties"));

@Test
void resolves_secret_from_application_properties() {
    var handle = Arc.container().instance(WorkflowDefinition.class, Identifier.Literal.of(SecretEchoFlow.class.getName()));
    assertTrue(handle.isAvailable());

    var model = handle.get().instance().start().join();
    assertEquals("s3cr3t!", model.as(String.class).orElseThrow());
}

Using a CredentialsProvider

@RegisterExtension
static final QuarkusUnitTest unit = new QuarkusUnitTest()
    .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
    .addClass(SecretEchoFlow.class)
    .addClass(DumbCredentialsProvider.class))
    // select our @Named("dumb") provider globally
    .overrideConfigKey("quarkus.flow.secrets.credentials-provider-name", "dumb");

@Test
void resolves_secret_from_provider() {
    var handle = Arc.container().instance(WorkflowDefinition.class, Identifier.Literal.of(SecretEchoFlow.class.getName()));
    assertTrue(handle.isAvailable());

    var model = handle.get().instance().start().join();
    assertEquals("s3cr3t!", model.as(String.class).orElseThrow());
}

See also