Spring AI Security & Compliance: Where Secrets, Authorization, PII Boundaries, and Audit Logs Actually Live

July 11, 2026 · Spring AI for Enterprise Java (part 11)

▶ Watch on YouTube & subscribe to The Stack Underflow

Your security team’s first question about a new AI feature is never “does it work.” It’s “where’s the key, who’s allowed to call it, what happens to the PII in the prompt, and can you prove what it did.” If you can’t answer all four before the demo, the feature doesn’t ship — no matter how good the model output looks.

The good news: none of this requires a separate security system bolted onto your Spring AI app. If you’ve been building through this series — ChatClient, RAG, tool calling, MCP, observability, evaluation — you already have the stack these four controls attach to. This episode places them on it: one config property, two authorization checkpoints, two advisors in the chain, and a logger you’re probably already one line away from having.

We’ll build this against Spring AI 2.0.0 GA, which requires Java 21 to build (Spring Boot 4.0’s runtime floor is Java 17, but building Spring AI 2.0.0 itself needs 21) and Spring Boot 4.0 / Spring Framework 7.0. The starter coordinate is org.springframework.ai:spring-ai-starter-model-openai:2.0.0.

The one-sentence version: compliance for a Spring AI app isn’t a new system — it’s four controls (secrets, authorization, a data boundary, and an audit trail) placed at exact points on the app/Spring-AI/model stack you’ve already built.

The four questions, mapped to the stack

Picture the stack in three bands: your application code, the Spring AI abstraction layer (ChatClient and its advisor chain), and the model provider on the other side. Each compliance question has a specific home:

QuestionControlWhere it lives
Where’s the key?Secretsspring.ai.openai.api-key, externalized config
Who’s allowed to call it?AuthorizationSpring Security around the app + a check inside @Tool
What happens to PII in the prompt?Data boundarySafeGuardAdvisor + ModerationModel in the advisor chain
Can you prove what it did?AuditSimpleLoggerAdvisor + a Micrometer observation

Two of these — the data boundary and audit — are things Spring AI ships as real classes. Two — authorization and, to a lesser extent, secrets management — are standard Spring patterns you’re pointing at the model call for the first time. Keeping that distinction straight matters when you’re talking to a compliance reviewer who will ask “is this a framework guarantee or your team’s convention.”

Control 1: secrets never live in code

The API key is the easiest control to get right and the most common one to get wrong in a demo repo. In Spring AI 2.0.0, it’s a single externalized property, bound from an environment variable or a vault at startup:

# application.yaml
spring:
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      chat:
        model: gpt-4o-mini

That spring.ai.openai.api-key property is read by AbstractOpenAiProperties (getApiKey() / setApiKey()), and it’s the one path forward in 2.0.0. There’s a real delta worth flagging if you’re coming from the 1.0.3-era course material or an older tutorial: OpenAiChatProperties.Options.apiKey — a per-call, options-level override — is now @Deprecated(since = "2.0.0", forRemoval = true), annotated @DeprecatedConfigurationProperty(replacement = "spring.ai.openai.api-key"). If you see code setting the key through ChatOptions on a per-request basis, that’s the deprecated shape. The top-level property is the only one to build on going forward.

The compliance-relevant point isn’t the property name, it’s the pattern: the key never appears in a .java file, a commit, or a hardcoded string anywhere in the request path. It comes from ${OPENAI_API_KEY}, which your platform (Kubernetes secret, Vault, AWS Secrets Manager, whatever your org standardizes on) injects at runtime.

Control 2: authorization at two layers

This is the one control where Spring AI itself provides nothing — and that’s the honest answer to give a reviewer who asks “does Spring AI have role-based access control for the model.” It doesn’t. There’s no Spring-AI-specific Spring Security bridge as of 2.0.0. What you actually build is two separate checkpoints, both standard Spring, both pointed at your AI code for the first time.

The outer checkpoint is ordinary Spring Security around whatever @RestController or @Service fronts your ChatClient call:

@RestController
@RequestMapping("/api/support")
class SupportController {

    private final ChatClient chatClient;

    SupportController(ChatClient.Builder builder) {
        this.chatClient = builder.build();
    }

    @PreAuthorize("hasRole('SUPPORT_AGENT')")
    @PostMapping("/query")
    String query(@RequestBody String question) {
        return chatClient.prompt()
            .user(question)
            .call()
            .content();
    }
}

@PreAuthorize and SecurityContext decide who can reach your endpoint at all. That’s your architecture — the same Spring Security you’d put in front of any other service, not a new AI-specific mechanism.

The inner checkpoint is narrower and lives inside every @Tool-annotated method a model can call. @Tool and @ToolParam (org.springframework.ai.tool.annotation) have no built-in access-control attribute — no @Tool(role = "...") exists in 2.0.0. If a tool can delete a record or issue a refund, the least-privilege check is code you write inside the method:

