A legacy upgrade can ship on schedule and still create a security incident: the new endpoint may expose a field the old interface never returned, or a diagnostic trace may retain personal data longer than the application does. Product managers should treat security and privacy discovery as part of the upgrade scope, not as final QA. A small code change is no reason to assume a small trust change.
The smallest code change can create the largest new data path
I agree with Your First Legacy Upgrade Needs a Seam, Not a Rewrite about limiting code churn, but I would not treat a thin seam as evidence that the new path preserves the old application’s access rules. A wrapper that reads an existing customer table may be only a few files of code, yet it can make records available to a different service account, network segment, cache, and logging system. Each destination changes who can obtain the data and how long a copy might survive.
Before estimating implementation, ask the team to draw one actual request from caller to database and back. Mark where identity is established, where authorization occurs, which fields cross each boundary, and which systems retain a copy. Use a captured staging request with synthetic data rather than an imagined “typical user,” because conditional fields and error responses are easy to miss in a design meeting. If an inventory finds, for example, 12 distinct write endpoints, that number is a measured baseline for this application only; it is also a better unit for test planning than “one API integration.”
The diagram should include failed requests. An NGINX access log can retain query parameters even when the application rejects the call, while an OpenTelemetry span can carry an identifier added before authorization runs. W3C Trace Context helps correlate requests across services, but its trace identifiers do not make attached attributes private. The useful product question is therefore not merely “Can the new service read this table?” It is “Which callers can cause this field to appear in a response, trace, cache entry, or support export?”
Do not assume that existing database permissions answer that question, because a shared service account can turn many user-specific decisions into one broadly privileged connection. PostgreSQL row-level security may narrow access when policies are correctly tied to the requesting identity; it costs policy design and tests for background jobs that have no interactive user. pgAudit can supply useful database-access evidence, but its logs need their own access and retention rules because audit records can reveal sensitive query details.
I would not approve a production wrapper that uses the legacy application’s all-purpose database credential merely to save integration time: a compromised wrapper would inherit permissions unrelated to its feature. Budget a dedicated account, the minimum required grants, and a review of stored procedures that might bypass the intended restrictions. Those are deliverables with acceptance tests, not an instruction to engineers to “be careful.”
Wrapping first wins only when identity survives the trip
There are two credible options. Wrap the existing behavior when its business rules are hard to reproduce and callers can be mapped reliably to its authorization decisions; the cost is an extra trust boundary and continued dependence on old permissions. Refactor the behavior in place when authorization is inseparable from hidden session state or direct database access; the cost is greater regression risk in code that may have little test coverage. Neither option wins on security by being newer, because both can misidentify the caller or mishandle data copied during the transition.
I would challenge Refactor or wrap first, when QA owns an untested legacy app on sequencing: map identity and data flows before committing to either choice, because QA cannot verify an authorization rule nobody has named. Ask for a matrix of actors, actions, and data scopes, including disabled accounts, delegated access, and background jobs. Then require negative tests: one tenant attempting another tenant’s record, a former employee retrying a saved URL, and an administrative operation attempted through an ordinary account.
An OAuth 2.0 access token or OpenID Connect ID token is not automatically a legacy session. Its audience, issuer, expiry, and subject must match the action being authorized; otherwise the wrapper may accept a valid token issued for a different service. If a browser is involved, review the session cookie’s Secure, HttpOnly, and SameSite attributes and test cross-site request forgery defenses. PKCE under RFC 7636 protects an authorization-code exchange, but it does not replace server-side authorization on the requested record.
Give the team a session lifetime to test rather than an indefinite instruction to “secure sessions.” A 15-minute idle limit is a value to tune against real user tasks and recovery paths, not a universal recommendation. Test what happens when the upstream session expires during a write and when a user’s access is revoked while a token remains valid. Retrying a failed write without an idempotency rule can create duplicate transactions, so the security test plan should include the business effect of retries as well as whether a request returns an error.
For service-to-service calls, mTLS or SPIFFE identities can make the calling workload explicit, but neither proves that the human user may view a particular record. That distinction belongs in the acceptance criteria: authenticate the workload, propagate only the user claims the downstream decision requires, and verify authorization at the point where data is returned or changed.
Observability is a privacy feature only after someone constrains it
Teams often add extensive logging during an untested upgrade because failures otherwise take too long to diagnose. That choice is defensible only if the team decides what must not enter logs and traces. A request body, bearer token, email address, or full error payload can pass from a tightly controlled database into a much wider observability platform. Sampling reduces volume, not the sensitivity of any event that is retained.
Specify an event schema before launch: request identifier, operation name, outcome, and a non-sensitive error code may be enough to investigate many failures. Configure OpenTelemetry instrumentation to exclude request bodies and credentials, and check application logging separately because instrumentation settings do not control every manual log statement. Hashing an email address is not necessarily anonymization, because a stable hash can still link events and can be guessed when the input space is small.
Here is a runnable check for a JSON Lines export named captured-events.jsonl from an authorized staging run. It rejects forbidden JSON keys, including nested ones; it does not detect secrets embedded in free-text messages, so keep that review in scope too.
python3 - <<'PY'
import json
blocked = {'email', 'session_id', 'access_token', 'ssn'}
def check(value):
if isinstance(value, dict):
for key, child in value.items():
assert key.lower() not in blocked, key
check(child)
elif isinstance(value, list):
for child in value: check(child)
for line in open("captured-events.jsonl"):
check(json.loads(line))
print("No blocked JSON keys found")
PY
Use Playwright to exercise the browser flows that generate these events, then inspect the resulting network responses and telemetry together; a clean application response does not imply a clean trace. OWASP ZAP can probe exposed routes, while OWASP ASVS 5.0.0 provides verification requirements to select from, but neither substitutes for testing the application’s particular tenant and role rules.
Retention needs a product decision as well as a storage setting. A 30-day trace-retention target is a proposed limit to validate against incident-investigation needs and applicable obligations, not a default to copy blindly. Ask who can search traces, whether support staff can export them, how deletion requests affect retained copies, and whether backups age out on a different schedule. AWS states that Amazon S3 has automatically applied server-side encryption to new objects since January 2023; that vendor-published protection does not answer who may read a trace stored there or when it is deleted.
A release gate should price the evidence, not just the implementation
A realistic scope separates feature work from evidence work. Put ownership and estimates against a data-flow inventory, a permission matrix, a staging telemetry capture, negative authorization tests, credential changes, and a rollback exercise. Each item should produce an artifact someone can inspect; “security review complete” is difficult to verify when it names neither the decision nor the evidence behind it.
Set the release gate before engineering begins. For a feature that exposes customer records, require a demonstrated denial for cross-account reads and writes, proof that the production service account cannot query unrelated tables, and a review of sampled error logs for personal data. Define who can accept an exception and its expiry date, because an unresolved exception otherwise becomes an undocumented permanent behavior. If an old endpoint cannot enforce the required boundary, reduce the feature’s reachable audience or move the work out of the release rather than describing the gap as a test limitation.
Allow time for findings that change the design. Discovering that the old application authorizes through server-side session state may invalidate a stateless wrapper estimate; discovering that a support export includes deleted records may extend privacy work beyond the new endpoint. Carry those as explicit scope risks with decision dates, because hiding them in a generic contingency makes it harder to choose between a smaller launch and a later one.
Plan rollback around data, not only deployment. Restoring the previous application version will not retract a record already copied into Redis, a trace already exported, or a credential already granted broad access. A rollback rehearsal should show how to disable the new route, revoke its credentials, stop further exports, and identify copies governed by retention rules. That exercise is worth scheduling before launch because its result can change whether the feature is safe to release at all.
The first planning task is to trace one forbidden request
At the next scoping meeting, choose a record one user must never see and trace an attempted read through the proposed design. Ask engineering and QA to show where it is denied and whether the attempt leaves sensitive data in logs or traces. If they cannot demonstrate both, add the missing discovery and tests to the estimate before approving a delivery date.



