Security Is Not Optional Engineering
Many engineers treat security as a feature to be added later — something the “security team” handles after the real engineering is done. This is exactly wrong. Security is a structural property of a system, like load-bearing capacity in a building. You can’t bolt it on after construction; it must be designed in from the start.
Consider: if your simulation platform processes proprietary structural designs for a client, a data breach doesn’t just violate a regulation — it exposes intellectual property that took years and millions of dollars to develop. If your bridge monitoring system can be tampered with, people could die. Security is not an IT concern. It is an engineering concern.
The Security Mindset
The security mindset means thinking about how a system can be misused, not just how it should be used. It means asking: “What happens if someone deliberately tries to break this?”
Threat Sources
| Threat Source | Description | Example in Engineering Context |
|---|---|---|
| External attackers | Hackers, competitors, nation-states | Ransomware targeting simulation infrastructure |
| Insider threats | Disgruntled employees, contractors | Engineer exporting proprietary FEM models before leaving |
| Accidental exposure | Misconfiguration, human error | S3 bucket with client designs left publicly accessible |
| Supply chain attacks | Compromised dependencies, tools | Malicious code in a Python package used for post-processing |
The STRIDE Threat Model
STRIDE is a systematic framework for identifying security threats. Developed at Microsoft, it categorizes threats into six types:
| Threat | Definition | Engineering Example | Mitigation |
|---|---|---|---|
| Spoofing | Pretending to be someone else | Attacker submits jobs as a legitimate client | Strong authentication (MFA, JWT) |
| Tampering | Modifying data or code | Altering simulation input parameters in transit | Integrity checks, signed payloads, HTTPS |
| Repudiation | Denying an action was taken | Client claims they never approved a design change | Audit logging, tamper-proof logs |
| Information disclosure | Exposing data to unauthorized parties | Simulation results visible to competing firms | Encryption at rest and in transit, access controls |
| Denial of service | Making a system unavailable | Flooding the job queue with fake simulations | Rate limiting, input validation, auto-scaling |
| Elevation of privilege | Gaining unauthorized access levels | Intern account gains admin access to all projects | Least privilege, role-based access control |
Core Security Principles
1. Least Privilege
Every component, user, and process should have the minimum permissions necessary to do its job — and no more.
Example: A simulation worker process needs to read input files from S3 and write results back. It does not need to delete files, modify IAM policies, or access the billing dashboard.
# BAD: Worker has full S3 access
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*"
}
# GOOD: Worker has minimum necessary permissions
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::simulation-inputs/*"
},
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::simulation-results/*"
}
Least privilege applies at every level: database users, API keys, IAM roles, file system permissions, network access. The principle is simple but requires discipline — it’s always easier to grant * and move on.
2. Input Validation: Never Trust User Input
This is the single most important security principle for developers. Every piece of data that enters your system from outside — user input, API calls, file uploads, query parameters — is a potential attack vector.
SQL Injection Example:
# BAD: String concatenation in SQL query
def get_simulation(sim_id):
query = f"SELECT * FROM simulations WHERE id = '{sim_id}'"
cursor.execute(query)
return cursor.fetchone()
# If sim_id = "'; DROP TABLE simulations; --"
# The query becomes:
# SELECT * FROM simulations WHERE id = ''; DROP TABLE simulations; --'
# Your entire simulations table is deleted.
# GOOD: Parameterized query
def get_simulation(sim_id):
query = "SELECT * FROM simulations WHERE id = %s"
cursor.execute(query, (sim_id,))
return cursor.fetchone()
# The database driver treats sim_id as DATA, not SQL code.
# No injection is possible regardless of what sim_id contains.
Common injection vectors:
| Vector | Attack | Defense |
|---|---|---|
| SQL queries | SQL injection | Parameterized queries, ORM |
| HTML output | Cross-site scripting (XSS) | Output encoding, CSP headers |
| File paths | Path traversal (../../etc/passwd) | Validate resolved paths, chroot |
| Shell commands | Command injection | Avoid shell=True, use subprocess with list args |
| Deserialization | Remote code execution | Never deserialize untrusted data (avoid pickle) |
Validation principles:
- Validate on the server. Client-side validation is for UX, not security. It can be bypassed trivially.
- Whitelist, don’t blacklist. Define what’s allowed, not what’s forbidden. Attackers are more creative than your blacklist.
- Validate type, length, format, and range. A simulation ID should be a UUID. If it’s not, reject it immediately.
- Fail closed. If validation fails, deny the request. Don’t try to “fix” the input.
3. Authentication and Authorization
Authentication (AuthN) answers: “Who are you?” Authorization (AuthZ) answers: “What are you allowed to do?” These are separate concerns and must be implemented separately.
import jwt
from functools import wraps
from flask import request, jsonify
def require_auth(allowed_roles):
"""Decorator that handles both authentication and authorization."""
def decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
# --- Authentication: Who are you? ---
token = request.headers.get("Authorization", "").replace("Bearer ", "")
if not token:
return jsonify({"error": "No token provided"}), 401
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])
except jwt.ExpiredSignatureError:
return jsonify({"error": "Token expired"}), 401
except jwt.InvalidTokenError:
return jsonify({"error": "Invalid token"}), 401
# --- Authorization: What can you do? ---
user_role = payload.get("role", "")
if user_role not in allowed_roles:
return jsonify({"error": "Insufficient permissions"}), 403
# Attach user info to request context
request.current_user = payload
return f(*args, **kwargs)
return wrapper
return decorator
# Usage:
@app.route("/api/simulations", methods=["POST"])
@require_auth(allowed_roles=["engineer", "admin"])
def create_simulation():
"""Only engineers and admins can create simulations."""
# request.current_user is available here
...
@app.route("/api/admin/users", methods=["DELETE"])
@require_auth(allowed_roles=["admin"])
def delete_user():
"""Only admins can delete users."""
...
Notice the clear separation: a 401 response means “we don’t know who you are” (authentication failure), while a 403 means “we know who you are, but you’re not allowed to do this” (authorization failure). These are different problems requiring different solutions.
4. Secrets Management
Secrets — API keys, database passwords, encryption keys — are the most common source of security breaches. The rule is simple: secrets never go in code.
# BAD: Hardcoded secrets
DB_PASSWORD = "super_secret_password_123"
AWS_ACCESS_KEY = "AKIAIOSFODNN7EXAMPLE"
# BETTER: Environment variables
import os
DB_PASSWORD = os.environ["DB_PASSWORD"]
AWS_ACCESS_KEY = os.environ["AWS_ACCESS_KEY_ID"]
# BEST: Secrets manager with rotation
import boto3
def get_secret(secret_name):
"""Retrieve a secret from AWS Secrets Manager."""
client = boto3.client("secretsmanager")
response = client.get_secret_value(SecretId=secret_name)
return json.loads(response["SecretString"])
db_config = get_secret("production/database")
connection = psycopg2.connect(
host=db_config["host"],
password=db_config["password"],
database=db_config["dbname"],
)
detect-secrets and gitleaks automate this. One leaked secret in a public repository can be exploited within minutes — automated bots scan GitHub continuously for exposed credentials.
Ethics and Professional Responsibility
As engineers who write software, you bear responsibility beyond code correctness. Software systems affect people, and the decisions you make have consequences.
Honest Representation
When your simulation shows a bridge design is borderline, you don’t adjust the safety factor to make the numbers work. The same principle applies to software: don’t misrepresent system capabilities, performance, or reliability. If your system has a known failure mode, document it. If your model has limitations, state them.
Open-Source Licensing
Using open-source software comes with obligations. Licenses are not suggestions — they are legal contracts:
| License | Can you use in commercial product? | Must you share your source code? | Must you include the license? |
|---|---|---|---|
| MIT | Yes | No | Yes |
| Apache 2.0 | Yes | No | Yes (with NOTICE) |
| GPL v3 | Yes, but… | Yes, if distributed | Yes |
| AGPL v3 | Yes, but… | Yes, even for SaaS | Yes |
If you include a GPL library in your simulation platform, you may be legally required to open-source your entire platform. This is not a hypothetical risk — it’s an active area of litigation.
Responsible AI Use
When using AI to generate code, you are responsible for that code. “The AI wrote it” is not a defense for a security vulnerability, a license violation, or incorrect results. Review AI-generated code with the same rigor you’d apply to code from a junior developer — capable but not always right.
Data Privacy
Engineering data often contains sensitive information: proprietary designs, client project details, personnel information. Regulations like GDPR, CCPA, and industry-specific standards (ITAR for defense, HIPAA for health-related structures) impose legal obligations. The key principles:
- Collect only what you need. Don’t store data “just in case.”
- Encrypt sensitive data at rest and in transit.
- Define retention policies. Data you don’t have can’t be breached.
- Implement access controls. Not everyone needs to see everything.
- Plan for data subject requests. Can you export or delete a specific client’s data?
Exercise 17.1: STRIDE Threat Model for a Simulation API
System description: A REST API that allows engineering firms to submit structural simulation jobs. The system includes:
- REST API with JWT authentication
- S3 bucket for input/output files
- PostgreSQL database for job metadata and user accounts
- Email notifications when jobs complete
For each STRIDE category, identify:
- At least one specific threat (not generic — specific to this system)
- The impact if the threat is realized (data loss, financial, safety, reputational)
- A concrete mitigation (not “use encryption” — specify what, where, and how)
Present your analysis as a table with columns: STRIDE Category, Specific Threat, Impact, and Mitigation.
Stretch goal: Draw a data flow diagram of the system and mark the trust boundaries (points where data crosses from a trusted zone to an untrusted zone, or vice versa). Every trust boundary is a place where STRIDE analysis is most critical.
Quiz
Question: Your simulation API accepts a filename parameter to retrieve result files. A user sends the request:
GET /api/results?filename=../../../etc/passwd
What type of vulnerability is this, and what is the correct defense?
- SQL injection; use parameterized queries.
- Cross-site scripting; encode the output.
- Path traversal; validate that the resolved file path stays within the allowed results directory.
- Denial of service; rate-limit the API.
Answer
c) Path traversal; validate that the resolved file path stays within the allowed results directory.
The attacker is using ../ sequences to navigate up the directory tree and access files outside the intended results directory. The defense is to resolve the full path (using os.path.realpath() or equivalent) and verify that the result starts with the allowed base directory. For example:
import os
RESULTS_DIR = "/var/data/simulation-results"
def get_result_file(filename):
requested_path = os.path.realpath(
os.path.join(RESULTS_DIR, filename)
)
if not requested_path.startswith(RESULTS_DIR):
raise PermissionError("Access denied: path traversal detected")
return open(requested_path, "r")
This is not SQL injection (no database query), not XSS (no HTML output), and not DoS (the goal is data exfiltration, not service disruption).