Production security for mid-sized teams: the foundation your project needs before going live
13/03/2026
Production security isn't just a list of enabled tools. It's a set of safeguards your team can verify. If you can't explain who has access, what's exposed, and what happens when something goes wrong, your system isn't ready.
Production security is often approached backward. Teams enable options, add rules, configure alerts, and assume the system is covered. But what they get isn't security, but rather a set of intentions.
The problem arises when those intentions are put to the test. An account that shouldn't exist remains active, a credential ends up in a log, a database is accessible from the internet because no one checked an initial shortcut, a backup appears to be created correctly but cannot be restored. And the alerts generate so much noise that the important one gets ignored.
A system in production is not secure because configurations from a document have been applied, but when the team can explain what risk that configuration reduces, verify that it works, and maintain it while the system changes.
This article is not a corporate security program, but a practical basis for medium-sized projects: the conditions that should be met before considering a system ready for production.
What is this article for (and what is it not for)
For most teams, production security isn't about sophisticated attackers or exotic threat models. It's about preventing recurring failures in everyday projects.
The usual problems are always the same: a compromised administrative account, a leaked credential, an exposed database because no one checked a temporary configuration, a backup that looks healthy but doesn't restore anything useful, or an alert stream so noisy that the important signal gets lost.
The goal is to turn those risks into concrete guarantees. In each area, your team should be able to answer three things clearly: what must be true, what risk reduces that truth, and how it would be verified under real-world conditions. If those answers are vague, the security model is too.
Production security fundamentals: access, secrets, and exposure
The first layer of security isn't glamorous, but it's where many preventable incidents begin. Before even considering application-level issues, your team should be confident that access is controlled, secrets are manageable, and the system only exposes what it's intended to expose.
Identity and access control
Most teams underestimate how many problems start with normal logins — not with an unknown vulnerability or a sophisticated attack, but with an account that shouldn't exist anymore, a privilege that was never reviewed, or strong authentication that was missing at the wrong time.
The principle is simple: only the right people should have access, only to what they need, and in a way that can be attributed and revoked.
This involves more than just stronger login methods. A secure production environment shouldn't rely on shared accounts, exceptions, or informal knowledge about who has access to what. Administrative access should be personal and reviewable, the permissions model should follow the principle of least privilege (not convenience), and revoking access should be a standard operating procedure.
In mid-sized teams, another risk quickly emerges. Production access becomes fragmented across infrastructure, repositories, DNS, email, monitoring, CI/CD, and third-party APIs. Even if each individual account is protected, the entire model becomes fragile if no one can view it as a single system.
The important test isn't whether an access policy exists, but whether a real revocation can be done quickly and completely. If a core developer's laptop is stolen tonight, can your team revoke their access to everything in minutes? Infrastructure, deployments, logs, third-party vendors. If the answer is "it depends on who's available," access control is weaker than it seems.
Secrets and sensitive settings
Many teams treat secrets as if they were just another configuration category. That's a mistake.
Secrets are not standard performance values, but high-impact assets with their own lifecycle: they can be leaked, copied, reused, rotated, invalidated, or forgotten. A production system isn't ready if it only answers "Where do we store secrets?" It also has to answer "What happens when one is exposed?"
The goal is for secrets to be separate from the code, limited in scope, protected during operation, and replaceable without panic.
Leaks rarely seem dramatic at first. A token appears in a log. A credential is left in an environment dump. A private key is copied into a deployment note. An integration secret is reused across environments because it was once convenient. Over time, the system accumulates an invisible fragility.
A more robust way to think about secrets is in terms of duration and scope. If a secret is leaked, what matters is how long it remains valid, what it can access, whether it can be cleanly replaced, and whether it appears in logs, process listings, or crash reports.
In a Django project deployed on AWS or a VPS, secrets should reside in environment variables or a dedicated manager. AWS Secrets Manager or HashiCorp Vault are common choices. Never in the repository. The local .env file is for development. In production, values should be injected in a controlled manner through the deployment pipeline.
The right test isn't "Do we use a secrets store?" It's "Can we rotate a production credential securely and quickly?" If the answer requires coordinating three people and manually handling four services, the model remains fragile.
Network exposure and infrastructure limitations
One of the simplest ways to reduce risk is to expose less. Every forgotten public port, endpoint, admin panel, or subdomain expands the attack surface.
Medium-sized projects rarely need widespread public exposure. But many end up with internal services accessible via the internet because it was the quickest way to get something working. It's common to find a Django admin panel without IP restrictions, a debug endpoint that's still running, a staging service accessible without a VPN, or old domains that still point to live systems.
The goal is clarity: public entry points should be deliberate, internal services should remain internal, and administrative access should be restricted by IP, VPN, or both.
In AWS infrastructure, security groups and VPCs handle this. In a VPS, firewall rules (iptables, ufw) and a clean Nginx configuration are used. In both cases, verification cannot be solely internal.
Reviewing the planned firewall rules isn't enough. Neither is reviewing an infrastructure diagram. Security here needs to be tested externally: verifying what's responding from a public network, which ports are accessible, which domains are still resolving, and which interfaces are exposing more than they should.
The truth about network exposure isn't inside the server. It's what an external scan sees.
Application and runtime: what the system is allowed to trust
Once the fundamentals are covered, the next question is whether the running system behaves securely. This is where many teams focus first. But application-level security only works well when the surrounding operating model is already sound.
Application trust limits
Most application security flaws are boundary flaws.
The application trusted an input it shouldn't have trusted. It assumed an authenticated user was authorized for everything. It treated internal traffic as safe simply because it came from the private network. It exposed internal error details because no one checked the exception path. It reused production data in a place where it should never have ended up.
The most useful framework for thinking about application security is not a list of vulnerability names. It's a map of trust boundaries.
User input, imported files, and browser requests should be treated as untrusted. Requests from internal services should also be evaluated based on their actual capabilities, because authentication is not the same as authorization, and "internal to the system" is not the same as secure.
This is especially important in systems that grow iteratively. A route starts as an internal convenience and then becomes part of a responsive workflow. A background process receives more permissions than intended because it was easier. A role model becomes inconsistent over time.
In a Django project, this translates into concrete measures: strict validation of forms and serializers, permissions per view (not just per login), active CSRF protection, and error paths that don't reveal stack traces or configuration. The `DEBUG = True` setting that everyone uses in development should be `False` in production. It seems obvious, but it still turns up in real-world audits.
Production failures also fall into this category. Error paths shouldn't reveal implementation details or sensitive context. The application should fail safely for users and usefully for those who operate it. That's where a tool like Sentry provides real value: it captures the full context of the error without exposing it to the end user.
The test is straightforward: can an unexpected user, request, file, or internal caller cross a boundary that your team considers secure? If this hasn't been deliberately tested, the boundaries are weaker than expected.
Runtime, supply chain, and public behavior
Security doesn't end with the application logic. What runs in production, how it runs, where it comes from, and what the outside world sees all affect the actual risk profile.
The goal is to maintain a comprehensible, controlled, and privilege-limited runtime environment: services should not run with more permissions than necessary, production artifacts should be traceable to controlled change, and dependencies should not accumulate without review.
Here, teams often confuse stable with secure. Components that haven't been reviewed in a long time may still function, but they also tend to become opaque: no one knows exactly which versions are in production, no one wants to touch the dependency tree, and no one trusts the impact of a necessary update.
In a Django project using Python, the requirements.txt or pyproject.toml file should be audited regularly. Tools like pip-audit or Dependabot automatically check your dependencies for known vulnerabilities. In a Flutter app, the dependencies in pub.dev deserve the same attention.
The same problem arises at the public layer. Your team configures the application one way and assumes that the transport protocol, redirects, cookies, and headers behave accordingly. But it never verifies the public response. On paper, everything seems correct. In practice, the public endpoint might tell a different story.
The best practice is clear. Your team should be able to link a production artifact to a controlled change, identify which dependencies are in use, and detect anomalous behavior before users report it. And your public responses should align with the guarantees you believe you're providing.
The secure configuration is not the one that exists in the repository. It's the one that the outside world can see.
Operational resilience: how the team changes and recovers the system
A system can have robust controls and still be fragile. Production security isn't just about avoiding bad outcomes. It's also about safely changing the system, detecting problems early, and recovering in a controlled manner.
Security in deployments and operations
Many production incidents are introduced by the team itself — not through negligence, but through normal changes made under pressure: a rushed deployment, an unchecked manual correction, a rollback that doesn't actually restore the previous state, or an emergency command that solves one problem while creating another.
The goal is for production changes to be predictable, attributable, and reversible. A system is more secure when deployments follow a controlled path, when emergency actions have clear rules, and when recovery doesn't depend on improvisation.
This matters because operational shortcuts accumulate hidden risk. Teams often believe they have a reliable deployment process, but in practice they have two or three: the documented one, the unofficial one, and the one someone uses when everything goes wrong. This leads to configurations deviating from the expected state, secrets being lost, and production ending up in a state that no one can fully explain.
A stronger model defines three things: a normal way to change production (the CI/CD pipeline), a limited alternative when that path is not available, and a clear point at which an incident ceases to be "testing things" and becomes a recovery scenario.
If the primary deployment method fails, can your team recover the system without making it less reliable? If the answer depends on undocumented commands or on one person remembering the correct sequence under pressure, operational security is weaker than it appears.
Backups, recovery, RTO and RPO
Many teams feel reassured once backups are in place. That reassurance is often premature.
The goal is not to have backup files, but to be able to restore service and data within acceptable limits when something fails.
That's where recovery objectives become useful. The Recovery Time Objective (RTO) defines how long the system can be down before serious damage occurs. The Recovery Point Objective (RPO) defines how much recent data can be lost. These aren't abstract questions. They determine whether you're planning for a service restart, a database restore, or the complete loss of the host.
Here, many teams discover the difference between managing backups and being prepared for recovery. A job might run every hour but not meet business needs because the actual restore points are too large. A retention policy might exist, yet the crucial point isn't retained. An archive might appear healthy but fail to restore anything useful.
The real sign of maturity is not the frequency of backups, but the confidence in restoration.
That confidence comes from a recovery process that exists outside of people's minds. If your team had to restore a production database tonight, they should know which scenario applies, what artifacts they need, and how success is validated. "Something broke" isn't a single incident. Restarting a service, fixing logical corruption, and recovering from a total infrastructure loss are distinct events with different paths.
That's why a recovery runbook matters. Not as filler documentation, but as a practical sequence that someone can execute under pressure. Recovery shouldn't depend on reconstructing decisions from old messages while the application is down.
The test is simple: when was the last realistic restore test, how long did it take, and were the necessary credentials and artifacts available outside the affected systems? If the answer is "we've never tested it end-to-end," the backups are still not reliable.
Incident monitoring and detection
A production system without monitoring lags behind. It learns about problems later than users or when the incident has already become costly.
The goal is not maximum observability, but useful monitoring.
Teams often collect too much and detect too little. They accumulate logs, metrics, traces, dashboards, and alert rules until the system becomes so noisy that everyone stops ignoring it. When that happens, there's monitoring, but no real preparedness.
A secure production environment should be able to quickly confirm whether the system is available, functioning correctly, degrading, and whether anything has changed that shouldn't have. Security-relevant events must be visible and accessible for investigation.
This includes both technical failures and security-relevant behaviors: repeated authentication failures, unusual administrative actions, backup errors, deployment problems, strange traffic changes, or privilege changes. These are not mere operational details, but rather part of the system's security posture.
But the signal-to-noise ratio matters more than many teams admit. An alert should mean that someone might need to take action. If security alerts end up in a folder that no one checks, the system isn't working.
According to the practices documented in the Google SRE Book , alerts should require action, not just be informational. An alert that doesn't require immediate action should be a ticket or a dashboard entry, not an alert.
The correct check is not whether alerts exist. It's whether the team trusts them and whether the first few minutes of an incident are structured enough to turn the signal into the required action.
Data obligations: beyond uptime
Technical teams often treat this part as separate from engineering, but it is not.
The goal is to ensure the system can handle the actual obligations associated with the data it stores, processes, and exports. For a technical team, the most helpful way to think about this is not in terms of regulatory compliance, but rather as a data lifecycle capability.
Your system should be able to identify where each user's data resides, delete it through a reasonable operational process, and export it without manual intervention on the database. Retention periods for business records, logs, and backups should be defined. And third parties that process data should be identified and vetted.
Many legal and privacy failures are engineering failures. The system cannot cleanly delete a user because the data is scattered across too many locations. The backup policy retains data longer than intended because no one modeled retention. A media export contains more than it should because the limits were never clearly defined. A third-party vendor processes data under assumptions that no one documented.
In a Django project, the ability to delete and export data should be a built-in system feature, not a script that someone runs manually when a request comes in. GDPR compliance is not optional if you have European users, and complying with it from the product design stage is cheaper than retrofitting it later.
Your team doesn't need to be legal experts to improve this. They need enough clarity to know what the system should be able to do with the data it holds. That's a technical requirement, not just a legal one.
Before the go-live: what should be done
Before going into production, your team shouldn't rely on optimism, local knowledge, or the hope that nothing strange will happen at the beginning.
The foundation should already be evident in how the system operates. Access can be revoked quickly, secrets can be rotated without panic, and public exposure is deliberate and verifiable from the outside. The application enforces its trust boundaries even when requests originate from within. Public behavior aligns with assumptions regarding transport, redirects, headers, and cookies.
The same standard applies to operations. Production changes follow a controlled path, and failures in that path don't force the team to improvise. Recovery expectations are understood in practical terms: how much data could be lost, how long the restoration would take, and what's needed to perform it. Backup failures are detected before a restoration is required. Alerts are credible enough to warrant action. Data deletion, export, and retention are built-in system capabilities.
If those conditions are still not met, the problem isn't that the system has suspended a formal checklist. The problem is that production assumptions continue to take the place of production guarantees.
Production security: a foundation, not a badge
The easiest way to make production security appear comprehensive is to list many configurations. The more difficult—and most useful—way is to define the guarantees the system must offer and verify them as it evolves.
That's the approach medium-sized projects need. No corporate theater. No tool worship. No a collection of tweaks that look reassuring in internal documents but behave differently in reality.
A production security foundation is much more practical than that: access is controlled, secrets are manageable, exposure is intentional, the application enforces its boundaries, the runtime environment is understandable, changes are controlled, backups actually restore, alerts mean something, and data obligations are reflected in system behavior.
Implementation will vary from project to project, but the guarantees shouldn't. And assumptions, in production, have a very short lifespan.
Production safety checklist
This checklist is designed to be reviewed for each project before go-live and periodically thereafter. Each item should be verifiable with a yes or no. If the answer is "no" or "I don't know," that item needs attention. Some items are designed for the Mecexis stack and/or may be optional depending on the project, but you can easily adapt them to the specifics of your technologies, environment, and team.
Access and identity
- All administrative access to production is personal and nominative (there are no shared or generic accounts).
- Each person has two-step authentication (2FA) enabled on all production services.
- Permissions follow the principle of least privilege: no one has more access than they need for their job.
- Removing access is part of the offboarding process (when someone leaves the team or project, their access is removed on the same day).
- A complete revocation of access (infrastructure, repositories, DNS, CI/CD, third-party services) can be done in less than an hour.
- There is an up-to-date inventory of all production access, including who has access to what and with what level of permissions.
- Access is reviewed periodically to detect outdated accounts or excessive permissions.
- Production SSH keys are associated with specific people and are rotated periodically.
Secrets and credentials
- No secrets (tokens, API keys, passwords, private keys) are in the source code or in the git history.
- Production secrets reside in environment variables or in a dedicated manager (AWS Secrets Manager, HashiCorp Vault or other equivalent).
- No secrets appear in logs, environment dumps, crash reports, or error messages.
- Production secrets and staging/development secrets are different (they are not reused between environments).
- All production credentials can be rotated quickly without the need for an emergency deployment.
- Each secret has a limited scope: it only accesses the resource it needs, with the minimum permissions.
- There is a documented process for how to act when a secret is exposed (rotation, notification, impact assessment).
- The .env file or equivalent is in the .gitignore and is never uploaded to the repository.
Network and infrastructure exhibition
- Public entry points (domains, ports, endpoints) are deliberate, documented, and reviewed.
- Internal services (databases, queues, caches) are not accessible from the internet.
- The Django (or equivalent) admin panel is restricted by IP, VPN, or both.
- Open ports on production servers are justified and documented (at least HTTP/HTTPS and SSH).
- There are no old domains, test subdomains, or DNS records pointing to unverified live systems.
- The staging and development environments are not publicly accessible without authentication.
- The firewall configuration (security groups on AWS, iptables/ufw on VPS) has been verified with an external scan.
- The SSL/TLS certificate is configured correctly and is renewed automatically.
- HTTP → HTTPS redirects are active on all production domains.
Application and code
- User input is validated and sanitized across all application routes (forms, API, uploads).
- Authentication and authorization are separate controls: being logged in does not mean having permission for everything.
- Error paths do not reveal stack traces, internal configuration, server paths, or sensitive data.
- DEBUG = False in production (verified, not just assumed).
- CSRF protection is active on all forms and endpoints that modify data.
- The security headers are configured: Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Strict-Transport-Security.
- File uploads are validated by type and size, and are stored outside the public web directory.
- Python dependencies (requirements.txt or pyproject.toml) are audited periodically using pip-audit, Dependabot or equivalent.
- Flutter dependencies (pubspec.yaml) are also reviewed for known vulnerabilities.
- Artifacts deployed in production can be traced back to a specific commit in the repository.
- There are no debug, test, or development endpoints accessible in production.
Deployments and operation
- Production deployments follow a controlled and reproducible path through the CI/CD pipeline.
- The pipeline runs automated tests before each deployment (no code reaches production without passing the tests).
- There is a documented alternative for deployment if the main pipeline is unavailable.
- Every change in production is attributable: who did it, when, and what changed.
- The rollback to the previous version works and has been tested at least once.
- Deployments do not require direct SSH access to the server under normal conditions.
- Configuration changes in production (environment variables, DNS, firewall rules) are also logged and traceable.
- There is a clear criterion to distinguish when an incident is resolved with a quick fix and when it requires a rollback.
Backups and recovery
- Database backups are run automatically as frequently as the business needs (RPO defined and documented).
- The maximum acceptable system downtime is defined (documented RTO).
- Backups are stored in a separate location from the production server (another AWS region, another provider, or external storage).
- The integrity of the backups is verified automatically (it is not enough for the job to say "OK").
- The last real test of restoration was in the last three months and it was successful.
- The credentials and artifacts needed for restoration are available outside of the affected systems (if the server is lost, it can still be restored).
- There is a restoration runbook with concrete steps that any team member can follow under pressure.
- Backups cover not only the database, but also user-uploaded files, configuration, and any data not in the repository.
- It has been verified that the backup and restoration meet the times defined in RTO and RPO.
Monitoring and alerts
- There is an automatic health check that verifies the availability of the application at least every minute.
- Basic metrics are monitored: availability, error rate, and resource saturation.
- Application errors are automatically captured with Sentry or equivalent, with sufficient context to diagnose them.
- The alerts are linked to actions: each one has an expected and documented response.
- There are at least two alert levels: urgent (immediate notification) and informative (review during working hours).
- The signal-to-noise ratio is sufficient for the team to trust the alerts and act when they are triggered.
- Security-relevant events are visible: repeated authentication failures, unusual administrative actions, privilege changes, and backup errors.
- The logs are centralized and can be queried without needing to connect via SSH to each server.
- The first few minutes of an incident are structured: who investigates, where to look first, and how to escalate.
Data, privacy and legal obligations
- The system can identify where each user's data resides (database, files, logs, backups, third-party services).
- There is an operational process to delete a user's data upon request, within the timeframes required by the GDPR.
- Exporting user data does not require manual surgery on the database.
- Retention periods are defined for business data, application logs, access logs, and backups.
- Production data is not copied to development or staging environments without being anonymized.
- Third parties that process user data (payment gateways, email services, analytics, infrastructure providers) are identified and documented.
- A cookie policy is in place and verified across all public domains.
- Forms that collect personal data inform the user how that data will be processed.