The topic of security covers many different facets within the SDLC. From focusing on secure application design to designing systems to protect computers, data, and networks against potential attacks, it is clear that security should be top of mind for all developers. This Zone provides the latest information on application vulnerabilities, how to incorporate security earlier in your SDLC practices, data governance, and more.
Why Continuous Application Security Testing Is No Longer Optional
Part 2: Securing and Scaling Goose-to-Java Agent Traffic With agentgateway
In this blog, we will continue our discussion from the previous blog, Parts 1 and 2. If you have not read it, please read it once. So far, we have discussed Caesar cipher, Vigenere cipher, symmetric encryption, AES, convergent encryption, and IV. If all these terms sound familiar to you — great! If not, please go back and read Part 1 and Part 2 first. Now, in Part 2, we ended with a teaser that in the next blog we will talk about hashing and asymmetric algorithms. So let's get into it! First, Let's Talk About Hashing So far, everything we discussed was about encryption and decryption — you encrypt something, and you can decrypt it back. Simple. But what if I tell you there is a technique where you convert data into something, and you can NEVER go back to the original? Sounds weird, right? Why would someone do that? Let me give you a real-life example. Imagine you are the owner of a hostel. You keep a register at the gate. Every night at 10 PM, you take a photo of this register. Now, the next morning, if someone modifies the register (adds a fake entry or removes one), you can easily compare yesterday's photo with today's register and catch the change. Hashing works exactly like this photo. You give any data as input; the hash function produces a fixed-size string (called a hash or digest). If even ONE character in the original data changes, the hash output changes completely. A Simple Example Input: "Sahil" Hash (SHA-256): 9b4c...a32f (a 64 character string) Input: "sahil" (just lowercase 's') Hash (SHA-256): 7f3a...b91e (a completely different 64-character string!) See? Even one small change → completely different hash. This property is called the Avalanche Effect. Important Properties of Hashing Let's keep it simple. A good hash function has these properties: One way (Irreversible): You can go from "Sahil" → hash, but NOT from hash → "Sahil." It's a one-way street. Like making an omelet from an egg — you can't get the egg back from the omelet.Deterministic: The same input will ALWAYS give the same output. "Sahil" will always produce the same hash every time.Fixed size output: No matter how big your input is — whether it's one word or an entire 1000-page book — the output hash size is always the same (for SHA-256, it's always 64 characters).Avalanche effect: Even a tiny change in input means a completely different hash. We just saw this above. So, Where Is Hashing Used? Great question! Here are the most common places: 1. Storing Passwords This is the most common use case. When you set a password on any website, good websites never store your actual password. They store the hash of your password. So when you log in next time: You type your passwordThe website hashes itCompares it with the stored hashIf they match → Welcome! This is why when you click "Forgot Password" on most websites, they reset your password instead of showing you the old one. Because they literally don't know what your old password was! 2. File Integrity Check You download software from the internet. How do you know no one tampered with it during download? The website gives you the hash of the original file. After downloading, you calculate the hash of your downloaded file. If they match, the file is safe! This is used everywhere — Linux ISO downloads, software releases on GitHub, etc. 3. Digital Signatures We will cover this more in detail in coming parts! Popular Hashing Algorithms MD5 – Old, fast, but now considered weak. Avoid using it.SHA-1 – Also old, mostly deprecated now.SHA-256 – The current gold standard. Used everywhere. (Bitcoin also uses this!)bcrypt/Argon2 – Special hashing algorithms designed specifically for passwords. They are intentionally slow — which makes brute force attacks harder.PBKDF2 (Password-Based Key Derivation Function 2) – Another password-specific algorithm. It takes your password + a salt and runs a hashing function thousands of times in a loop (this is called key stretching). The more iterations, the harder it is to brute force. It is widely used and is the recommended choice in many government and enterprise security standards (like NIST). Wait — Can Someone Still Crack Hashes? Yes! There are ways to try. The most common one is called a Rainbow Table Attack. Here's how it works — imagine I am a hacker and I have pre-calculated the hashes of millions of common passwords: "password" → 5f4dcc..."123456" → e10adc..."admin" → 21232f... Now if I get your stored hash from a database breach, I just look it up in my table. If your hash matches any entry → I know your password! Solution? SALT! No, not the one you put in food In cryptography, a Salt is a random value that is added to your password before hashing. Your password: "mypassword" Random Salt: "xK9#mQ" Combined: "mypasswordxK9#mQ" Hash of this: (some unique hash) Now, even if two people have the same password "mypassword", because their salts are different, their stored hashes will be completely different! Rainbow Table attacks become useless. The salt is stored alongside the hash in the database (it's not a secret; it just needs to be unique per user). Now Let's Talk About Asymmetric Encryption Remember in Part 2 we discussed symmetric encryption — where the same key is used for both encryption and decryption? The problem with symmetric encryption is — how do you share the key safely? Imagine Rahul in Delhi wants to send an encrypted message to Priya in Mumbai. He needs to share the key with her first. But if he sends the key over the internet, a hacker can intercept the key and then decrypt all future messages. This is known as the Key Distribution Problem. Asymmetric encryption solves this beautifully. The Magic of Two Keys In asymmetric encryption, instead of one key, you have two keys: Public key – You share this with the WHOLE WORLD. Anyone can have it.Private key – This stays with you ONLY. Never share it with anyone. The magic is: Whatever is encrypted with the Public Key can ONLY be decrypted with the Private Key. And these two keys are mathematically linked to each other. Real Life Example — The Magic Mailbox Think of it like a special mailbox: The mailbox has a slot (public key) — anyone can drop a letter in it.But only YOU have the key to open the mailbox (private key) — only you can read the letters. Rahul wants to send a secret message to Priya: Priya shares her Public Key with Rahul (and the whole world — no problem!)Rahul uses Priya's Public Key to encrypt the messageThe encrypted message travels over the internet — even if a hacker intercepts it, they can't read itPriya uses her Private Key to decrypt the message No need to share any secret key beforehand! The problem of key distribution is solved! Most Popular Asymmetric Algorithm: RSA RSA (named after its inventors Rivest, Shamir, and Adleman) is the most famous asymmetric algorithm. It is based on a very simple mathematical observation: It is very easy to multiply two large prime numbers. But it is extremely hard to factorize the result back into those two primes. For example: Easy: 61 × 53 = 3233Hard: Given 3233, find the two prime factors (61 and 53) When the numbers are hundreds of digits long, even the fastest computers in the world would take millions of years to crack it. That's the security of RSA! RSA key sizes you will commonly see: 1024-bit (old, avoid), 2048-bit (current standard), 4096-bit (extra secure). Symmetric vs. Asymmetric — When to Use What? Symmetricasymmetric Keys Same key for encrypt & decrypt Different keys (public + private) Speed Very Fast Slow Key Sharing Problem Yes, it exists No, solved! Example Algo AES RSA Used For Encrypting large data Key exchange, Digital Signatures In the real world, both are actually used together! The typical flow is: Use asymmetric encryption to securely exchange a secret keyThen use symmetric (AES) encryption for the actual data — because it's much faster This combo is how HTTPS (the secure web) actually works! When you open any https:// website, this exact thing is happening in the background. That little lock you see in your browser? That's this. Terms We Have Learned So Far (Including Parts 1 & 2) CryptographyAlgorithmPlain textKeyCipher textSymmetric encryptionConvergent encryptionInitialization vector (IV)HashingHash/DigestAvalanche effectSaltRainbow table attackAsymmetric encryptionPublic keyPrivate keyRSA Please keep them in mind, as these are the generic terms used everywhere in the world of encryption and decryption. Coming in Part 4 (Part 4 is in progress — stay tuned!) In the next blog, we will gossip about some very interesting things like: Digital signatures – How do you prove that a message is really from who it claims to be from?PKI infrastructure – The backbone of trust on the internetSSL/TLS – What actually happens when you open an HTTPS website, step by stepEnvelope encryption – A very clever technique used by cloud providers like AWS and GCPAnd more... Stay tuned for Part 4! If you liked this blog, do give it a like and share it with someone who you think should learn this. Let's spread the knowledge! Read the previous parts here: Part 1 and Part 2.
An AI agent is not defined by how intelligently it talks. It's defined by what it's trusted to do. Give a language model a chat window, and you have an interface. Give it access to production APIs, identity, business logic, memory, and the authority to execute actions on your behalf, and you have something categorically different: a new kind of software actor, one that can read your data, write to your systems, and make decisions faster than any human reviewer can watch in real time. That distinction is where most of the industry's "agent" marketing quietly falls apart. And in 2026, the gap between marketing and engineering has become measurable. AvePoint's third annual State of AI report, based on 750 global IT leaders across financial services, healthcare, and government, found that 88.4% of organizations experienced at least one AI agent–related security breach in the past twelve months, with data leakage and manipulation by untrusted input as the two leading causes (AvePoint, State of AI 2026). Separately, McKinsey's 2026 AI Trust Maturity Survey — fielded across roughly 500 organizations between December 2025 and January 2026 — found that only about 30% of organizations have reached a mature level of governance and agentic controls (McKinsey, State of AI Trust 2026). Adoption has outrun governance by a wide margin, and the bill is starting to come due. I spent several hours this year with Ted Kornish, CTO of Gravity, a platform built for corporate sustainability and emissions reporting, to press on a narrower question than "does your product have an agent?" The question was: what has to be true, architecturally, before a system earns the word at all? Kornish's answers are the evidence in this piece, not the subject. Gravity is a useful case study — a production system operating in a regulated domain where a wrong answer can end up in front of a regulator — but the principles below are my own synthesis of what separates a genuine enterprise agent from a chatbot wearing a lanyard. The Five-Question Agent Test Before accepting any vendor's "AI agent" claim — including Gravity's — I now run it through five questions: Can it execute actions, not just generate recommendations?Can it compose multiple capabilities to accomplish a goal it wasn't explicitly scripted for?Does it operate through the same business logic and APIs your human users already use?Can every action it takes be attributed to a specific identity and audited after the fact?Can a potentially destructive action be previewed, validated, and approved before it commits? If the answer to the first three is no, you're looking at an AI feature — useful, maybe, but bounded and predictable. If the answer to four and five is no, you're looking at an AI feature that probably shouldn't have production access yet, regardless of how capable its model is. Kornish's version of this test, from our conversation, is the cleanest I've heard a vendor articulate unprompted: "If you can list every task it handles, it is a feature. If you can't because it can compose a wide variety of platform capabilities, it's an agent." Ask a vendor to enumerate every task their agent handles on a sales call. Most stall out after three or four items, and the stall itself is diagnostic — a genuine agent's capability surface resists a clean list because it's compositional, not because nobody documented it. Why API-First Architecture Is the Real Moat Here's the engineering claim underneath the marketing claim, and it's the one I'd stake the rest of this article on. Most enterprise platforms were built UI-first, with an API bolted on afterward as a partial mirror of what the interface already does. That ordering was fine for a decade of humans clicking buttons through a screen. It becomes a structural liability the instant an agent needs to act with the same range a human employee has — because if the API was never built to be complete, an engineering team is stuck choosing between two bad options: let the agent do less than a human could, or build a second, parallel execution path just to give it a fighting chance. Neither ages well. The first caps the agent's usefulness permanently. The second means maintaining two versions of every business rule indefinitely, with near-certainty that they drift apart in some edge case nobody thought to test. Gravity sidestepped that fork by never building a second path: "We never built a separate 'agent version' of Gravity with its own private set of functions. On the read path, the agent calls the exact same HTTP API our product UI calls... On the write path, there's a service layer that provides one execution path for any given action, one place validation and business rules live, and one codebase to test and maintain instead of two." The architectural implication here is bigger than agent convenience. Once an agent becomes just another API client, the API itself becomes part of the agent's safety boundary. Authentication, authorization, idempotency, validation, rate limiting, transaction handling, and auditability stop being secondary infrastructure and become prerequisites for autonomy. That reframes the first question an engineering team should ask when evaluating agent readiness. It isn't "which model should we use." It's closer to: can an untrusted software actor safely exercise the application capabilities we already have? Kornish's line on this is worth sitting with: "An agent is really just a new kind of caller on an API that was already capable; if that API doesn't exist, there's nothing for the agent to stand on." That's not a race to fine-tune the flashiest model. It's a race to have quietly built a serious, typed, permissioned API years before anyone had a reason to think an LLM would ever call it — and most incumbent platforms already lost that race without knowing it, because the decision was made a decade ago by a team with no reason to think about agents at all. The hardest part, in his account, isn't reads. Reads are comparatively low-stakes; nothing breaks if a summary is slightly stale. Writes are a different category of problem entirely, because most enterprise write paths aren't atomic — you're editing individual records one at a time, and the only thing worse than writing the wrong data is writing partial data, which leaves the system in a state nobody designed for and nobody can cleanly roll back. That's the real, unglamorous reason so many "enterprise AI agents" on the market today are read-only interfaces wearing an agent's branding: atomic writes are hard, and most legacy write paths were never built for it. What This Looks Like In Production Abstract architecture arguments are easy to nod along to and hard to actually picture. Here's a concrete workflow, reconstructed from how Gravity describes its own execution path for a regulatory reporting task: Plain Text User: "Prepare this quarter's emissions report and flag anything that needs my attention." ↓ Agent — parses the objective, checks its ~85 versioned skills for the relevant reporting operations (progressive disclosure: hand the model what's relevant to this step, not everything) ↓ APIs — retrieves source data through the same HTTP API the product UI uses, governed by the same permissions ↓ Deterministic Engine — runs the actual emissions math; the model never touches the calculation itself ↓ Validation — checks for anomalies, missing fields, and inconsistencies against prior filings ↓ Agent — drafts the report narrative, cites every source and every calculation it pulled from ↓ Human — reviews the staged preview, approves or rejects ↓ Production — the report commits; the full trace is retained The model's job in that chain isn't the arithmetic — it's everything around the arithmetic. Sourcing the right document, filling a gap, matching the tone of last year's filing so the report reads as one continuous document rather than two authors stitched together. The ground truth stays boring and deterministic. The model's intelligence gets spent on the parts that used to consume a compliance team's entire week. Agent vs. Copilot vs. Automation The three categories get flattened together constantly, and the flattening is exactly what lets the word "agent" get retrofitted onto anything with a chat box. CapabilityCopilotAutomationAI AgentGenerates contentYesSometimesYesFollows a predefined workflowYesYesYesHandles goals it wasn't explicitly scripted forLimitedNoYesComposes tools/capabilities dynamicallyLimitedNoYesExecutes production actionsLimitedYesYesRequires human approval on risky actionsUsuallySometimesShould, by designMaintains task state across sessionsLimitedYesYesAdapts execution mid-taskLimitedNoYes These categories overlap in practice more than the table suggests, and that's worth saying plainly: the meaningful distinction isn't the marketing label a vendor chose; it's the system's actual degree of autonomy, compositionality, and execution authority. The Five Layers, and Why They Aren't Interchangeable Looking at Gravity's stack alongside broader enterprise agent design patterns, a consistent structure emerges — five layers, each dependent on the one below it, each a distinct point of failure if it's missing or built poorly. Plain Text Layer 5 — Governance Identity · audit · human approval ↑ Layer 4 — Execution Layer The API business logic actually runs through ↑ Layer 3 — Domain Skills Versioned, tested, maintained like production content ↑ Layer 2 — Planning & Reasoning Goal decomposition, task checklists ↑ Layer 1 — Foundation Model Replaceable, increasingly commoditized My interpretation of this stack is that the layers are not equally substitutable. The foundation model is replaceable — most serious vendors have access to roughly the same handful of frontier models, and Layer 1 is where competitive advantage is evaporating fastest. The planning harness can evolve; skills can be versioned and rolled back like any other content. But the execution and governance layers are much harder to swap out, because they encode an organization's actual operating rules — its permission model, its validation logic, its regulatory obligations. That suggests the durable competitive advantage in enterprise agents sits lower in the stack than most of the AI discourse right now assumes. Everyone is fighting over Layer 1. Most of the actual failures live in Layer 4 and Layer 5 — the parts that don't show up in a demo. Kornish's own framing on where the industry is spending its engineering effort was more candid than I expected from a CTO on the record: "we're very bitter-lesson-pilled over here: increasingly, the agent is just a model and a thin harness... the system prompt is getting shorter over time as the models internalize more capabilities." The elaborate custom orchestration graphs that dominated agent architecture discussions for the last two years are being displaced — thinner harnesses, base models absorbing more of that responsibility natively, and open protocols like Model Context Protocol doing at the ecosystem level what bespoke orchestration used to do inside one vendor's codebase. My Technical Take: The Agent Is Only As Strong As Its Execution Boundary Here's the architectural lesson I actually take from this, stated plainly and separately from anything Kornish said: an enterprise agent shouldn't be designed as an intelligent application sitting on top of an existing platform. It should be designed as an execution client operating inside a security and transaction boundary that already exists independently of it. That distinction sounds pedantic until you trace what happens when it's missing. If a model can reason but can't safely execute, it's a copilot — useful, bounded, not what we're talking about in this piece. If it can execute but bypasses the platform's own authorization and validation logic to do so, it's not an agent, it's a security liability wearing an agent's branding. If it can execute safely but nothing about that execution is observable after the fact, it's operationally untrustworthy regardless of how well it performed in the room. And if it clears all three of those bars but is never re-evaluated as the model, the skills, and the surrounding data shift, its reliability will decay quietly, without anyone noticing until an incident forces the question. That chain leads to a fairly simple principle, and it's the one I'd want any engineering team to write on a whiteboard before they start: the model should decide what to attempt. The platform should decide what is allowed to happen. Collapse that distinction — let the model's judgment double as the authorization check — and you don't have an agent. You have a very articulate way of bypassing your own access controls. I'd formalize this as the agent execution boundary — the layer, architecturally distinct from the model itself, where a proposed action gets checked against identity, authorization, validation, deterministic logic, and (ideally) a dry run, before it's ever allowed near production data: Plain Text USER INTENT │ ▼ ┌─────────────────────┐ │ AI MODEL │ │ Reason/Plan │ └──────────┬──────────┘ │ ▼ ┌─────────────────────┐ │ AGENT SKILLS │ └──────────┬──────────┘ │ ▼ ┌─────────────────────────┐ │ EXECUTION BOUNDARY │ │ │ │ Identity │ │ Authorization │ │ Validation │ │ Deterministic Logic │ │ Dry Run │ │ Audit │ └────────────┬────────────┘ │ Human Approval │ ▼ PRODUCTION DATA The model should never be the final authority on whether an action is valid. It proposes; the execution boundary determines whether the proposal is permitted, reversible, and safe to commit. Everything Gravity described to me — the shared API surface, the dry-run engine, the staged preview, the permission-inherited approval gate — is one implementation of that boundary. It isn't the only valid implementation. But any architecture that skips having one, in favor of trusting the model's own judgment about what's safe, is building on a foundation that will eventually fail in a way nobody can cleanly attribute afterward. Deterministic vs. Probabilistic Is the Real Design Boundary The design decision that matters most isn't "AI versus not AI." It's probabilistic versus deterministic execution, and a serious architecture puts uncertainty exactly where judgment is valuable and removes it everywhere correctness is mandatory. Kornish didn't hedge when I asked him where Gravity draws that line: "Emissions math always runs through our deterministic calculation engine, no exceptions, because a regulator does not accept 'the model recalculated it slightly differently this time' as an answer." His broader rule generalizes well past sustainability reporting into any regulated domain — healthcare, finance, insurance: where correctness counts, use a calculator; where you need judgment, use a person; where approximate correctness is fine, AI saves time and money. The model can decide which document to inspect, which skill to invoke, and how to explain an anomaly in plain language. It should not silently become the source of truth for a number that could have been deterministically reproduced. Any output that could become evidence in an audit, a regulatory filing, or a legal dispute should trace back to a deterministic calculation — never a model's best guess, no matter how good that guess usually is. "Usually" is not a standard a regulator will accept, and it shouldn't be a standard engineering teams accept for themselves either. What Gravity Gets Right — And Where I'd Push Back To be specific about what Gravity's architecture actually demonstrates, rather than just asserting it's good: API-first execution with no parallel agent path, a deterministic calculation engine that the model cannot override, permission inheritance instead of a separate AI policy layer, staged writes with mandatory human approval, persistent task state across sessions, a visible reasoning trace and tool-invocation history, and continuous evaluation fed by production telemetry rather than a one-time launch gate. Gravity isn't interesting because it has an AI agent. What's interesting is the infrastructure surrounding the agent — the model is one component of the system; the API, the identity model, the deterministic engine, and the governance layer are what determine whether that model can be trusted to act in the real world at all. That said, don't mistake this for an endorsement without edges. A few things I'd want to see stress-tested before calling any system like this "solved," Gravity included: how the skill-versioning process holds up under a genuinely adversarial red-team exercise, not just replay against known scenarios; whether the "agent inherits operator permissions" model actually closes the gap on authorized-but-unintended actions (more on that below); and how the observability stack performs under a long-running, multi-day task where context has had real time to drift. None of these are unique to Gravity. They're unresolved for the industry broadly, which is exactly why they're worth naming instead of glossing over. Identity Is the Security Boundary — But It Isn't the Whole Boundary This is where I'd ask security-minded readers to slow down, because the industry's threat model for agents is still catching up to what's actually being deployed in production right now. Kornish's framing — treat the agent as a new employee with its own login, not a master key that opens every door in the building — maps directly onto zero trust: never assume implicit trust from network location or system role, verify explicitly on every call, grant only the minimum privilege the task requires. "Every action carries the operating user's identity through the entire call chain," he told me, "so when the agent hits our API, it's authorized exactly the way that user would be, not under some elevated service account." Permissions get recalculated on every request rather than cached at login — cached permissions are exactly the kind of stale-state bug that turns into an incident report months after the access was actually revoked. From a security engineering perspective, this changes the traditional question of "what can the agent access" into a more useful one: under whose authority is this specific action being performed? That distinction matters because it should follow the action through the entire call chain, not just the login event. But identity and least privilege, however well implemented, are necessary and not sufficient — and 2026 has produced hard evidence of exactly where that gap sits. In June 2025, researchers at Aim Security disclosed a zero-click prompt injection against Microsoft 365 Copilot, later assigned CVE-2025-32711 with a CVSS score of 9.3 and nicknamed EchoLeak. A single crafted email, no user interaction required, and Copilot followed hidden instructions embedded in the email to pull data out of OneDrive, SharePoint, and Teams (Beam AI, 5 Real AI Agent Security Breaches in 2026). The exploit didn't need a broken permission model. Copilot was, in a narrow technical sense, doing exactly what it was authorized to do — retrieve and summarize data the user could already access. It was the intent behind the action that was compromised, not the authorization. That's the distinction the OWASP GenAI Security Project formalized when it published the Top 10 for Agentic Applications in December 2025, ranking Agent Goal Hijack as ASI01 — an attacker redirecting an agent's objective through content it reads, rather than code it runs, so the agent pursues the attacker's goal while believing it still serves the user's (Cycode, OWASP Top 10 for Agentic Applications 2026). There's an important line between authorization failure and intent failure. Traditional security controls are built to prevent unauthorized actions. Agent security has to address a class of failure traditional AppSec never had to model: an authorized action performed for an unauthorized purpose. A compromised document can convince an agent to delete a record the operating user genuinely has permission to delete. The API correctly authorizes the request. The system is still compromised. Simon Willison's "lethal trifecta" is the cleanest mental model I've seen for when this actually turns dangerous: access to private data, exposure to untrusted content, and the ability to communicate externally. None of the three legs is a vulnerability in isolation. It's the combination that creates the exposure — an attacker slips an instruction into content the agent will process, the agent executes it, private data leaves the perimeter (Getia Consulting, AI Agent Security 2026). This isn't a theoretical framing anymore. Check Point Research documented a single operator combining Claude Code and GPT-4.1 to breach nine Mexican government agencies between late December 2025 and mid-February 2026, converting roughly 1,088 typed prompts into more than 5,300 AI-executed commands and exposing on the order of 400 million records — tax filings, civil registry, patient, vehicle, and electoral data (awesome-ai-agent-attacks, GitHub). That incident wasn't a prompt injection against a victim's agent — it was an attacker using agentic tooling as their own offensive platform — but it's a preview of the asymmetry defenders are up against: the same architecture that makes an agent useful for legitimate multi-step work makes it useful for an attacker's multi-step work too. The supply chain is its own exposure. In March 2026, a malicious package sat live on PyPI for roughly three hours — the compromised LiteLLM release, which serves as the model gateway for CrewAI, DSPy, Microsoft GraphRAG, and a long list of other agent frameworks — during which roughly 47,000 downloads occurred, pulling a backdoored autonomous attack tool in alongside the update (Help Net Security, June 2026). That maps to ASI04 in OWASP's taxonomy — agentic supply chain vulnerabilities — a category that barely existed as a named risk eighteen months ago. This is why identity propagation, least privilege, and audit trails are necessary but not sufficient on their own. Agent security also needs constrained tool semantics, confirmation boundaries on irreversible actions, data provenance the agent can actually answer for, and evaluation against adversarial instructions as a standing practice, not a pre-launch checklist item. Observability as an Accountability Mechanism, Not a Debugging Aid I'd push this argument one step further than the industry currently takes it: an agent's execution trace should be treated as a first-class security artifact, not an engineering convenience. A traditional distributed trace tells you where a request traveled. An agent trace needs to answer a different set of questions — why the system selected a given tool, which identity authorized the action, what data informed the decision, what parameters were supplied, what the tool returned, and whether a human actually approved the resulting write. Gravity's implementation — a visible execution checklist that doubles as a timeline, a reasoning trace per step, full tool-invocation history, and a staged preview of every proposed change before commit — is the same instinct that produced distributed tracing and OpenTelemetry in cloud-native infrastructure a decade ago: a system too complex to reason about from the outside has to narrate its own behavior from the inside, continuously, or nobody downstream trusts it under real load. That reframing — trace as accountability mechanism rather than debugging tool — is, in my view, the more consequential shift, because it's what turns an incident review from forensics into something closer to a routine audit. The Hard Problems That Remain None of the above should read as "solved." The honest list of what's still genuinely unresolved, industry-wide, in mid-2026: Prompt injection, both direct and indirect, remains structurally difficult to fully close — OWASP's own 2026 LLM Security research put the year-over-year surge in injection attempts at 340% (AI Magicx, April 2026).Memory poisoning — a persisted context corrupted so a later, unrelated task inherits false assumptions — is now its own OWASP category (ASI06) and doesn't map cleanly onto any pre-LLM threat model.Authorized-but-unintended actions, the EchoLeak pattern, aren't fixed by permission inheritance alone.Long-running task failures and context drift over multi-day agent sessions are still mostly evaluated in demos, not adversarial production conditions.Skill and harness regressions can silently degrade a capability customers were actively relying on, which is why versioning and rollback matter as much for skills as for any other production code.Evaluation at scale — proving an agent's judgment holds up across the long tail of messy, real scenarios rather than a fixed benchmark set — remains closer to an open research problem than a solved engineering one. The goal with each of these isn't to eliminate the risk outright. It's to make it observable, bounded, testable, and recoverable — which is a materially different, more honest bar than "safe." My Framework for Evaluating Enterprise Agents After going through Gravity's architecture in this level of detail and cross-referencing it against the broader 2026 threat and adoption data, I'd reduce production readiness to seven questions: Agency – Can the system independently execute multi-step objectives, not just suggest them?Compositionality – Can it combine capabilities it wasn't explicitly wired into a fixed workflow to perform?Execution – Does every action pass through the same validated business logic a human user would hit?Identity – Can every action be attributed to a specific principal, recalculated per call rather than cached?Determinism – Are correctness-critical operations owned by a deterministic system, not a model's best guess?Governance – Can risky mutations be previewed, validated, approved, and audited before they commit?Evaluation – Can the system demonstrate its behavior stays reliable as models, skills, and scenarios keep shifting underneath it? If an agent fails several of these, improving the model is very rarely the first engineering problem worth solving. Where This Is Headed Kornish's predictions for the next three to five years weren't flashy, which is exactly why I'd take them seriously. Scope stays the whole story — agents confined to what they were demoed on keep losing ground to agents that can operate across an entire platform, the same pattern that played out when narrow point solutions gave way to platforms in every other software category. Vendors without an API-first foundation fall further behind every year, because that kind of scope isn't something you retrofit after the fact. Gartner's own projection points in the same direction: 40% of enterprise applications are expected to embed task-specific AI agents by the end of 2026, up from under 5% in 2025 (Paul Okhrem, Enterprise AI Agents Adoption Statistics 2026). And observability finishes its transition from differentiator to baseline expectation, tracing the same arc logging and monitoring took in cloud infrastructure roughly a decade ago. The industry's conversation about enterprise AI has spent the last few years almost entirely on model capability. The next phase is being decided somewhere else — in the API surface, the identity model, the deterministic core, and the governance wrapped around every write. That's not a glamorous place for the conversation to move. It's also the only place it was ever going to survive contact with a regulator, an auditor, or an attacker. I conducted this interview with Ted Kornish, CTO of Gravity, specifically to understand how these architectural questions are being resolved inside a production system operating in a regulated domain. His platform is the case study here; the analysis and framework above are my own.
A simple access check uncovered something alarming: several dashboards still showed employee compensation based on an organizational hierarchy that was no longer relevant. Our row-level security framework stopped synchronizing because of a Workday HCM API timeout issue, but meanwhile, the entitlements on those dashboards did not get adjusted to reflect the new organization setup. No permission changes were made. Despite changes in the hierarchy, the access layer was unable to adapt to the changes. This is the reason why this project became necessary. First of all, we completely redesigned the access layer that sits behind all BI tools, Adaptive Planning models, and Snowflake shares used by our FP&A and risk departments. It was done not using any static table with roles, but with the help of tracking constantly changing hierarchies of organizations, cost centers, legal entities, and products that include deal closures and divisions' separations. Let me explain how it was accomplished: first, I will describe the process of keeping hierarchies up-to-date in the ingestion layer; then I will explain the visibility engine and its policies of providing access at a row level. Finally, I will show how to keep access scopes the same in Power BI, Tableau, and Adaptive Planning. Overview The system obtains hierarchical information from Workday HCM, Salesforce, and the internal table of the legal entities in order to reconcile these three conflicting lists into a single tagged entity graph. Visibility between users and the entities is calculated upon any change of the hierarchy edges, and not after a periodic re-access review as in previous attempts. The visibility enforcement occurs on the Snowflake row level, guaranteeing that Power BI, Tableau, and custom SQL queries obtain the same limited results set, which helps avoid discrepancies because of individual permission tables per tool. Consistent entity scope is provided for the Workday Adaptive Planning security groups; as a result, there will be no mismatches between allowed viewers of the planning sheets and the dashboards. Orphan nodes in the hierarchy are identified before their contribution to visibility gaps appears. Components The framework is made up of four main parts: hierarchy ingestion layer, dynamic security mapping engine, Snowflake row-level access control, and synchronization layer that relays outcomes to BI and planning software applications. These parts produced practical insights during difficult experiential learning. Hierarchy Ingestion Layer Four sources have been used for this hierarchical workflow. They include Workday HCM organizational hierarchy and cost centers hierarchy, location level hierarchy that groups individual branches/plants/facilities according to regions, account hierarchy of Salesforce accessed using the Bulk API and a legal entities reference table maintained manually in Snowflake. The four hierarchical sources have been transformed into a single closure table having fields as entity_id, parent_id, hierarchy_type, and effective_date. While one of the first considerations was to use only the organizational hierarchy in Workday HCM, the reason was mainly because of its completeness. But it is simply impossible to rely on just this hierarchy because there could be instances where a single cost center reports itself to two legal entities due to allocation of services. Additionally, the location-level hierarchy of any branch does not have anything to do with the organizational hierarchy position of its manager – it could be a branch within one region reporting to a manager in another region. So, maintaining each hierarchy separately and ensuring that configurations are retained helps. One of the difficult aspects of this entire process was the fact that a complete extract of the Workday HCM hierarchy was extremely slow, such that it caused issues with the synchronization to adaptive planning. To avoid this problem, it was necessary to implement change detection instead. Dynamic Security Mapping Engine This process goes through the closure table starting at each user’s home node and translates all the descendant entity_ids into a flatter structure of USER_ENTITY_ACCESS, along with the effective dates. Key to the design is the fact that a user’s home node is not a simple identifier but a collection of (user, hierarchy_type, home_node). Thus, the same user can have a User + Location Hierarchy as well as a User + Organization Hierarchy, each handled in the same fashion. Access was previously managed through manually maintained grants in Snowflake for each business unit. That worked fine until some organizational change would occur, at which time many grants had to be updated. Moving to the recursive resolution from a single source-of-truth hierarchy allowed this to become unnecessary; there is no grant to maintain when a cost center moves. We found that the effect of one edge change could be more far-reaching than expected. As an example, when a cost center moved to a different regional vice president, the map engine would do a traversal of the closure table starting at this node and then down the whole chain and then write out all the affected USER_ENTITY_ACCESS entries for anyone who mapped back to this node. In one typical move, over two thousand downstream entries in the USER_ENTITY_ACCESS table were affected by changing just one edge in the hierarchy. To solve this problem, the original map engine did a full recalculation of all the accesses in the table for each pass through the process. This was a brute force solution but one that put enough of a strain on the system that it locked up the table in business hours. The second map engine solves this issue by updating only the affected subtree. Foe example, one engine solves any hierarchy, not just one. Think about a regional operations manager. Their User + Location Hierarchy assignment specifies the regional level home node, meaning that all locations below that region within the location hierarchy become part of their assignment – all branches and facilities belonging to that region. The User + Org Hierarchy assignment is a distinct and separate home node, further down the chain: exactly their own team and subordinates within it because their responsibility is not over all employees within that region but those reporting to them directly. Both assignments live in the same USER_ROLE_ASSIGNMENT table, distinguished only by hierarchy_type. They are both solved by the very same recursive procedure. Start from the home node, go over all descendants in that hierarchy chain within the closure table, and write into the USER_ENTITY_ACCESS table. This technique also allows modeling of hierarchies that do not exist yet – say, Product Line hierarchy – because we simply add another hierarchy type to the closure table, specify a home node for it, and the same engine will recognize it on next runs. Snowflake Row-Access Enforcement In Snowflake, a policy is added on the fact tables: GL detail, planning actuals, and HR costs, joining them against USER_ENTITY_ACCESS by the current session’s user ID mapped into the tenant. Hence, irrespective of who queries the data using what software tool, the result set will be automatically filtered for authorized rows only. Another option considered included creating a secure view per each business unit, running into the centralized control plane problem described by GFT's Azure Synapse Analytics – New Insights Into Data Security on DZone. The view-per-unit solution would not scale above dozens of units and required having a different Power BI dataset for each view. Adding a single row-access policy allowed setting up proper restrictions at the table level without making any changes in consumer applications. The design principle used for access restriction at runtime based on an explicit row filter by identity context and not through application-layer WHERE clauses is not exclusive to Snowflake. Another example of implementing similar logic in PostgreSQL by means of its native row-level security and session-wide tenant identifier can be found in Multi-Tenant Data Isolation and Row Level Security on DZone. Even though the approach differs from a closure table self-join, the idea behind it is the same: enforce data security in the database itself, not in the application code of consumers. A technical problem emerged with the initial policy function that used entity codes. Once Finance renamed a number of those, some of the codes were retained in cached data extracts and silently caused access restrictions due to missing codes. Therefore, a surrogate keys level was implemented to avoid invalidating already computed access rights after renames. For instance, the process illustrates that a certain policy works consistently throughout all tiers of roles: simply ask the fact table by means of an arbitrary user in the corresponding role tier and compare the entity count with the scope for that particular tier. Cost center managers, for example, would not have any access beyond their own cost center, which includes only the subordinates of that cost center; access beyond those two scopes would be considered a flaw in the policy, and not a change in the business itself, unless proven otherwise. BI and Planning Sync Layer The sync process takes USER_ENTITY_ACCESS and passes it to three recipients: Power BI Row-Level Security (RLS) role membership, Tableau user filters, and security groups of Workday Adaptive Planning. This process guarantees the matching of the access control scope between a planning sheet and a dashboard built using the very same data. In the past, each BI team kept the maintenance of its RLS roles separate. Thus, there was an inherent possibility of the situation described in the introduction of this document, when Power BI roles were delayed compared to the Snowflake policy, causing the misalignment period and the related risks. The centralization of all pushes into one mapping table reduced the likelihood of such an incident. One issue came up during the development phase of the project because of the need for trial-and-error solutions. Namely, the Adaptive Planning API sets the rate limit for security group updates, making the mass updates impracticable. The successful implementation of the batch update method, where two hundred users could be updated in one request, became evident after trying the five hundred per request solution and failing because of the throttle errors. Prerequisites Read access to Workday HCM reports' web service, restricted by organization and using a special service account.Profile-level access to Salesforce bulk API that queries account hierarchy fields.Snowflake database edition enabling row access policies, along with a role capable of creating and attaching them.Credentials for Workday Adaptive Planning Integration API, with write access to security groups.Python version 3.10 or later, the snowflake-connector-python library, and job orchestration, where we used a scheduled container, but any other scheduler could have done the job.A concise list of hierarchy types required for the job. We had wasted much time dealing with hierarchy types nobody used further downstream. Overall, the flow is straightforward and linear – hierarchy sources are responsible for populating the closure table, which gets flattened by the mapping engine to produce the access table, followed by enforcing it through row access policy and expanding the scope through the sync layer. Figure 5 illustrates the entire process briefly. Design issues that continually arise in the context of such modeling include where each hierarchy comes from, and how an organizational level maps to a set of defined entities. These are two questions that can better be addressed by example. For instance, choosing a hierarchy graph vs. choosing a tree graph: The organizational level hierarchy describes supervisory reporting lines within Workday HCM, in which an individual contributor reports to a manager, who in turn reports to a director, and so on, independent of their actual geographical location. The cost-center hierarchy can be maintained within Workday HCM but follows a financial reporting line instead of the personnel management line, thus allowing for a cost center to be assigned to two legal entities if different allocations need to be created by shared services. There is yet another hierarchy, which is called the location level hierarchy, according to which each individual branch, plant, or facility consolidates into a region and eventually into a business unit, although often a branch can be geographically located in one region while being organizationally assigned to a completely different region via a regional manager. Finally, there is a legal entity hierarchy kept by Corporate Finance that describes corporate regulations and taxes, and not reporting lines. As mentioned before, there is also the account hierarchy coming from Salesforce, which comes from how RevOps divides accounts during the closure process. Each node in the closure table is marked with its domain membership rather than assuming the same hierarchy for all domains. It is precisely the latter that does not work when a branch/cost center/account needs to have different rollups depending on its hierarchy type. The following example illustrates how role tier relates to scope. The scope of the executive role is restricted to the entire organization and multiple tiers down within the organization. This allows for visibility over thousands of entities. On the other hand, the regional vice president's role allows for a scope restriction to a particular region at the location level as well as the entire organizational hierarchy below the individuals within that region. Hence, this leads to a large scope but still within limits, and one which is not the entire organization but rather a subset thereof. Similarly, the cost center manager's role limits scope to the cost center he is in as well as any individuals reporting directly to him. In turn, the analyst's role has a scope that is limited only to certain cost centers that have been defined in advance and does not inherit scope from any of the two hierarchies. Troubleshooting / Lessons Learned The closure table going stale without anyone noticing This document explains the failure mode created at the beginning of the document: The API timeout made the closure table go out of date, and none of the components downstream had any way to check whether the data they were using was up to date before they started using it. To solve this problem, a mechanism was built to prevent synchronization after the closure table has aged past a certain point to alert the operation staff that outdated entitlements would not be served without notifying anyone first. Any aging at all in the closure table may create a picture of an organization that does not exist anymore due to a quick reorganization. Orphaned nodes from in-flight Salesforce account merges Merges may create the problem whereby the child is tied to an expired parent ID during the final propagation. During a normal run, this type of problem could involve from a few to several tens of entries, based on the level of mergers made prior to the cycle run. This type of problem creates the risk of attribution of wrong parents; thus, all the records involving this problem are isolated in a reviewing table. The full-rebuild job locking the access table during business hours In the first approach, the whole access table would be recomputed each time, which is very effective but leaves the USER_ENTITY_ACCESS locked for long enough to queue the BI refreshes after it. Since in the second approach recomputation occurs only in the subtree concerned—as shown by the cost center example—locks become almost negligible, even in cases of thousands of changes due to reorganization. Adaptive Planning throttling the security group push Individually pushing updates to the security groups became unsustainable due to the large number involved. This is as previously discussed, where a relatively small-sized batch was successful while significantly larger ones experienced errors of being throttled by the Integration API. Conclusion This article will describe the design of the dynamic multi-hierarchy security model that is based on Workday HCM, Salesforce, Snowflake, Power BI, Tableau, and Workday Adaptive Planning. This methodology allows one to reconcile the hierarchical data coming from four different sources, such as the organization hierarchy, cost center hierarchy, location hierarchy, and legal entity hierarchy, in addition to the account hierarchy coming from Salesforce, into a tagged graph rather than trees, which can be inconsistent. Moreover, visibility recomputation is achieved due to any actual change within these hierarchies and not depending on periodic reviews as in most cases. It will also be proven that having a single permission table per Snowflake row rather than permission tables for each BI tool helps reduce the visibility gap, which caused the initiation of this project.
Stolen credentials served as the entry point in 22% of breaches last year, and in attacks on basic web applications that figure climbs to 88%. Those numbers describe a password problem, and databases sit at the end of nearly every attack path. A username and password prove nothing about the machine presenting them. Mutual TLS closes that gap by requiring both sides of a connection to present certificates and prove who they are before a single query runs. Securing production database connections has convinced me that enterprises implement mutual TLS using readily available tools and established certificate management practices. What Certificate-Based Authentication Actually Closes Off A password travels, gets shared, gets phished, and gets left behind in a script. A certificate bound to a specific client does none of those things easily, which is why mutual authentication blunts three familiar attack patterns: stolen credentials replayed from an unfamiliar host, spoofed clients impersonating an application server, and lateral movement after an attacker gains an initial foothold. Enterprises usually maintain password rotation policies that are triggered after a set period or upon an employee's exit from the team. Certificate-based authentication safeguards the data in case a team misses rotating those credentials, because the password alone no longer grants entry. I ran into this while setting up an open-source alerting tool, where the password had to live in a config file or a session variable. The session variable fails when the tool auto-restarts during maintenance, leaving a hardcoded password or a decryption utility to mask it. A certificate addresses that problem because its lifetime can be governed by the organization's security policies. Once the certificate expires, a password alone is no longer sufficient to authenticate the client. Machine identities now outnumber human identities by more than 80 to 1 in the average organization, and each database connection string is one of them. The Rollout Decisions That Matter Most An mTLS program stands on three design choices. The first is the certificate authority, where an internal CA gives the team full control over issuance and revocation for database traffic that never leaves the estate. The second is cipher selection, which deserves more attention than it gets, because Oracle, MySQL, and MongoDB each negotiate TLS differently, and a cipher suite that works on one engine can fail the handshake on another. The third is rotation, and this is where programs die quietly. 81% of organizations have suffered at least two outages caused by expired certificates in a two-year window. Six months to a year is an ideal certificate lifetime across a polyglot estate, though the organization's security baselines govern, and estates handling critical PII or PCI data, or carrying past breach attempts, can justify a reduced lifetime. Renewing very frequently creates its own outages, because some systems require a reboot to bring new certificates into effect, a real challenge for heavily used 24/7 applications without a high availability solution ready. Keeping lifetimes consistent across Oracle, MongoDB, MySQL, and PostgreSQL helps manage the rotations, but databases are not all created in a single day, so expiry timelines differ and an inventory or dashboard tracking every expiry becomes essential. Above all, automating renewal wherever possible reduces the risk of downtime from an expired certificate. Securing the Monitoring Layer Itself Monitoring an encrypted estate raises a question teams often skip, which is how to keep the monitoring path from becoming the weak point. Many organizations use Prometheus to collect database metrics, with more than two-thirds of organizations running it in production, yet exporters may be deployed with unencrypted scrape endpoints if they are not configured to use TLS. One common deployment approach is to run the database exporter process on the database server itself. In Prometheus-based environments, configuring both the client connection (--config.my-cnf) and the exporter (--web.config.file) to use client certificates allows the monitoring pipeline to follow the same mutual authentication model as the database it monitors. This helps ensure that metrics are collected over authenticated, encrypted connections rather than introducing a weaker path into the environment. Choosing the Right Approach Organizations can implement mutual TLS using either commercial certificate management platforms or open-source tooling. The right approach depends on factors such as certificate volume, compliance requirements, auditing needs, and the operational resources available to manage the environment. Early in my career, I assumed enterprise-licensed products were the default choice for every deployment. Over time, I found that decision is more nuanced. Large environments managing tens of thousands of certificates may benefit from centralized lifecycle management, auditing, and governance features, while many organizations can successfully implement mTLS using open-source tools that meet their operational requirements. The priority should be selecting an approach that supports reliable certificate issuance, rotation, and revocation while integrating with existing security processes. Regardless of the tooling, a well-managed certificate lifecycle is what ultimately strengthens database authentication and reduces operational risk.
The rise of autonomous AI agents within business software demands a fresh approach to security. Unlike earlier chatbot tools, modern agents act with real privileges, such as updating databases, calling microservices, composing and even executing code, or triggering workflows on their own. This shift expands the blast radius of any flaw or compromise. As one Microsoft analysis observes, today’s AI agents “can update database records, trigger enterprise workflows, access sensitive data, and interact with production systems all autonomously.” In practice, that means a mistake or exploit can have immediate operational impact instead of just a reputational cost. With agents in the loop, input manipulation becomes especially dangerous. Prompt-injection attacks let adversaries commandeer an AI by feeding it malicious instructions in user inputs or hidden in external data. A carefully crafted prompt or document can cause an agent to reveal secrets or perform harmful actions. These manipulations can be direct (an attacker’s text overriding the agent’s instructions) or indirect (for example, hidden commands embedded in HTML or metadata that the agent ingests). By definition, even inputs imperceptible to humans can subvert the model, forcing it to break safety rules. In effect, prompt injection can trick an AI into disclosing internal prompts, executing arbitrary commands, or making unauthorized changes. Protect AI Agents With Layered Security Controls Defending against prompt injection requires layered controls. It is not enough to trust the LLM’s built-in safeguards. The application must sanitize and constrain every input. For example, the OWASP GenAI guidelines recommend semantic filtering of user inputs and strict output validation. In practice, this often means cleaning or escaping suspicious tokens in the prompt, enforcing clear response schemas, and even tagging or quarantining untrusted data before it reaches the model. Developers should also build resilience into AI calls, for example by wrapping each agent invocation in a circuit-breaker or retry mechanism so that anomalous behavior triggers a safe fallback rather than a cascade of errors. Java @CircuitBreaker(name="agentService", fallbackMethod="fallbackAgent") @Retry(name="agentService", maxAttempts=3, backoff=@Backoff(delay=200)) public String executeAgentTask(String taskId, String input) { String safeInput = inputFilter.sanitize(input); return agentClient.postForObject("/tasks/" + taskId, safeInput, String.class); } private String fallbackAgent(String taskId, String input, Exception ex) { log.error("Agent {} failed: {}", taskId, ex.getMessage()); return "error"; } In this example, inputFilter.sanitize strips any suspicious content from the prompt, and the circuit-breaker ensures repeated failures lead to a controlled fallback. The fallbackAgent method logs the failure and returns a safe default response, preventing a hijacked prompt from causing uncontrolled retries or side effects. Embedding such patterns helps contain injected instructions and makes anomalies visible for audit. Agents also expand supply-chain and data-poisoning attack surfaces. AI applications often depend on third-party models, libraries, or datasets, each of which could harbor backdoors. In a real incident, attackers compromised an open-source Python package used in a model’s pipeline, effectively inserting malicious logic into every system that imported it. To guard against this, organizations must treat AI dependencies as critically as any library or service. Models and data should come from verifiable, signed sources, and teams should maintain an AI-focused Software Bill of Materials (SBOM) tracking each model and dataset. Regular scans of model files and packages (for example, by verifying cryptographic hashes or digital signatures) can detect tampering before models reach production. Another insidious vector is agent memory poisoning. Unlike stateless microservices, AI agents may accumulate knowledge across sessions or tasks. If an adversary can insert malicious “memories” or biased information into that knowledge base, the agent may repeat or amplify harmful logic over time. Researchers have shown that injecting only a few hundred carefully crafted documents into a training or retrieval database can reliably hijack a model’s outputs in specific domains. In an enterprise, this might translate to a support chatbot that starts rejecting valid requests or approving fraudulent transactions because its knowledge was skewed. Mitigations include thoroughly vetting any external data fed to the agent, cross-checking facts against trusted sources, and periodically resetting or auditing the agent’s internal state. For example, a system could clear an agent’s “short-term memory” after each sensitive transaction, or require digital signatures on any new knowledge items. Protect AI Agents With Layered Security Controls Identity and access control for agents is equally critical. Agents act as non-human service identities, so if an attacker steals an agent’s credentials, they essentially hijack its privileges. Recorded Future warns that “compromised credentials, SSO platforms, or agent identities could enable large-scale... data exfiltration”. In practice, a stolen token could let an attacker quietly siphon data or trigger commands anywhere the agent has access. To counter this, enterprises should issue each agent a unique short-lived token and restrict its scope strictly. Java String token = credentialService.issueShortLivedToken(agentId); apiClient.setAuthToken(token); apiClient.callExternalService(requestPayload); Here, each API call by the agent uses a fresh, scoped token. If the token is leaked or abused, its very short life and limited permissions contain the damage. In practice, agent tokens should be rotated frequently, and every action should be logged under the agent’s identity. If an agent suddenly tries to access an unexpected endpoint, automated policies should block or flag the request. In essence, treat agents like privileged users with their own IAM lifecycle by implementing least-privilege roles, multi-factor approvals for high-value operations, and full auditing of their activities. Multi-agent workflows introduce additional complexity. Agents often invoke other tools or orchestrate chains of sub-agents. In such pipelines, a compromise anywhere can cascade. For example, if Agent A trusts a data input or command from Agent B, and B has been misled or maliciously tampered with, A may unknowingly act on bad instructions. To mitigate this, every handoff between agents or tools should be authenticated and checked. Enforce endpoint authentication and message signing on each channel between agents, and apply authorization checks at every step. Segmentation and strong encryption on inter-agent communications can prevent a breach in one component from jumping to others. Monitor AI Agents for Anomalies and Unauthorized Actions At runtime, anomaly detection and monitoring provide a final safety net. Agents in production should exhibit well-defined baselines of behavior. An agent that usually looks up customer records, for instance, should not suddenly be streaming large volumes of payroll data. Security telemetry that logs every prompt, response, and tool invocation lets defenders spot when an agent deviates from its norm. Modern SIEM and AIOps platforms can ingest these logs and flag unusual patterns (for example, spikes in outbound data or unexpected API calls). By correlating agent activity with traditional logs and threat intelligence, teams can detect and contain a misbehaving agent before it causes systemic damage. In summary, securing enterprise applications in the agent era means integrating AI-specific defenses throughout the stack. Zero-Trust principles apply fully where we treat each agent call as untrusted until verified, grant agents only minimal permissions, and require human approval for any high-impact decision. Defense-in-depth remains essential as it sanitizes every input, isolates AI subsystems from sensitive resources, and monitors all outputs continuously. Industry standards are beginning to catch up. For example, NIST’s new AI Risk Management Framework and the Cloud Security Alliance’s guidelines explicitly recommend continuous threat modeling, red teaming of AI, and traceability for data and models. Ultimately, the agentic AI era raises the security stakes from theory into daily practice. Organizations that build AI-aware threat modeling, least-privilege IAM, prompt filtering, and anomaly monitoring into their DevSecOps pipelines will be best equipped to embrace AI agents safely. By doing the hard work now by integrating model security into the software lifecycle, enterprises can unlock the productivity of agents while keeping adversaries at bay.
If you let users publish something, such as a page, prototype, or dashboard, sooner or later you want an "embed this" button so they can drop it into a blog, a portfolio, or docs, the way a CodePen result embeds. Then you ship the iframe, and it renders a blank box: refused to connect. The reflex is to blame the iframe. It's almost never the iframe. It's a response header. The Two Headers That Decide Whether You Can Be Framed There are two mechanisms, and they are not equivalent: X-Frame-Options is the legacy control. It has three meaningful states: DENY, SAMEORIGIN, and the deprecated, widely-ignored ALLOW-FROM. Crucially, there is no value that means "allow any origin" or "allow this list of origins." It is deny / same-origin / nothing-useful. If your edge returns X-Frame-Options: SAMEORIGIN, a third-party site can never frame you, full stop.CSP frame-ancestors is the modern replacement. It is part of Content-Security-Policy and takes a real source list: frame-ancestors 'none', 'self', https://example.com, or *. It is granular where X-Frame-Options is binary. The catch that trips people up: if you send both, X-Frame-Options is still honored by many browsers and will block framing regardless of how permissive your frame-ancestors is. So to actually be embeddable by third parties, you have to remove X-Frame-Options, not just add a permissive frame-ancestors next to it. The Footgun: One Global Security-Headers Middleware Here is the trap. The application that rendered our published sites already made the right call in code: it disabled frameguard and emitted a permissive frame-ancestors. And yet every embed was blank. The header was not coming from the app. It was re-added at the edge. A single shared "secure-headers" middleware, the kind every reverse proxy ships and every security checklist tells you to apply globally - included X-Frame-Options: SAMEORIGIN in its response headers. The proxy ran that middleware on the router that served published user sites, stamping SAMEORIGIN on top of the app's deliberate "please frame me" headers. The edge won. State it plainly: applying one blanket security-headers policy to every route is a footgun the moment one of those routes is supposed to serve embeddable content. That middleware is correct for your API and your authenticated app. It is wrong for the one route whose entire job is to be put inside someone else's <iframe>. The Fix: Scope Headers Per Trust Zone The fix is not "turn off security headers." It is to stop treating every route as one trust zone: Authenticated and sensitive routes (/api, realtime/WebSocket, the editor app) keep the full secure-headers set, including X-Frame-Options: SAMEORIGIN. Those should never be framed; clickjacking protection stays.The route that serves published, public, client-only user pages gets a near-identical header set - same X-Content-Type-Options, Referrer-Policy, Strict-Transport-Security - but without X-Frame-Options. Whether such a page can be framed is then governed by the frame-ancestors the page itself serves. In practice, that is a second middleware that is a copy of the first minus one header, pointed only at the published-pages router. Surgical. Nothing else loses protection. YAML secure-headers: # sensitive routes - keeps clickjacking protection headers: customResponseHeaders: X-Frame-Options: "SAMEORIGIN" contentTypeNosniff: true referrerPolicy: "strict-origin-when-cross-origin" stsSeconds: 31536000 pages-headers: # same set, minus X-Frame-Options - embeddable pages only headers: contentTypeNosniff: true referrerPolicy: "strict-origin-when-cross-origin" stsSeconds: 31536000 Then the page that is meant to be embeddable expresses its own policy: YAML Content-Security-Policy: frame-ancestors *; (or a specific allowlist, if only certain hosts should embed it). Embedding User-Generated Content Safely "Make it embeddable" and "make it safe" have to hold at the same time, because you are putting code you did not write into a frame. A few rules that travel well: Isolate every project on its own origin. Serve each published site from its own subdomain ({slug}.example.io), never a shared path. Origin isolation means one project's script cannot reach another's storage, cookies, or DOM. This is the single biggest lever.Sandbox the frame. The embedding side should use <iframe sandbox="allow-scripts allow-popups ..."> and grant only the capabilities the content needs. Omit allow-same-origin where you can, so the framed document runs with an opaque origin.Let the page opt out. A published page should be able to override the edge default and refuse framing - its own X-Frame-Options / frame-ancestors should win over the proxy default. Author intent beats infrastructure default.Keep authenticated surfaces un-framable. The embeddable posture applies to public content only. Anything behind a login keeps SAMEORIGIN. This is the posture we landed on at Playcode, an AI website and app builder: published projects each live on their own origin, the published-pages route drops X-Frame-Options so a one-line embed drops a live project into any blog or docs page, while the editor, API, and Playcode Cloud backend keep full clickjacking protection. A static published page carries the same minimal framing risk that previews and custom domains already had. The difference is that it is now a deliberate, scoped decision instead of an inconsistent accident across routes. Takeaways A blank "refused to connect" embed is almost always X-Frame-Options, not your iframe.X-Frame-Options cannot express "allow these origins" - use CSP frame-ancestors for anything granular, and drop X-Frame-Options entirely on routes that must be embeddable.Do not apply one global security-headers middleware to routes that serve embeddable content; scope headers per trust zone.Embeddability and safety coexist through origin isolation, the iframe sandbox attribute, and letting the page author's policy win over the edge default.
Let me describe a workflow that exists in thousands of engineering organizations right now. Somebody sets up a cron job. It runs terraform plan against production every few hours. When the plan output isn't empty, it fires a Slack notification. The team calls this "drift detection." For about two weeks, it works. Engineers look at every alert, investigate changes, and fix things. Then the noise starts. Auto-scaling groups change desired_capacity. It's not drift; that's the system doing its job. Someone updated a tag through the cost allocation tool. An external script modified a description field. The load balancer's idle timeout was changed by an automation nobody remembers writing. Within a month, the Slack channel is muted. Within two months, the cron job is either disabled or silently ignored. And that's when someone modifies a security group through the AWS console "temporarily" and forgets to revert it. I've seen this pattern at every organization I've worked at. The problem isn't that drift detection doesn't work. It works well. It finds everything, tells you nothing about what matters and what is actually important, and eventually drowns in its own noise. The Signal-to-Noise Problem Here's the main issue with terraform plan as a drift detection mechanism. Something changed, or it didn't. There's no concept of severity, no notion of risk, no way to distinguish between a tag modification and an exposed database. We cannot tell from the change how much of a risk that is. Consider two drift events: Event A: aws_s3_bucket.logs the tags.Environment attribute changed from "production" to "prod"Event B:aws_security_group.api_gateway — the inbound rule now includes a rule allowing port 22 from 0.0.0.0/0 Terraform plan presents both as equivalent changes. But Event A is a cosmetic inconsistency that has zero operational impact. Event B is an active security vulnerability that could be the first step in a breach. When you're getting 40 alerts a day and most of them look like Event A, how long does it take before you stop carefully examining each one? Studies on alert fatigue show that when engineers are flooded with too many alerts, it becomes harder to respond effectively. As a result, critical issues can be overlooked along with less important alerts. Monitoring tools addressed this problem years ago by prioritizing alerts based on severity and sending them to the right teams. Infrastructure drift detection has not yet adopted these practices. Thinking in Severity Tiers The solution isn't to stop detecting drift. It's to classify it. When I started building a drift detection tool for my own use, severity classification was the feature I cared about most. After iterating on several models, I landed on four tiers: Critical: Changes that directly affect security boundaries. If someone modified a security group's ingress rules, an IAM policy, a KMS key policy, or an S3 public access configuration, I want to know about it right now. High: Changes that affect compute capacity, data persistence, or encryption. An instance type change in production means your capacity planning is wrong. A database with publicly_accessible flipped to true is a problem waiting to happen. An encryption setting change needs investigation.Medium: the default bucket for attribute changes that don't match any explicit rule. Worth knowing about, not worth getting paged for.Low: Tags, descriptions, labels. The metadata that external systems modify constantly and that nobody needs to be alerted about. At first, I tried using three severity levels. However, that was too simple because it did not clearly separate different types of serious issues. For example, changing an IAM policy could create a security risk, while changing an instance type could cause performance or capacity problems. Both are important, but they have different impacts. I also tried using five severity levels, but that was too detailed. It became difficult to consistently decide which level an issue belonged to, especially when the differences between levels were small. Attribute-Level Classification The key insight is that severity depends on which attribute changed, not just which resource type changed. An aws_security_group resource changing its tags is low severity. The same resource changing its ingress rules is critical. Classifying by resource type alone would make all security group changes critical, which defeats the purpose. You'd still get noise from tag modifications. The classification engine I built uses pattern matching rules that match against the resource type and attribute combination. For example: aws_security_group..ingress maps to critical, aws_security_group..tags maps to low, aws_iam_policy..policy maps to critical, aws_instance..instance_type maps to high, and any *.tags pattern maps to low. When a resource has multiple changed attributes at different severity levels, the maximum applies. A security group with both a tag change (low) and an ingress change (critical) gets reported as critical. This prevents the scenario where someone dismisses a critical alert because it's attached to what looks like a mostly-harmless tag update. I chose fnmatch glob patterns over regular expressions deliberately. The people editing these rules are operations engineers responding to incidents at 2 AM, not writing parsers. A pattern like aws_security_group.*.ingress is instantly readable. The Numbers I tested this approach across 150+ Terraform workspaces managing 847 AWS resources. I introduced 62 drift events across four categories: security-relevant changes (security group and IAM modifications), operational changes (instance types, database configs), metadata changes (tags, descriptions), and expected changes (auto-scaling adjustments). With binary detection (standard terraform plan), all 62 drift events were flagged as 100% of changes, with security-relevant ones buried in noise. Filtering to High and Critical severity only reduced the alert count to 17 (27% of total) while still catching 7 of 8 security-relevant changes 94% security coverage. Adding ignore rules for expected drift like autoscaling reduced it further to just 12 alerts (19% of total) at the same 94% security coverage. That's a 73% reduction in alert volume while retaining 94% of security-relevant changes. The severity classification also performed well against manual expert review. Two engineers independently labeled all 62 events. Agreement rates with automated classification: critical 96%, high 91%, medium 88%, low 95%. The Ignore Layer Beyond severity classification, there's a category of drift that shouldn't be classified at all it should be filtered out entirely. Auto-scaling groups change desired_capacity every few minutes. That's not drift. That's the autoscaler doing exactly what it's supposed to do. ECS services change desired_count for the same reason. Tag attributes like LastModified get updated by external tools constantly. An ignore file (similar in concept to .gitignore) handles this. You list patterns like aws_autoscaling_group..desired_capacity and aws_ecs_service..desired_count, and those changes are filtered out before classification, removing an entire class of noise without any risk to security coverage. Configuration as Institutional Knowledge Here's something I didn't anticipate when I started building this: the severity configuration file becomes a living document of your organization's security values. When you mark a rule like aws_rds_instance.*.storage_encrypted as critical, you are defining what is important for your environment. When you add a new pattern after an incident, you are documenting a lesson learned. Over time, this knowledge is stored in a version-controlled YAML file instead of relying on team members to remember it. So when a new engineer asks, "Do we care about CloudFront origin changes?", they can find the answer directly in the configuration. That incident comment in the config file is institutional knowledge being captured and enforced, not just documented. Cross-Cloud Applicability The pattern-based approach works across cloud providers. For Azure, patterns like azurerm_network_security_group..security_rule, azurerm_role_assignment. and azurerm_key_vault_access_policy.* map to critical, while azurerm_virtual_machine.*.vm_size maps to high. For GCP, patterns like google_compute_firewall..allow, google_compute_firewall..source_ranges, and google_project_iam_binding.* map to critical, while google_compute_instance.*.machine_type maps to high. The severity tiers are universal. The patterns are provider-specific. A well-maintained rule set should cover the top 20-30 most security-sensitive resource types and attributes for each cloud provider you use. From Detection to Governance Severity classification opens the door to something more powerful than alerting: governance. Once drift has a severity score, you can build policies around it. In CI/CD, you can fail the deployment pipeline if Critical drift exists in the target environment. For escalation routing, you can send critical drift to PagerDuty, high to Slack, and log medium/low silently for weekly review. For auto-remediation, you can automatically run terraform apply for low-severity drift like tag corrections but require human approval for anything high or above. For compliance, you can generate weekly reports showing drift by severity for security review. Getting Started If you want to try this approach, tfdrift is the open-source tool I built implementing everything described in this article. Install it with pip install tfdrift, then run tfdrift scan --path ./your-terraform-dir to scan your infrastructure. Run tfdrift init to generate a starter configuration file. It ships with 60+ built-in severity rules for AWS, Azure, and GCP, all configurable via YAML. It supports Slack and PagerDuty notifications, JSON/Markdown/HTML output, auto-remediation with safety guards, and OpenTofu via a --binary flag. But the specific tool matters less than the approach. The core idea — classifying drift by security impact and routing alerts accordingly — is implementable with any combination of terraform plan, a JSON parser, and a pattern matcher. Key Takeaways Binary drift detection creates alert fatigue. When all changes are treated equally, teams stop checking, and that's when security-critical changes get missed. Four severity tiers hit the right granularity. Critical for security boundaries, high for compute and encryption, medium for other changes, and low for metadata. Three is too coarse, five is too hard to distinguish consistently. Classify by attribute, not just resource type. A security group changing tags is low, but the same resource changing ingress rules is critical. Attribute-level classification is what makes severity useful. Severity filtering reduces alert volume by 73% while maintaining 94% security coverage based on evaluation across 150+ Terraform workspaces. The severity config becomes institutional knowledge. Your configuration file is a version-controlled, reviewable record of what your organization considers security-critical infrastructure changes.
Most engineering teams working on healthtech applications reach a point where someone asks a question that sounds simple but isn't: How do we make sure a developer testing a new feature can't accidentally access production patient data? The answer determines whether the architecture that follows will be auditable or not. Teams that answer it with process — "we have policies about that" — spend the next 18 months patching access-control gaps that reopen every time a new engineer joins or a new service gets wired in. Teams that answer it architecturally spend a week setting up AWS Organizations correctly and then largely stop thinking about it. This article covers the multi-account architecture pattern for HIPAA-compliant infrastructure — specifically, the account structure decisions that either enforce PHI workload isolation or make it a permanent source of audit findings. Why Single-Account PHI Isolation Fails at the Seams A single AWS account running production, staging, and development workloads creates a specific problem that IAM policies alone cannot fully solve. The issue is not that IAM is insufficient as a technology. IAM policies enforced within an account are only as reliable as the discipline of the people who manage them. A policy that restricts a developer's access to production RDS today can be modified tomorrow by anyone with sufficient IAM permissions. Nothing in the account structure itself prevents the boundary from being crossed. In practice, the gaps show up in predictable ways. A pipeline service role gets broad permissions during a sprint because scoping them properly would have taken an extra hour. An engineer copies an IAM role from staging to production because it was faster than creating a new one. A debugging session in production happens under an account that was supposed to be read-only. None of these are malicious decisions. They are the natural result of putting access control boundaries inside an environment where the people who need to cross them also have the permissions to do so. The access control problem that surfaces during security reviews is almost always this one — not a missing encryption setting or an unpatched vulnerability, but access boundaries that exist on paper and drift in practice. The Multi-Account Model: Enforcement at the Boundary AWS Organizations with a properly structured multi-account hierarchy solves this problem by moving the enforcement point outside the accounts being protected. The boundary is no longer an IAM policy that someone with IAM permissions can modify. It is an account boundary that the engineers inside those accounts cannot cross, enforced by Service Control Policies applied at the organizational unit level. The recommended structure has four organizational units under the root: a Security OU containing a Log Archive account and a Security Tooling account, a Production OU containing only the Production account where PHI workloads run, a Non-Production OU containing Staging and Development accounts, and a Shared Services OU containing the account used for CI/CD pipelines, DNS, and shared tooling. The Production OU sits under its own organizational unit with SCPs that restrict what can happen inside it, regardless of what IAM policies exist within the production account itself. An engineer whose IAM role in the development account grants broad permissions has those permissions scoped to the development account. Crossing into production requires a separate role, in a separate account, with a separate set of credentials. The architectural boundary is the enforcement mechanism, not the IAM policy. The Log Archive account under the Security OU serves a specific purpose: it is the only account to which CloudTrail logs from all other accounts are delivered, and it is an account to which production engineers have no write access. This means the evidence trail for PHI access events cannot be modified by the accounts generating those events - which is exactly what auditors verify when they ask about log integrity. Service Control Policies: What to Enforce at the OU Level SCPs applied to the Production OU are where the architectural enforcement becomes concrete. The first policy prevents anyone inside the production account from disabling CloudTrail, including account administrators: JSON { "Effect": "Deny", "Action": [ "cloudtrail:StopLogging", "cloudtrail:DeleteTrail", "cloudtrail:UpdateTrail" ], "Resource": "*" } CloudTrail continuity across the full audit period is not something that should depend on engineering discipline. It should be architecturally enforced. An account that can leave the organization can escape every SCP applied to it. This policy closes that path: JSON { "Effect": "Deny", "Action": "organizations:LeaveOrganization", "Resource": "*" } PHI that moves outside defined regions may fall outside data residency commitments. This policy locks the production account to specific regions: JSON { "Effect": "Deny", "Action": "*", "Resource": "*", "Condition": { "StringNotEquals": { "aws:RequestedRegion": ["us-east-1", "eu-west-1"] } }, "NotAction": [ "iam:*", "organizations:*", "route53:*", "budgets:*", "waf:*", "cloudfront:*", "globalaccelerator:*", "importexport:*", "support:*", "trustedadvisor:*" ] } EBS encryption is not enforced by default in all account configurations. This policy makes an unencrypted volume impossible to create in the production account: JSON { "Effect": "Deny", "Action": "ec2:RunInstances", "Resource": "arn:aws:ec2:*:*:volume/*", "Condition": { "Bool": { "ec2:Encrypted": "false" } } } Cross-Account Access: The Pattern That Doesn't Create New Gaps Multi-account architecture introduces a problem engineers feel immediately: how does anything talk to anything else? A CI/CD pipeline in the Shared Services account needs to deploy to production. A developer needs read access to production logs during an incident. A monitoring service needs metrics from all accounts. The answer is cross-account IAM roles with tightly scoped trust policies. A role created in the production account with minimum required permissions defines a trust policy that allows only specific principals from specific accounts to assume it, and only under specific conditions like MFA or an external ID: JSON { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::SHARED-SERVICES-ACCOUNT-ID:role/DeploymentRole" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "deployment-pipeline-prod" } } } ] } The deployment role in the Shared Services account can assume the deployment role in production - but only that role, only from that account, and only with the correct external ID. A developer's personal IAM credentials cannot assume it. An engineer who compromises the development account cannot use that foothold to pivot into production. This pattern creates cross-account access without creating a backdoor through the account boundary. The boundary holds because the trust relationship is explicit, narrow, and auditable through CloudTrail - every role assumption generates a log entry in both accounts. What This Architecture Makes Provable The operational argument for multi-account PHI isolation often focuses on security. The architectural argument that matters more for engineering teams dealing with audits and enterprise security reviews is about provability. In a single-account setup, proving that a developer did not touch production PHI during a given period requires auditing IAM policies, CloudTrail logs, and access history, and then arguing that the policies were correctly configured and consistently enforced throughout the period. There is always a gap between what the policy said and what actually happened, and that gap is what auditors probe. In a multi-account setup, the same question has a simpler answer. The developer's credentials are scoped to the development account. The development account has no access to the production account's resources. Access to production PHI requires a separate role assumption that is logged, requires separate credentials, and would appear immediately in CloudTrail. You are not arguing that the configuration was correct. You are pointing to an architectural boundary that makes the question moot. This shift from arguable to verifiable is what separates teams that sail through security reviews from teams that spend three weeks responding to follow-up questions. The Operational Overhead Is Smaller Than It Looks The most common objection to multi-account architecture from engineering teams is overhead. More accounts means more IAM configuration, more billing to reconcile, more consoles to log into. In practice, this friction is front-loaded and largely disappears once the structure is in place. AWS Control Tower reduces the account provisioning overhead significantly - new accounts inherit the correct SCP structure, logging configuration, and security baseline automatically. Account Vending Machine patterns built on top of Service Catalog or Terraform can provision a correctly configured new account in minutes. After the initial setup, adding a new account is not significantly more work than adding a new VPC. The billing concern is resolved through AWS Organizations consolidated billing, where all accounts roll up to a single payment method with unified cost visibility. The console switching concern is resolved through IAM Identity Center, which provides a single sign-on entry point across all accounts in the organization. The overhead that remains is real but small. The alternative - treating IAM policies inside a single account as the primary PHI protection mechanism - creates ongoing operational overhead that grows with the team and never fully goes away. Final Thoughts PHI workload isolation is an architectural problem, not a policy problem. IAM policies enforced inside an account are only as reliable as the operational discipline of the team maintaining them. Account boundaries enforced by SCPs at the organizational level are reliable by construction — they hold regardless of what happens inside the accounts they protect. The multi-account structure described here is not a compliance checkbox. It is the architecture that makes the access control claims in a security review actually true rather than approximately true with caveats. When an auditor asks how you prevent developer access to production PHI, the strongest answer available on AWS is an account boundary that the developer's credentials cannot cross. Building that boundary is a week of work. Not building it is a permanent source of audit findings.
Broken Object Level Authorization (BOLA) occurs when a REST API exposes an object identifier—such as an account, transaction, or loan ID — without verifying whether the authenticated user is authorized to access that specific resource. To protect fintech REST APIs, implement server-side authorization checks for every object request, validate permissions using the user's authenticated context and resource ownership, and avoid relying on client-supplied IDs alone. Using unpredictable identifiers such as UUID v4 or ULIDs can reduce object enumeration, but they should be treated as an additional security layer—not a replacement for authorization. Expert Insight: In retail banking systems, BOLA can expose sensitive customer and financial data when attackers manipulate object IDs, such as changing /api/v1/accounts/1001 to /api/v1/accounts/1002. Randomized identifiers make enumeration harder, but the core defense is object-level authorization on every API request. Policy-based controls, including tools such as Open Policy Agent (OPA), can help enforce consistent ownership and access rules across services. 1. Understanding BOLA in Fintech Ecosystems Fintech APIs handle highly sensitive operations, including transaction retrieval, account information, payment processing, and ledger-related activities. Broken object-level authorization (BOLA) occurs when an API uses a client-supplied object identifier to retrieve or modify a database record without verifying that the authenticated user has permission to access that specific resource. Authentication vs. authorization: Authentication confirms who the user is, such as validating a JWT. Authorization determines what that authenticated user is permitted to access or modify. A valid login does not automatically grant access to every financial object.The scale of risk: Modern open-banking ecosystems connect banks, fintech platforms, payment providers, and third-party applications. A missing object-level authorization check can therefore expose sensitive account, transaction, or payment data beyond the intended user or organization. Example: If a customer can access /api/v1/accounts/1001 and simply change the ID to /api/v1/accounts/1002 to retrieve another customer's account, the endpoint has a potential BOLA vulnerability. 2. The Anatomy of a Banking BOLA Attack Consider a poorly secured REST API endpoint used to fetch a customer's monthly credit card statement: HTTP GET /api/v1/statements?account_id=89234 An attacker first logs in with their own valid account and accesses their statement using account_id=89234. They then use an interception proxy such as Burp Suite to change the account ID in the outgoing HTTPS request: HTTP GET /api/v1/statements?account_id=89235 If the backend directly uses 89235 to query the database without checking whether this account belongs to the authenticated user, the API may return the victim's private banking information. This is a classic BOLA vulnerability. The main issue is that the API checks whether the user is logged in, but fails to check whether that user is actually allowed to access the requested account. 3. Top 5 Architectural Practices to Mitigate BOLA a. Avoid Sequential Integer IDs in Public APIs Avoid exposing simple auto-increment database IDs such as 1, 2, or 3 through public API endpoints. Use unpredictable identifiers such as UUIDv4 or ULID (Universally Unique Lexicographically Sortable Identifier) instead. This makes automated ID guessing and enumeration much harder. However, random identifiers should be treated as an extra security layer, not as a replacement for proper authorization checks. b. Do Not Depend on Client-Supplied Parameters for Authorization The client should never decide the access boundary simply by sending an account or resource ID in the URL. Instead, the backend should get the authenticated user's identity from a securely verified session or validated JWT claims and then check whether that user has permission to access the requested resource. c. Use Fine-Grained Access Control (FGAC) Use authorization models such as attribute-based access control (ABAC) or relationship-based access control (ReBAC) when the application needs more detailed permission rules. For example, the system can maintain clear relationships between users, accounts, transactions, loans, and other financial resources. The API can then check whether the requested object is actually linked to the current user's permitted scope. d. Centralize Common API Security Policies In a microservices environment, repeating authorization logic separately in every service can create gaps and inconsistent rules. API gateways such as Kong, Apigee, or AWS API Gateway can help enforce common authentication, token validation, routing, and security policies at the edge. However, sensitive object-level authorization should still be enforced by the service that owns the resource. e. Shift Security Testing Left Include API authorization testing throughout the CI/CD pipeline instead of waiting until production. Automated security tests can change resource identifiers, use different user identities, and verify that unauthorized requests are rejected. For example, a test can confirm that User A cannot access User B's account and that the API returns an appropriate 403 Forbidden or 404 Not Found response according to the application's security design. Securing fintech APIs (such as AutoPay By NPCI) against BOLA is critical for safeguarding sensitive user data [OWASP]. Teams can utilize architecture resources and deployment calculators to audit system compliance costs, optimize processing infrastructure, and seamlessly bridge secure development workflows with enterprise-grade financial technology standards. 4. Implementing Contextual Code-Level Checks At the code level, a secure Java/Spring Boot controller should perform an object-level authorization check before passing the request to the service or repository layer. Java @GetMapping("/api/v1/accounts/{accountId}") public ResponseEntity<AccountDetails> getAccount(@PathVariable String accountId, @AuthenticationPrincipal JwtPrincipal principal) { // Check if the authenticated user UUID matches the requested resource ownership if (!authorizationService.isOwner(principal.getUserId(), accountId)) { throw new AccessDeniedException("Unauthorized resource access attempt."); } return ResponseEntity.ok(accountService.findById(accountId)); } 5. The Verdict: How to Audit Your System Step 1: Review all public REST API endpoints that accept user IDs, account IDs, transaction IDs, or other object identifiers through URL paths, query parameters, or JSON request bodies.Step 2: Make sure your QA and security tests include cross-user and cross-tenant access checks. For example, authenticate as User A and try to access User B's statement. The request should be rejected.Step 3: Use centralized authorization controls, middleware, or framework-level security components to apply identity and permission checks consistently across API endpoints. This helps reduce the chance of one controller accidentally missing an important authorization check. A proper BOLA audit should verify not only whether users are authenticated, but also whether they can access only the financial objects they are actually authorized to use.
Dynamic testing is essential because it uncovers vulnerabilities in running applications. But while SAST gets the attention because it’s shift-left and relatively straightforward to fix, DAST often gets stuck in the backlog. Application security testing generally splits into two approaches. SAST (static analysis) scans source code before it ever runs, catching issues while a developer is still in the file, which is why fixes tend to happen fast. You're editing code you just wrote, with full context on what it does and why. DAST (dynamic analysis) works differently. It tests an application while it's running, sending real requests at live endpoints to see what breaks, the same way an attacker would probe it from outside. That's what makes DAST so valuable. It catches vulnerabilities that only show up in production behavior, not in the code itself. But it's also what makes DAST findings harder to act on. A SAST finding points to a file and a line. A DAST finding might simply point to a URL that returned something it shouldn't have, with no direct link back to the code that caused it. That gap is why DAST findings so often stall in the backlog while SAST findings get resolved first. Let’s break down how developers can make DAST findings behave less like alerts and more like bug reports they can actually work on. A Finding Needs Repro Evidence, Not Just a Vulnerability Name A vulnerability name alone is not very useful. What matters are the details and the ability to quickly reproduce the issue. A developer needs the request, the payload, the vulnerable parameter, and the auth context it ran under. It’s important to hand the finding over as a request the developer can run, not just a description they have to read. A HAR file captures the full request and response cycle. A working curl command lets someone fire off the exact same request from their terminal and watch it fail the same way. Either format turns a mere alert into a bug report worth acting on. A Finding Doesn't Know Who Owns It A DAST finding lives at the network layer. It knows the endpoint that responded and the payload that broke it. On the other hand, DAST is unaware of which repository owns that endpoint, or which team gets paged if it breaks. It’s important to remember here that DAST works by hitting an application from the outside, in the same way that an attacker might, so it was never going to have visibility into the underlying codebase. If you can’t bridge that gap, then a finding just sits there, because nobody can confirm it's theirs to fix. The solution comes from correlating the runtime finding with a repository and a code path. Pairing DAST with SAST data helps, since SAST already has the codebase mapped, though the correlation is rarely perfect. A monorepo or a shared service layer can leave 'this endpoint belongs to this team' genuinely unclear, no matter how good the integration is. API inventory data helps too, tying live endpoints back to the services behind them, but it depends on that inventory being kept current, which not every team manages well. The finding still won’t be actionable on its own. But if you manage to turn "this URL is vulnerable" into "this line, in this repo, probably owned by this team," then you’ve effectively given a developer a starting point from which they can actually work. A Severity Score Doesn't Tell You If Anyone Can Reach It Knowing where a finding lives and who owns it still doesn't guarantee anyone acts on it. A developer with a backlog full of feature work needs a reason to bump a security fix ahead of everything else, and a severity label alone rarely makes that case. CVSS scores describe how bad a vulnerability might be theoretically, based on the vulnerability itself, but they do so without any context into the environment in which the vulnerability sits. A 9.8 score on an endpoint that isn't internet-facing and requires authentication hardly deserves the same attention as a 9.8 that's wide open. Indeed, the score alone can't tell a developer which type of situation they're facing. The catch is that this context isn't always so easy to attach. Someone has to actually know the app's architecture well enough to know what’s actually reachable — parameters that are often under-documented, especially as apps undergo so many dynamic changes. When reachability context is missing, the honest move is to flag the finding as an unverified exposure rather than allow an outdated assumption to drive a prioritization decision. Getting this right matters more than getting to it fast, since a developer who acts on bad exposure data once will start ignoring the context field altogether. Findings Die in Dashboards Developers Never Open A finding can have perfect reproduction steps, a clear owner, and full exploitability context, and still go nowhere if it's sitting in a security dashboard the developer never logs into. Not to imply that this is a discipline problem. Developers simply prefer to work out of their own backlog – Jira, Linear, whatever the team uses – and a separate security tool is one more login, one more context switch, one more thing to remember to check. Ideally, findings should land automatically in the same place developers already work, whether that's a ticket created through the platform's API or a message in the team's Slack channel. Prioritizing these pushes can be challenging to nail down as well, because auto-creating a ticket for every low-severity finding just adds noise to a backlog, effectively training developers to ignore the security label entirely. Opening a dev ticket works best when it's reserved for critical and high-severity findings. Lower-severity findings are often better left in a queue that gets triaged in batches. A Fix That's Never Retested Is Just a Guess The final step in remediation is confirming that the fix actually worked. Ironically, it's the step most likely to get skipped. All too often, a developer closes the ticket, moves on, and nobody circles back to check whether the underlying request still fails the same way it did before. The fastest way to check is to simply rerun the exact request that triggered the finding in the first place. This can take place either as a one-click retest button or as an automated check upon the next deploy. No matter which method you use, the developer shouldn't have to manually rebuild the original request from memory or dig back through the ticket to reconstruct what to test. Retesting is also where a false sense of progress might creep in. A finding that stops firing isn't necessarily a finding that's been fixed – the endpoint could have moved, a WAF rule could be masking it, the auth flow could have changed in a way that makes the original payload irrelevant without addressing the underlying flaw. A clean retest is useful as a signal, not proof, and treating it as automatic closure is how vulnerabilities quietly resurface months later under a different path. Conclusion DAST finds real vulnerabilities; they’re just harder to act on. But that doesn’t make it acceptable to ignore them. Reproduction evidence turns an alert into something the developer can run and test themselves. Mapping a finding to its repo turns it into an assigned task. Exploitability context gives it urgency. Routing it into Jira or Slack gets it seen. Retesting proves that the fix has taken hold. With a few tweaks in how findings are created and reach developers, critical vulnerabilities that surface during dynamic testing can finally get the attention they deserve.
Product Security,
Microsoft
Data/AI,
Cisco
Josephine Eskaline Joyce, Ph.D
Chief Architect,
IBM
Technical Writer,
Self-Employed