Stage 7 · Master
Security Hardening
Securing Secrets and Sanitizing Input
How a Go backend should keep credentials out of ordinary code paths, why validation is broader than SQL injection defense, and how those rules map onto Meridian's current service boundaries.
Two Different Security Problems
Secret management and input sanitization are often discussed together because both sit near application boundaries, but they solve different classes of risk. Secret handling protects what the system knows: passwords, keys, tokens, and connection strings. Input sanitization protects what the system accepts: URLs, JSON fields, query parameters, headers, and request sizes. Mixing the two ideas leads to shallow defenses, such as believing that parameterized SQL solves all input risk or that storing a password in an environment variable automatically makes it safe.
| Problem | Typical examples | Primary control |
|---|---|---|
| Secret exposure | Database passwords, Redis passwords, signing keys, third-party API tokens | Keep secrets out of source control, minimize who can read them, and inject them only at runtime. |
| Input misuse | Oversized JSON bodies, unknown fields, invalid enums, unsafe sort keys, wildcard-abuse search strings | Decode strictly, validate early, normalize values, and allow-list behavior-shaping fields. |
| Authorization confusion | A valid request touching another tenant's data | Enforce tenant and object ownership in application logic even after basic input validation passes. |
Secret-Handling Principles
A production-grade service treats secrets as a narrow runtime dependency, not as a source file, Docker build argument, or general-purpose debug variable. Good secret handling follows three rules. First, fake development values may live in .env.example, but production values must come from a controlled runtime source. Second, each service should receive only the secrets it genuinely needs. Third, secret rotation must be operationally possible without rebuilding the image. If any of those rules fails, ordinary maintenance becomes a security risk.
Input-Sanitization Principles
Input sanitization begins before business logic runs. The transport layer should reject bodies that are too large, contain unknown fields, repeat incompatible shapes, or provide values outside the documented contract. Then the application layer should normalize and validate accepted fields: trim whitespace, bound lengths, allow-list enums, parse timestamps, and reject behavior-shaping identifiers that the client should not control directly. Parameterized SQL matters, but it covers only literal value injection. It does not secure a user-controlled ORDER BY, an unsafe wildcard search, or a DTO that accepts fields the client should never be allowed to set.
A request can be syntactically valid JSON and still be unsafe or nonsensical. Security-grade validation asks whether the request is allowed, bounded, and semantically coherent, not merely whether it parses.
How This Applies to Meridian
In the real repo, .env.example contains development-only values such as POSTGRES_PASSWORD=meridian_dev_password and REDIS_PASSWORD=meridian_redis_password. That is acceptable because the project is explicit that the file is for local development. The future production design must move those values into Kubernetes Secrets or an external secret manager. On the input side, the current services expose only /healthz, so there are no business handlers yet. The correct educational move is therefore to establish the validation patterns now, before create-user, create-resident, and create-invoice endpoints appear and force ad hoc security decisions later.
Planned Implementation Seams
apps/gateway/internal/config/config.gomodifyResponsibility: Teach gateway configuration to prefer runtime-provided secret sources for future credentials rather than only plain environment strings.
Why now: The gateway will eventually hold auth and upstream credentials that should not be copied through every debug surface.
apps/identity-service/internal/http/requests/create-user_request.gocreateResponsibility: Decode and validate identity-service write requests strictly before domain logic sees them.
Why now: Identity endpoints create high-value records and should establish validation discipline early.
apps/community-service/internal/http/requests/create-resident_request.gocreateResponsibility: Normalize names, addresses, and IDs before storage or downstream processing.
Why now: Community-service will accept broad user-provided input and therefore needs an explicit request boundary.
apps/billing-service/internal/store/invoice_repository.gocreateResponsibility: Use allow-lists for sorting and filtering fields that cannot be parameterized safely as identifiers.
Why now: Billing queries will eventually expose search and listing surfaces where client-controlled sorting must remain server-owned.
Reference Patterns
apiVersion: v1
kind: Secret
metadata:
name: platform-secrets
namespace: meridian
type: Opaque
stringData:
POSTGRES_PASSWORD: replace-from-external-secret-store
REDIS_PASSWORD: replace-from-external-secret-storeThe manifest demonstrates category, not a recommended storage process. In a mature deployment, the values should come from an external secret manager rather than living as plaintext inside version control.
Mark this lesson complete to store local progress and unlock a cleaner resume path the next time you visit.