@Tool(description = "Issue a refund for an order")
String issueRefund(@ToolParam(description = "Order ID") String orderId,
                    @ToolParam(description = "Amount in cents") long amountCents) {
    if (!currentCaller().hasPermission("REFUND_ISSUE")) {
        return "Not authorized to issue refunds.";
    }
    if (amountCents > currentCaller().refundLimitCents()) {
        return "Amount exceeds this caller's refund limit.";
    }
    return refundService.issue(orderId, amountCents);
}

Two checkpoints, not one: Spring Security decides who can call your app; your own code inside the tool decides what this caller is actually allowed to do once they’re in.

Control 3: the data boundary — blocking and flagging PII before the model sees it

This is where Spring AI 2.0.0 does real, shippable work. Two advisors sit in the ChatClient advisor chain, both running before a request ever crosses to the model provider.

SafeGuardAdvisor blocks the call outright if user input contains configured sensitive words. It implements both CallAdvisor and StreamAdvisor, and it’s built through SafeGuardAdvisor.Builder:

Advisor safeGuardAdvisor = SafeGuardAdvisor.builder()
    .sensitiveWords(List.of("ssn", "social security number", "credit card"))
    .failureResponse("I can't process requests containing that information.")
    .order(0)
    .build();

ModerationModel, in org.springframework.ai.moderation, gives you a PII-detection signal instead of a hard block — Categories.isPii() and CategoryScores.getPii() let you flag and route a request (say, to a human reviewer or a redaction step) rather than reject it outright:

ModerationResponse response = moderationModel.call(
    new ModerationPrompt("My SSN is 000-00-0000, can you help with my claim?")
);
Categories categories = response.getResult().getOutput().getCategories();
if (categories.isPii()) {
    // route to redaction / human review instead of the model
}

Wire both into the ChatClient through the fluent advisors(...) attach point:

ChatClient chatClient = builder
    .defaultAdvisors(
        safeGuardAdvisor,
        SimpleLoggerAdvisor.builder().build()
    )
    .build();

ChatClient exposes advisors(Advisor...), advisors(List<Advisor>), and advisors(Consumer<ChatClient.AdvisorSpec>) — pick whichever fits how the rest of your app assembles the chain. The important architectural fact: SafeGuardAdvisor and ModerationModel are real, current Spring AI 2.0.0 classes, not something you’re bolting on next to the framework.

Control 4: audit — proving what happened, not just that it worked

Every request, blocked or allowed, should leave a trail. SimpleLoggerAdvisor (org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor) logs the request and response for every call that passes through it, and — like SafeGuardAdvisor — it’s a real CallAdvisor / StreamAdvisor you drop into the same chain:

Advisor auditLogger = SimpleLoggerAdvisor.builder().build();

Pair it with the Micrometer observation that’s already instrumenting every ChatClient call through ChatClientObservationConvention and ChatModelObservationConvention. You don’t fabricate metric names for this — the observation exists per call, and it’s the mechanism your observability stack (from Episode 9) taps to answer “who called it, what was asked, when, and what did it cost.” The combination of a structured log line and a Micrometer span is what “provable” looks like to a compliance reviewer — not a claim, an artifact they can query.

Pattern vs. feature: know which is which

Before you tell a compliance team “Spring AI handles this,” check the column:

Real Spring AI feature (cite by class name)Your architecture pattern (built with standard tools)
SafeGuardAdvisorSpring Security (@PreAuthorize, SecurityContext)
ModerationModel / Categories.isPii()Least-privilege check inside @Tool
spring.ai.openai.api-keyProfile-scoped config (spring.profiles.active=...)
SimpleLoggerAdvisor

Being precise here isn’t pedantry — a reviewer who’s told “Spring AI enforces role-based access” and later finds no such class in the framework will (correctly) stop trusting the rest of your answers. Say what’s a shipped guarantee and what’s your team’s convention, every time.

How to apply this right now

  1. Grep your codebase for hardcoded API keys. Anything that isn’t reading from spring.ai.openai.api-key (or the equivalent property for your provider) is a finding waiting to happen.
  2. Replace any OpenAiChatProperties.Options.apiKey usage. It’s deprecated for removal in 2.0.0 — move the value to the top-level spring.ai.openai.api-key property.
  3. Put @PreAuthorize (or your org’s equivalent) on every controller endpoint that fronts a ChatClient call. Don’t assume “it’s just a chat endpoint” exempts it from the same access rules as any other service.
  4. Audit every @Tool method for a privilege check. If a tool can mutate state or read sensitive data, the model choosing to call it isn’t authorization — your code inside the method is.
  5. Add SafeGuardAdvisor with a sensitive-words list scoped to your domain, not a generic placeholder list — think in terms of what your specific data model treats as sensitive.
  6. Wire a ModerationModel check into any flow that accepts free-text user input headed toward a ChatClient call, and decide up front what “PII flagged” does — redact, route to review, or reject.
  7. Confirm SimpleLoggerAdvisor (or an equivalent audit advisor) is in every advisor chain, not just the ones you remembered to add logging to.
  8. Check that your Micrometer/observability pipeline actually captures the chat observation, not just HTTP-layer metrics — that’s the layer a compliance query needs.

