Repository navigation
security_setup
This document covers every security measure in place across the Mealchemy repository, from the CI pipeline through to the running containers and the application layer. These are not optional guidelines, they are enforced either by CI failing or by the architecture itself preventing the violation.
Every pull request is scanned for accidentally committed secrets using gitleaks. It scans the entire git history, not just the latest commit. This means if a password was committed three weeks ago and then removed, gitleaks will still find it.
The scan runs as the first job in ci.yml before any tests execute. If a secret is detected the pipeline fails immediately and nothing else runs.
Secrets that are caught include API keys, database passwords, JWT secrets, tokens, and private keys. The scan covers all file types including config files, .env files that were accidentally staged, and source code.
The .env and .secrets files are both listed in .gitignore. They must never be committed under any circumstances.
Every uses: action in every workflow file is pinned to a full SHA commit hash rather than a version tag. For example:
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11instead of:
uses: actions/checkout@v4Version tags like v4 are mutable. A compromised maintainer or a supply chain attack can redirect v4 to point at malicious code, and every repository using that tag silently runs the attacker's code on the next pipeline run. A SHA hash is immutable. The exact code that runs in CI today is identical to what will run in six months unless someone deliberately changes the hash in the workflow file.
This applies to every action across ci.yml, deploy-design-spec.yml, wiki-sync.yml, and pr-labeler.yml.
Both Docker images (mealchemy-backend and mealchemy-engine) are scanned with Trivy on every pull request as part of the docker-security-scan job. Trivy scans:
- OS packages installed in the image
- Application dependencies (Maven packages for the backend, pip packages for the engine)
- Known CVEs in the base images themselves
The job runs as a matrix so both images are scanned in parallel. It fails the build on any HIGH or CRITICAL severity vulnerability. Medium and low severity findings are reported but do not fail the build.
If Trivy flags a vulnerability, the fix is to update the affected dependency or base image and push a new commit. Do not suppress Trivy findings without a documented reason.
Both Docker images run the application as a non-root system user named mealchemy. The user is created inside the Dockerfile and the application process is started as that user.
Running containers as root means a process that escapes the container has full root access to the host machine. Running as a non-root user limits this. If any container escapes or application exploits this, it is limited to only what the mealchemy user is allowed to do, which is nothing outside the application directory.
This applies to both the Spring Boot backend container and the Python engine container.
Every service in docker-compose.yml has explicit CPU and memory resource limits. This prevents a single leaking or runaway container from consuming all available resources on the host machine and taking down the entire local stack.
This is a defence-in-depth measure. A memory leak in the backend or the engine cannot cascade into killing the Postgres container or starving the host OS of resources.
No password, secret key, API key, connection string, or token is hardcoded anywhere in the codebase. All credentials are injected at runtime through environment variables.
| Environment | How credentials are injected |
|---|---|
| Local Docker |
.env file passed via --env-file .env
|
| Local act CI |
.secrets file loaded via --secret-file .secrets
|
| GitHub Actions CI | GitHub repository Secrets |
| Render (production) | Render environment variable dashboard |
The ${VAR:fallback} syntax in application.properties means the app uses the environment variable if set and falls back to a local default otherwise. The local defaults only work on localhost and are useless in any other environment.
Violating this rule is treated as a critical security incident where keys will be rotated if violated.
All protected API endpoints require a valid JWT token. The token is issued on login and must be sent with every subsequent request in the Authorization: Bearer <token> header.
Key properties of the JWT implementation:
| Property | Value |
|---|---|
| Algorithm | HS256 |
| Expiry | 24 hours |
| Secret key | Minimum 32 characters, stored in environment variable only |
| Payload contents |
userId and expiry only |
The JWT secret is never committed to source code. It is read from the JWT_SECRET environment variable at runtime.
The token payload contains only the userId and expiry time. No roles, emails, or other user data are stored in the token. Roles are fetched fresh from the database on each authenticated request, which means a role change takes effect immediately without waiting for a token to expire.
The JwtAuthFilter runs before every controller method. It reads the Authorization header, validates the signature and expiry, and registers the authenticated user with Spring Security. If the token is missing, malformed, or expired, the request is not blocked at the filter level but Spring Security will reject it at the endpoint if authentication is required.
User passwords are hashed using BCrypt with a cost factor of 12 before being stored in the database. Plaintext passwords are never stored anywhere.
BCrypt cost factor 12 means the hashing operation is intentionally slow, making brute-force and rainbow table attacks computationally expensive. The higher the cost factor, the slower each guess attempt is.
Password hashes are excluded from every DTO and every API response. The passwordHash field never leaves the backend.
The Spring Boot backend is fully stateless. Authentication state lives entirely in the JWT token. There are no server-side sessions, no in-memory user state, and no session cookies.
Spring Security is configured with SessionCreationPolicy.STATELESS which disables session creation entirely. CSRF protection is also disabled because CSRF attacks target session-based authentication and are not applicable to token-based stateless APIs.
ddl-auto=validate is set in application.properties. JPA never creates, alters, or drops tables. Flyway runs migrations. Hibernate only checks that entity classes match the schema at startup.
Setting ddl-auto=create or ddl-auto=update is not enabled. These modes let Hibernate modify the database schema, which diverges the schema from the migration history and makes the database impossible to reproduce reliably.
Password hashes, internal flags, and soft-delete timestamps are never included in any API response. The DTOs that cross the network boundary are the only thing Flutter ever sees.
The least privilege principle is applied at every layer:
| Layer | How it is applied |
|---|---|
| Docker containers | Run as non-root mealchemy user |
| Database user | No superuser, only required permissions |
| GitHub Personal Access Token for CI | Contents: Read-only |
| JWT payload | Only userId and expiry, no sensitive data |
| API responses | DTOs only, not raw entities |
| Engine | No database access, API calls only |
· Team PUL5E · COS 301 · University of Pretoria · 2026 ·
Team PUL5E · COS 301
Demo 4
-
Software Requirements Specifications (SRS)
-
Software Architecture Specifications (SAS)
Demo 3
-
Software Requirements Specifications (SRS)
-
Software Architecture Specifications (SAS)
Demo 2
-
Software Requirements Specifications (SRS)
-
Software Architecture Specifications (SAS)
Demo 1
- Docker Setup
- CI Setup
- ACT Setup
- pgAdmin Setup
- Security Setup
- Render Setup
- Design Spec Deployment
- Wiki Submodule
Updated: 13 September 2026