Harden Your Deployment¶
Security hardening, production logging, and runtime constraints for containerised cosalette applications.
Prerequisites
A working containerized application and a running Compose deployment.
Logging¶
Use JSON format in containers¶
Set --log-format json (or the MYAPP_LOGGING__FORMAT=json env var) for structured
NDJSON output. This is the recommended format for containerised deployments:
{"timestamp":"2026-03-05T10:15:30.123Z","level":"INFO","message":"Connected to broker","host":"mosquitto","port":1883}
Docker's default json-file log driver wraps each line in its own JSON envelope, so
structured log lines are preserved as single entries.
Let Docker handle log rotation¶
In a container, do not set MYAPP_LOGGING__FILE — write to stdout/stderr and let
the Docker daemon manage rotation:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
This applies globally to all containers. You can also set logging: per-service in
docker-compose.yml:
services:
myapp:
# ...
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
Redact secrets from logs and error payloads¶
Exception text and log messages can embed tokens, MAC addresses or URLs with
credentials. Pass redact= to App to scrub them from every line the app
logs and from disclosed error messages on the broker:
import re
app = cosalette.App(
"myapp",
redact=[r"token=[^&\s]+", re.compile(r"([0-9A-F]{2}:){5}[0-9A-F]{2}", re.I)],
)
Each match becomes [REDACTED]. Redaction is best effort: a broken redactor
lets the text through and logs one WARNING, so keep secrets out of exception
messages in the first place and disclose only the error types you need
(see Error Handling).
Log aggregation¶
For fleet-wide observability, forward container logs to a centralised system. Grafana Loki with Promtail is a lightweight option that works well on Raspberry Pi hardware. Configure the Loki Docker logging driver or run Promtail as a sidecar container that tails the Docker log files.
Viewing logs¶
# Last 20 log entries
docker logs --tail 20 myapp
# Follow live output
docker logs -f myapp
# Filter structured logs with jq
docker logs myapp 2>&1 | jq 'select(.level == "ERROR")'
Docker Hardening¶
The reference Dockerfile in Containerize Your Application includes baseline hardening: non-root user, minimal runtime image, no shell entrypoint, immutable venv. For production IoT deployments, consider these additional measures.
Image scanning¶
Scan built images for vulnerabilities before deploying them:
# Scan with Trivy (local)
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy:0.59.2 \
image --severity HIGH,CRITICAL \
myapp:latest
# Scan with Grype (alternative)
grype myapp:latest
CI integration: The cosalette devcontainer image is scanned weekly with Trivy.
Adapt .github/workflows/devcontainer-build.yml for your own application images.
Runtime security¶
- Read-only root filesystem: Add
read_only: trueto the Compose service. If the app writes to/app/data, mount it as a writable volume. - Drop capabilities: Add
cap_drop: [ALL]to strip Linux capabilities unless your app genuinely needs raw sockets, privileged ports, or device access. - No new privileges: Add
security_opt: ["no-new-privileges:true"]. - User namespace remapping: Enable Docker's
userns-remapso container root (UID 0) maps to an unprivileged UID on the host. The reference Dockerfile already runs as UID 1000, so this is defense-in-depth if a container escape occurs.
Example hardened Compose service:
services:
myapp:
image: myapp:latest
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- app-data:/app/data # writable volume for persistence
tmpfs:
- /tmp # ephemeral tmpfs for scratch space
environment:
# ... (same as before)
Device access and capabilities
If your app binds raw sockets or accesses hardware, you may need to selectively
add back capabilities like CAP_NET_RAW or CAP_SYS_ADMIN. Test thoroughly —
hardening that breaks functionality is worse than no hardening.
Pinning base images¶
The reference Dockerfile uses mutable tags (python:3.14-slim) for developer
convenience. For production, pin to a full image digest:
Update the digest when Dependabot or Renovate opens a PR for a new base image. This prevents supply-chain attacks where an attacker compromises a mutable tag.
Network isolation¶
Run each app in its own Docker network or limit communication with network policies. The reference Compose file already keeps the broker and app in a shared network — external services have no direct access unless you explicitly publish ports.
Secrets management¶
Avoid embedding credentials in environment variables or the image. Use Docker secrets (Swarm) or mount secrets from a secure volume:
services:
myapp:
image: myapp:latest
# ...
environment:
MYAPP_MQTT__PASSWORD_FILE: /run/secrets/mqtt_password
secrets:
- mqtt_password
secrets:
mqtt_password:
file: ./secrets/mqtt_password.txt
Then update your app's Settings to load passwords from files when *_FILE env vars
are set. cosalette does not provide built-in _FILE support — implement it in your
app's Settings.__init__ or use a wrapper like pydantic-vault.
A minimal secure implementation:
import os
import stat
from pathlib import Path
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
mqtt_password: str = ""
def model_post_init(self, __context: object) -> None:
if path_str := os.environ.get("MYAPP_MQTT__PASSWORD_FILE"):
path = Path(path_str)
# Verify restrictive permissions (owner-read-only)
mode = path.stat().st_mode
if mode & (stat.S_IRGRP | stat.S_IROTH):
raise RuntimeError(f"Secret file {path} is world/group readable")
self.mqtt_password = path.read_text().strip()
Secret file hygiene
Always strip whitespace from file contents. Verify file permissions are 0600 or
stricter (chmod 600 ./secrets/mqtt_password.txt). Avoid logging secret values.
Related guides:
- Containerize Your Application — Dockerfile and multi-arch builds
- Deploy with Docker Compose — Compose configuration, health checks, persistence, and Ansible rollout
- Troubleshoot a Deployment — diagnosing common problems