Common misconceptions

“Spring AI has built-in role-based access control for tools.” It doesn’t. @Tool and @ToolParam carry no authorization attribute in 2.0.0. Any access control on a tool call is code you write inside the method body — a real gap to plan for, not an oversight in your reading of the docs.

“Setting the API key through ChatOptions per-request is the modern pattern.” It’s the opposite — OpenAiChatProperties.Options.apiKey is deprecated for removal as of 2.0.0. The externalized top-level property, spring.ai.openai.api-key, is the one supported path.

SafeGuardAdvisor and ModerationModel do the same thing.” They don’t. SafeGuardAdvisor is a hard block on configured sensitive words before the call reaches the model. ModerationModel is a classification signal — it flags content (including PII, via Categories.isPii()) without necessarily stopping the request. Blocking and flagging are different controls with different failure modes; use both where each is appropriate.

“Logging the request and response is enough for an audit trail.” A log line tells you what happened once; a Micrometer observation (via ChatClientObservationConvention) gives you queryable, aggregable evidence across every call — who, when, and cost, not just the last payload you happened to log. Compliance audits ask for patterns over time, not spot checks.

Frequently asked questions

Does Spring AI provide its own authentication or authorization system for AI endpoints? No. As of 2.0.0, there is no Spring-AI-specific Spring Security bridge. You secure the endpoints in front of your ChatClient calls with standard Spring Security (@PreAuthorize, SecurityContext) exactly as you would any other service, and you add your own privilege checks inside @Tool methods for finer-grained control.

What’s the difference between SafeGuardAdvisor and a ModerationModel call? SafeGuardAdvisor blocks a request outright when it matches a configured list of sensitive words — the call to the model never happens. ModerationModel, via Categories.isPii() and CategoryScores.getPii(), classifies content and flags it (for PII or other categories) without necessarily blocking it, so you can redact, route, or log the request instead of rejecting it.

Is spring.ai.openai.api-key still safe to use, or is it also deprecated? It’s the current, non-deprecated, top-level path — bound by AbstractOpenAiProperties. What’s deprecated is a different, narrower thing: OpenAiChatProperties.Options.apiKey, a per-call override at the options level. If your code only ever sets the key once at the top-level property, you’re already on the supported path.

How do I prove to an auditor that a specific AI call happened and what it cost? Combine SimpleLoggerAdvisor (a structured log line per call) with the Micrometer observation that ChatClientObservationConvention and ChatModelObservationConvention already produce for every ChatClient invocation. The observation is your queryable, aggregable record — who called it, when, and the token/cost data your observability backend captures from it.

Do I need Java 21 to run a Spring AI 2.0.0 app in production? You need Java 21 to build it — Spring AI 2.0.0 requires Java 21 as the build baseline even though Spring Boot 4.0’s runtime floor is Java 17. Confirm your CI and local toolchains are pinned to 21 before you start adding these advisors; a build-tool mismatch here is a common first failure.

Can I use SafeGuardAdvisor and ModerationModel together in the same advisor chain? Yes, and that’s the recommended shape — SafeGuardAdvisor as an early hard stop for known sensitive terms, ModerationModel as a classification pass for softer signals like PII that you want to route rather than reject. Attach both through ChatClient’s advisors(Advisor...) (or the List/Consumer<AdvisorSpec> overloads) alongside SimpleLoggerAdvisor so every request — blocked, flagged, or clean — is logged the same way.

Where this fits in the series

This is Episode 11 of Spring AI for Enterprise Java. The previous episode covered evaluation and guardrailsEvaluator, RelevancyEvaluator, and FactCheckingEvaluator for checking output quality before it reaches a user. This episode adds the control layer around the call itself: secrets, authorization, the PII boundary, and audit.

Next up, the full production blueprint assembles everything from this series — ChatClient, RAG, tools, MCP, observability, evaluation, and the four controls covered here — into one production-style application.

Runnable code for this episode lives in the 11-security-compliance module of the spring-ai-enterprise-examples repo.

Spring AI moves fast, and this series is pinned to 2.0.0 — if an API in this post shifts in a later release, the update notes will be at thestackunderflow.com/spring-ai. If you want the video walkthrough of this exact stack when it’s live, subscribe to @TheStackUnderflow on YouTube.

Found this useful? The deep version lives on YouTube — new breakdowns of how AI dev tools actually work, weekly.

Subscribe on YouTube →