feat(pki-server): support trusted reverse-proxy authentication

Support explicit direct-mTLS and trusted-reverse-proxy authentication
modes with mutually authenticated backend transport.

Keep proxy and end-client principals separate, validate forwarded
certificates independently, and enforce narrowly scoped forwarding
authority for RFC 9440 and NGINX escaped-PEM profiles.
This commit is contained in:
2026-08-04 23:07:01 +02:00
parent 5b896ee2a2
commit 7328f075dd
29 changed files with 1954 additions and 72 deletions

View File

@@ -65,7 +65,9 @@ public final class ServerOperationGateway {
* Complete transport-neutral request admission input.
*
* @param realmId exact realm identity
* @param principalId authenticated or public principal identity
* @param principalId authenticated end-client principal identity
* @param authenticationMode explicit administrative authentication mode
* @param transportPrincipalId trusted proxy identity, present only in proxy mode
* @param operation existing typed operation
* @param resource exact non-bearer resource reference
* @param relationship established object relationship
@@ -73,13 +75,24 @@ public final class ServerOperationGateway {
* @param approvalId optional durable approval reference
* @param correlationId safe finite request correlation identity
*/
public record Request(RealmId realmId, String principalId, AdministrativeOperation operation,
public record Request(RealmId realmId, String principalId,
AdministrativeAuthenticationMode authenticationMode, Optional<String> transportPrincipalId,
AdministrativeOperation operation,
Permission.Resource resource, Permission.Relationship relationship, Permission.Context context,
Optional<String> approvalId, String correlationId) {
/** Validates the immutable request. */
public Request {
Objects.requireNonNull(realmId, "realmId");
Permission.requirePrincipal(principalId);
Objects.requireNonNull(authenticationMode, "authenticationMode");
transportPrincipalId = Objects.requireNonNull(transportPrincipalId, "transportPrincipalId");
transportPrincipalId.ifPresent(Permission::requirePrincipal);
if (authenticationMode == AdministrativeAuthenticationMode.DIRECT_MTLS
&& transportPrincipalId.isPresent()
|| authenticationMode == AdministrativeAuthenticationMode.TRUSTED_REVERSE_PROXY
&& transportPrincipalId.isEmpty()) {
throw new IllegalArgumentException("Gateway authentication identity context is invalid");
}
Objects.requireNonNull(operation, "operation");
Objects.requireNonNull(resource, "resource");
Objects.requireNonNull(relationship, "relationship");
@@ -92,8 +105,17 @@ public final class ServerOperationGateway {
public Request(RealmId realmId, String principalId, PkiOperation operation,
Permission.Resource resource, Permission.Relationship relationship, Permission.Context context,
Optional<String> approvalId, String correlationId) {
this(realmId, principalId, new AdministrativeOperation.Pki(operation), resource, relationship,
context, approvalId, correlationId);
this(realmId, principalId, AdministrativeAuthenticationMode.DIRECT_MTLS, Optional.empty(),
new AdministrativeOperation.Pki(operation), resource, relationship, context, approvalId,
correlationId);
}
/** Creates a direct-mTLS request for existing embedded control callers. */
public Request(RealmId realmId, String principalId, AdministrativeOperation operation,
Permission.Resource resource, Permission.Relationship relationship, Permission.Context context,
Optional<String> approvalId, String correlationId) {
this(realmId, principalId, AdministrativeAuthenticationMode.DIRECT_MTLS, Optional.empty(), operation,
resource, relationship, context, approvalId, correlationId);
}
}
@@ -272,8 +294,7 @@ public final class ServerOperationGateway {
}
if (claimedApproval.isPresent()) approvals.complete(claimedApproval.orElseThrow(), classification(backend));
audit.record("GATEWAY_RESULT", principal.principalId(), exact.resource().objectId(),
Map.of("operation", exact.operation().name(), "result", classification(backend),
"correlation", exact.correlationId()));
gatewayDetails(exact, "result", classification(backend)));
return new Outcome.Executed(backend);
}
ServerControlOperation controlOperation = ((AdministrativeOperation.Control) exact.operation()).operation();
@@ -282,7 +303,7 @@ public final class ServerOperationGateway {
String result = classification(controlResult);
if (claimedApproval.isPresent()) approvals.complete(claimedApproval.orElseThrow(), result);
audit.record("GATEWAY_RESULT", principal.principalId(), exact.resource().objectId(),
Map.of("operation", exact.operation().name(), "result", result, "correlation", exact.correlationId()));
gatewayDetails(exact, "result", result));
return new Outcome.ControlExecuted(controlResult);
}
@@ -316,6 +337,60 @@ public final class ServerOperationGateway {
return decision;
}
/**
* Authorizes a dedicated service principal to attest one forwarded client
* identity. Break-glass grants are deliberately excluded from this transport
* trust decision.
*
* @param principalId authenticated backend TLS peer identity
* @param correlationId bounded request correlation identity
* @return default-deny decision with explicit-deny precedence
*/
public AuthorizationEngine.Decision authorizeForwardedIdentity(String principalId, String correlationId) {
openCheck.run();
Permission.requirePrincipal(principalId);
Permission.requireBounded(correlationId, 256, "correlation ID");
SecurityPrincipal principal = control.requirePrincipal(principalId);
if (principal.type() != SecurityPrincipal.Type.SERVICE) {
return new AuthorizationEngine.Decision(AuthorizationEngine.Code.NO_MATCHING_GRANT, false);
}
List<Permission.Grant> principalGrants = grants(principal);
if (principalGrants.stream().anyMatch(grant -> grant.enabled()
&& grant.effect() == Permission.Effect.ALLOW
&& grant.action() != Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY)
|| !breakGlass.activeFor(principalId).grants().isEmpty()) {
return new AuthorizationEngine.Decision(AuthorizationEngine.Code.NO_MATCHING_GRANT, false);
}
Permission.Resource resource = new Permission.Resource(Permission.ResourceType.REALM,
new Permission.Scope(realmId, Optional.empty(), Optional.empty(), Optional.empty()),
Optional.empty(), Optional.empty());
AuthorizationEngine.Decision decision = authorization.authorize(new AuthorizationEngine.Request(realmId,
exposure, principal, Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY, resource,
Permission.Relationship.ANY, Permission.DataView.METADATA_REDACTED, Permission.Context.empty(),
principalGrants, java.util.Set.of()));
audit.record(decision.allowed() ? "PROXY_FORWARD_ALLOW" : "PROXY_FORWARD_DENY", principalId,
Optional.empty(), Map.of("action", Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY.name(),
"code", decision.code().name(), "correlation", correlationId));
return decision;
}
/**
* Fails startup when a configured proxy service principal has any enabled
* allowing authority beyond forwarded-identity attestation.
*
* @param principalId configured trusted proxy principal
*/
public void validateForwardingPrincipal(String principalId) {
openCheck.run();
SecurityPrincipal principal = control.requirePrincipal(principalId);
if (!principal.enabled() || principal.type() != SecurityPrincipal.Type.SERVICE
|| grants(principal).stream().anyMatch(grant -> grant.enabled()
&& grant.effect() == Permission.Effect.ALLOW
&& grant.action() != Permission.Action.FORWARD_AUTHENTICATED_CLIENT_IDENTITY)) {
throw new IllegalArgumentException("Trusted proxy principal authority is not narrowly scoped");
}
}
/**
* Returns operation descriptors discoverable by one principal without
* returning grant scopes or protected resource identities.
@@ -463,11 +538,21 @@ public final class ServerOperationGateway {
private Outcome denied(Request request, AuthorizationEngine.Code code) {
audit.record("AUTHORIZATION_DENY", request.principalId(), Optional.empty(),
Map.of("operation", request.operation().name(), "code", code.name(),
"correlation", request.correlationId()));
gatewayDetails(request, "code", code.name()));
return new Outcome.Denied(code);
}
private static Map<String, String> gatewayDetails(Request request, String resultName, String result) {
Map<String, String> details = new LinkedHashMap<>();
details.put("operation", request.operation().name());
details.put(resultName, result);
details.put("correlation", request.correlationId());
details.put("authenticationMode", request.authenticationMode().name());
details.put("endClientPrincipal", request.principalId());
request.transportPrincipalId().ifPresent(value -> details.put("transportPrincipal", value));
return Map.copyOf(details);
}
private static Permission.Action action(PkiOperation operation, Permission.Action defaultAction) {
if (operation instanceof PkiOperation.TransitionAuthority transition) {
return switch (transition.state()) {