Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
e97ed50
Add context-bound Java secret provider baseline
Oct 15, 2026
9d9db82
Qualify Java secret expiry and concurrent contexts
Oct 15, 2026
0886b12
Prove concurrent Java secret resolution
Oct 15, 2026
c77cd94
Add controlled Vault KV v2 secret adapter
Oct 15, 2026
047ddd0
Reject untrusted Vault HTTP profile before token resolution
Oct 15, 2026
d80b828
Qualify profile-scoped TLS trust for Vault example
Oct 15, 2026
c41a5e3
Capture Vault TLS telemetry and assert canary redaction
Oct 15, 2026
554d244
test: verify Secret Provider against live Vault KV v2 TLS
Oct 15, 2026
53c8ebc
fix: bind file secrets to the original secure directory
Oct 15, 2026
ae6f92e
Add Vault exported telemetry probe to secret example
Oct 16, 2026
cd014e2
Probe denied Vault read through exported telemetry
Oct 16, 2026
2b1b546
Cover Vault missing malformed and expired telemetry probes
Oct 16, 2026
8c9e8fa
Exercise stalled Vault response in telemetry probe
Oct 16, 2026
fa6a25f
Make Vault TLS example resilient to aborted handshakes
Oct 17, 2026
8b385e1
test: verify Vault bootstrap token rotation fails closed
Oct 17, 2026
08a1f92
test: reject removed Vault TLS trust in one Context
Oct 17, 2026
7671849
feat: support explicit private CA overlap for HTTP profiles
Oct 17, 2026
f797707
Verify in-flight TLS trust replacement semantics
Oct 17, 2026
153e3dc
Verify mixed-CA multi-context rollover
Oct 17, 2026
2bc4b6b
test: bound stalled Vault TLS read across trust replacement
Oct 17, 2026
0a1efd6
test: qualify Vault TLS rollover across client processes
Oct 17, 2026
ca56321
Exercise secret clock and shared custom SPI in native example
Oct 17, 2026
2d12225
Probe independent mutation ledgers with shared secret provider
Oct 17, 2026
76b3f84
Test shared Vault provider request isolation across contexts
Oct 17, 2026
6b21282
Integrate isolated Secret Provider into current main gates
Oct 17, 2026
01f7b79
Prevent reflective JSON from exposing SecretValue bytes
Oct 17, 2026
1cf2471
test: prove shared Vault provider request isolation
Oct 18, 2026
a32ddaa
Prevent HTTP credential serialization in Java
Oct 18, 2026
a847dac
test: preserve pending graph state across secret rotation (#267) [ski…
Oct 22, 2026
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
43 changes: 43 additions & 0 deletions examples/secret-provider/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Context-owned Secret Provider example

Run `bash examples/secret-provider/verify.sh`. The runtime-owned test binds a
symbolic reference to an owner-only local file at trusted startup and resolves
it through `UserContext` with a comment. Changing the file is visible at the
next resolution.

A second test installs a fixed Vault KV v2 path and separate bootstrap token
provider, then uses the real HTTP adapter against a loopback fixture. It checks
the path, token header, response version, and a stalled-response timeout. Both
tests run twice, and the verifier rejects secret canaries in output. This is a
controlled fixture, not a production Vault deployment.

Private-CA rollover is explicit: `trustedRootsPem(List.of(oldPem, newPem))`
temporarily trusts both roots for this named HTTPS profile, replacing system
roots rather than expanding process-wide trust. A later profile with only
`trustedRootPem(newPem)` retires the old CA for new requests. The TLS example
checks reads through both CA-signed server certificates during overlap and
rejects the old certificate after retirement. Each list element contains one
PEM certificate; the bounded profile accepts at most four roots. In-flight
requests already holding the old HTTP service may finish within their existing
budget after the Context profile is replaced; replacement is not retroactive
revocation. The example pauses one accepted TLS request, retires its CA for
new calls, proves a second call sends no HTTP request, then lets the first
finish. Production certificate rollout still requires traffic coordination and
draining; do not discard the old CA before all instances have the overlap set.
The controlled two-instance fixture keeps old- and new-CA servers online
together, advances independent Contexts through mixed and overlap profiles,
rejects an old endpoint after trust retirement, then restores the old
instance by routing it to the new endpoint. It checks exact server-side HTTP
request counts; it is not a separate-host production rollout.
The process-rollover fixture additionally starts separate JVM clients against
both live certificate endpoints. The old route is moved to the new endpoint
while overlap trust is still active, before new-only trust is installed.
After retirement, a deliberately stale old route is rejected before HTTP.
The servers observe two old and five new HTTP requests, and each child process
must report its own PID and a canary-free result. This is loopback process
isolation, not multi-host traffic management.
The example also accepts an old-CA Vault read but deliberately never returns
its response. The read ends within its 900 ms whole-call budget as
`UNAVAILABLE`, without a second HTTP request; only then is the Context switched
to new-only trust. This is a bounded-drain test, not proof that changing trust
immediately cancels in-flight requests.
11 changes: 11 additions & 0 deletions examples/secret-provider/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent><groupId>io.teaql</groupId><artifactId>teaql-java-parent</artifactId><version>1.555-RELEASE</version><relativePath>../../pom.xml</relativePath></parent>
<artifactId>teaql-secret-provider-example</artifactId>
<dependencies>
<dependency><groupId>io.teaql</groupId><artifactId>teaql-runtime</artifactId></dependency>
<dependency><groupId>io.teaql</groupId><artifactId>teaql-tool-http</artifactId></dependency>
<dependency><groupId>io.teaql</groupId><artifactId>teaql-opentelemetry</artifactId><version>${project.version}</version><scope>test</scope></dependency>
<dependency><groupId>junit</groupId><artifactId>junit</artifactId><scope>test</scope></dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package io.teaql.examples.secret;

import static org.junit.Assert.*;
import static org.junit.Assume.assumeTrue;

import com.sun.net.httpserver.HttpServer;
import io.teaql.core.http.HttpRequestService;
import io.teaql.core.meta.SimpleEntityMetaFactory;
import io.teaql.core.secret.SecretFailure;
import io.teaql.core.secret.SecretReference;
import io.teaql.core.secret.SecretValue;
import io.teaql.opentelemetry.StandardOpenTelemetryFactory;
import io.teaql.runtime.DefaultUserContext;
import io.teaql.runtime.TeaQLRuntime;
import io.teaql.runtime.secret.VaultKv2SecretProvider;
import io.teaql.tools.http.HttpServiceProfile;
import io.teaql.tools.http.JdkHttpRequestService;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import org.junit.Test;

/** Opt-in Vault -> standard OTLP probe; backend readback is checked by conformance. */
public class SecretOpenObserveProbeTest {
private static final String TOKEN = "PRIVATE_VAULT_TOKEN_CANARY";
private static final String VALUE = "PRIVATE_VAULT_VALUE_CANARY";

@Test public void exportedVaultTraceDoesNotContainCredentialOrResponseValue() throws Exception {
assumeTrue("true".equals(System.getenv("TEAQL_SECRET_LIVE_OTLP")));
URI endpoint = URI.create(System.getenv("TEAQL_TELEMETRY_OTLP_ENDPOINT"));
assertEquals("127.0.0.1", endpoint.getHost());
String serviceName = System.getenv("TEAQL_TELEMETRY_SERVICE_NAME");
assertNotNull(serviceName);
String outcome = System.getenv().getOrDefault("TEAQL_SECRET_PROBE_OUTCOME", "success");
assertTrue("unsupported probe outcome", List.of("success", "denied", "missing",
"malformed", "expired", "timeout").contains(outcome));
var telemetry = new StandardOpenTelemetryFactory().create(serviceName, endpoint);
var carrier = new AtomicReference<String>();
HttpServer server = HttpServer.create(new InetSocketAddress(
InetAddress.getByAddress(new byte[] {127, 0, 0, 1}), 0), 0);
server.createContext("/v1/secret/data/payment/api", exchange -> {
assertEquals(TOKEN, exchange.getRequestHeaders().getFirst("X-Vault-Token"));
carrier.set(exchange.getRequestHeaders().getFirst("traceparent"));
if (outcome.equals("timeout")) {
try {
Thread.sleep(1800);
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
} finally {
exchange.close();
}
return;
}
String response = switch (outcome) {
case "denied", "missing" -> "{\"errors\":[\"" + VALUE + "\"]}";
case "malformed" -> "{\"data\":\"" + VALUE + "\",";
case "expired" -> "{\"data\":{\"data\":{\"value\":\"" + VALUE
+ "\"},\"metadata\":{\"version\":7,\"destroyed\":false,\"deletion_time\":\"2020-01-01T00:00:00Z\"}}}";
default -> "{\"data\":{\"data\":{\"value\":\"" + VALUE
+ "\"},\"metadata\":{\"version\":7,\"destroyed\":false,\"deletion_time\":\"\"}}}";
};
byte[] body = response.getBytes(StandardCharsets.UTF_8);
exchange.sendResponseHeaders(outcome.equals("denied") ? 403 : outcome.equals("missing") ? 404 : 200, body.length);
try (var output = exchange.getResponseBody()) { output.write(body); }
});
server.start();
try {
var runtime = TeaQLRuntime.builder().metadata(new SimpleEntityMetaFactory())
.telemetry(telemetry).build();
var reference = new SecretReference("payment", "api_key");
var provider = VaultKv2SecretProvider.builder("vault", "secret",
(caller, ignored) -> new SecretValue(TOKEN.getBytes(StandardCharsets.UTF_8), null, null),
new SecretReference("bootstrap", "vault_token"))
.bind(reference, "payment/api", "value").build();
var context = new DefaultUserContext(runtime).withSecretProvider(provider);
var profileBuilder = HttpServiceProfile.builder("vault",
URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/"));
if (outcome.equals("timeout")) {
profileBuilder.budgets(Duration.ofSeconds(2), Duration.ofMillis(700),
Duration.ofSeconds(3));
}
var profile = profileBuilder.build();
context.putAttribute(HttpRequestService.class.getName(),
new JdkHttpRequestService(List.of(profile)));
if (outcome.equals("success")) {
assertEquals(VALUE, new String(context.resolveSecret(reference, "verify Vault adapter")
.exposeBytes(), StandardCharsets.UTF_8));
} else {
SecretFailure.Code expected = switch (outcome) {
case "denied" -> SecretFailure.Code.DENIED;
case "missing" -> SecretFailure.Code.MISSING;
case "expired" -> SecretFailure.Code.EXPIRED;
default -> SecretFailure.Code.UNAVAILABLE;
};
assertEquals(expected, assertThrows(SecretFailure.class,
() -> context.resolveSecret(reference, "verify Vault adapter")).code());
}
assertNotNull(carrier.get());
assertTrue(carrier.get().startsWith("00-") && carrier.get().length() == 55);
telemetry.flush();
System.out.println("SECRET_OTLP_PROBE_PASS outcome=" + outcome
+ " traceparent=" + carrier.get());
} finally {
server.stop(0);
telemetry.shutdown();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package io.teaql.examples.secret;

import io.teaql.core.secret.SecretFailure;
import io.teaql.core.secret.SecretProvider;
import io.teaql.core.secret.SecretReference;
import io.teaql.core.secret.SecretValue;
import io.teaql.core.businessid.BusinessClock;
import io.teaql.core.BaseEntity;
import io.teaql.core.EntityKey;
import io.teaql.runtime.DefaultUserContext;
import io.teaql.runtime.secret.BoundSecretProvider;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.attribute.PosixFilePermission;
import java.time.Instant;
import java.time.LocalDate;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import static org.junit.Assert.*;

public class SecretProviderExampleTest {
@Rule public TemporaryFolder temporary = new TemporaryFolder();

private static final class LedgerProbe extends BaseEntity {
private String name = "original";

private LedgerProbe(long id) {
updateId(id);
}

private void updateName(String replacement) {
handleUpdate("name", name, replacement);
name = replacement;
}
}

@Test public void startupBindingAndContextResolutionObserveRotation() throws Exception {
Path root = temporary.newFolder("keys").toPath();
org.junit.Assume.assumeTrue(Files.getFileStore(root).supportsFileAttributeView("posix"));
Files.setPosixFilePermissions(root, Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE));
Path file = root.resolve("signing-key");
Files.writeString(file, "private-example-key-v1");
Files.setPosixFilePermissions(file, Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
SecretReference reference = new SecretReference("document", "signing");
BoundSecretProvider provider = BoundSecretProvider.builder(root)
.file(reference, Path.of("signing-key"), "v1", null).build();
DefaultUserContext context = new DefaultUserContext(null).withSecretProvider(provider);
Object marker = new Object();
BusinessClock clock = ignored -> LocalDate.of(2000, 1, 1);
context.putAttribute("example.rotation-marker", marker);
context.putAttribute(BusinessClock.class.getName(), clock);
LedgerProbe pending = new LedgerProbe(42);
pending.updateName("pending before rotation");
var ledger = pending.getEntityMutationLedger();
EntityKey pendingKey = new EntityKey(pending.typeName(), 42L);

assertEquals("private-example-key-v1", new String(
context.resolveSecret(reference, "sign document").exposeBytes(), StandardCharsets.UTF_8));
Files.writeString(file, "private-example-key-v2");
context.withSecretProvider(BoundSecretProvider.builder(root)
.file(reference, Path.of("signing-key"), "v2", null).build());
var rotated = context.resolveSecret(reference, "sign next document");
assertEquals("private-example-key-v2", new String(rotated.exposeBytes(), StandardCharsets.UTF_8));
assertEquals("v2", rotated.version());
assertSame(marker, context.getAttribute("example.rotation-marker"));
assertSame(clock, context.getAttribute(BusinessClock.class.getName()));
assertSame(ledger, pending.getEntityMutationLedger());
assertEquals("pending before rotation", ledger.get(pendingKey, "name"));
assertTrue(pending.dirtyFields().contains("name"));
assertFalse(rotated.toString().contains("private-example-key"));
assertEquals(SecretFailure.Code.INVALID_INPUT,
assertThrows(SecretFailure.class,
() -> context.resolveSecret(reference, " ")).code());
System.out.println("SECRET_ROTATION_LEDGER_UNCHANGED_PASS language=java context=1 clock=1 pending=1");
}

@Test public void operationalExpiryAndSharedCustomProviderRemainContextIsolated() throws Exception {
SecretReference reference = new SecretReference("document", "signing");
Path root = temporary.newFolder("expired-keys").toPath();
org.junit.Assume.assumeTrue(Files.getFileStore(root).supportsFileAttributeView("posix"));
Files.setPosixFilePermissions(root, Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE, PosixFilePermission.OWNER_EXECUTE));
Path key = root.resolve("signing-key");
Files.writeString(key, "private-example-key");
Files.setPosixFilePermissions(key, Set.of(PosixFilePermission.OWNER_READ,
PosixFilePermission.OWNER_WRITE));
BoundSecretProvider expired = BoundSecretProvider.builder(root)
.file(reference, Path.of("signing-key"), "expired", Instant.EPOCH).build();
DefaultUserContext frozen = new DefaultUserContext(null).withSecretProvider(expired);
frozen.putAttribute(BusinessClock.class.getName(),
(BusinessClock) ignored -> LocalDate.of(1900, 1, 1));
assertEquals(SecretFailure.Code.EXPIRED,
assertThrows(SecretFailure.class,
() -> frozen.resolveSecret(reference, "check operational expiry")).code());

CountDownLatch entered = new CountDownLatch(2);
SecretProvider custom = (caller, requested) -> {
entered.countDown();
try {
if (!entered.await(3, TimeUnit.SECONDS)) {
throw new SecretFailure(SecretFailure.Code.UNAVAILABLE);
}
} catch (InterruptedException interrupted) {
Thread.currentThread().interrupt();
throw new SecretFailure(SecretFailure.Code.UNAVAILABLE);
}
return new SecretValue(caller.getAttribute("example.business-date").toString()
.getBytes(StandardCharsets.UTF_8), null, null);
};
DefaultUserContext first = new DefaultUserContext(null).withSecretProvider(custom);
DefaultUserContext second = new DefaultUserContext(null).withSecretProvider(custom);
first.putAttribute("example.business-date", "2000-01-01");
second.putAttribute("example.business-date", "2100-01-01");
// Same type and ID make accidental process- or provider-owned ledger
// sharing observable even when the two calls run concurrently.
LedgerProbe firstEntity = new LedgerProbe(42);
LedgerProbe secondEntity = new LedgerProbe(42);
var executor = Executors.newFixedThreadPool(2);
try {
var firstCall = executor.submit(() -> {
String resolved = new String(first.resolveSecret(reference, "first custom context")
.exposeBytes(), StandardCharsets.UTF_8);
firstEntity.updateName("first pending");
return resolved;
});
var secondCall = executor.submit(() -> {
String resolved = new String(second.resolveSecret(reference, "second custom context")
.exposeBytes(), StandardCharsets.UTF_8);
secondEntity.updateName("second pending");
return resolved;
});
assertEquals("2000-01-01", firstCall.get(4, TimeUnit.SECONDS));
assertEquals("2100-01-01", secondCall.get(4, TimeUnit.SECONDS));
} finally {
executor.shutdownNow();
}
assertEquals("2000-01-01", first.getAttribute("example.business-date"));
assertEquals("2100-01-01", second.getAttribute("example.business-date"));
EntityKey ledgerKey = new EntityKey(firstEntity.typeName(), 42L);
assertNotSame(firstEntity.getEntityMutationLedger(), secondEntity.getEntityMutationLedger());
assertEquals("first pending", firstEntity.getEntityMutationLedger().get(ledgerKey, "name"));
assertEquals("second pending", secondEntity.getEntityMutationLedger().get(ledgerKey, "name"));
firstEntity.getEntityMutationLedger().clearCurrentChangeSet();
assertNull(firstEntity.getEntityMutationLedger().get(ledgerKey, "name"));
assertEquals("second pending", secondEntity.getEntityMutationLedger().get(ledgerKey, "name"));
assertTrue(secondEntity.dirtyFields().contains("name"));
System.out.println("SECRET_CONTEXT_LEDGER_ISOLATION_PASS language=java overlap=1 same_id=1 clear_isolated=1");
}
}
Loading