Linux Update Management Server
Security principles, operational requirements, hardening status, and responsible handling of sensitive information in LUMS.
Version: 2.5 Project: LUMS Slogan: Linux Update Management without the noise.
LUMS follows a simple principle:
Centralized management does not mean centralized trust.
LUMS manages Linux clients, receives inventory information, and distributes update jobs.
The actual package installation takes place on the managed Linux client.
┌─────────────────────┐
│ LUMS Server │
│ │
│ Nginx │
│ Docker │
│ Gunicorn │
│ Flask │
│ Authentication │
│ Authorization │
│ Inventory │
│ Update Jobs │
└──────────┬──────────┘
│
HTTPS + Token
│
┌──────────▼──────────┐
│ Linux Client │
│ │
│ lums-agent │
│ execution watcher │
│ APT / dpkg │
└─────────────────────┘
LUMS security depends on multiple layers:
Network
↓
TLS
↓
Nginx
↓
Authentication
↓
Authorization
↓
Application
↓
Gunicorn
↓
Docker
↓
Database
↓
Operating System
↓
APT / dpkg
A weakness in one layer must never be used as a reason to disable another security layer.
This document covers:
- Docker deployment security
- Gunicorn application serving
- Database initialization
- Server authentication
- Client authentication
- Authorization
- TLS certificates
- Secret management
- SQLite database protection
- Nginx configuration
- systemd agent services
- Execution watcher security
- Update execution
- Logging
- Backup protection
- Incident handling
- Security testing
- Security maintenance
- Current security limitations
- Remaining hardening work
This document does not replace the official security documentation of:
- Ubuntu
- Debian
- Docker
- Python
- Flask
- Gunicorn
- Nginx
- SQLite
- APT
- dpkg
- systemd
The current LUMS deployment uses:
- Flask inside a Docker container
- Gunicorn as the WSGI application server
- SQLite in a persistent Docker volume
- Nginx as an HTTPS reverse proxy
- A Linux reporting agent
- A separate execution watcher
- Bearer-token authentication
- systemd timers for scheduled execution
- A root-managed mounted Flask secret
Client
│
│ HTTPS :443
▼
Nginx
│
│ HTTP localhost
▼
127.0.0.1:5050
│
│ Docker port mapping
▼
Docker container :5000
│
▼
Gunicorn
│
├── Worker 1
└── Worker 2
│
▼
Flask application
│
├── Authentication
├── Authorization
├── Inventory
└── Update Jobs
│
▼
SQLite database
The Docker application is bound to localhost:
127.0.0.1:5050 → container port 5000
The application is not directly exposed as a network service on ports 5000 or 5050.
The Flask secret is no longer supplied through the normal container environment.
Instead, production uses:
Host:
/etc/lums/secrets/lums_secret
│ read-only bind mount
Container:
/run/secrets/lums_secret
The application reads the secret through:
LUMS_SECRET_KEY_FILE=/run/secrets/lums_secret
The following controls have been implemented and tested:
| Security control | Status |
|---|---|
| Non-root Docker container | Verified |
| Drop all Linux capabilities | Verified |
| Read-only root filesystem | Verified |
| Writable application data only through persistent volume | Verified |
/tmp isolated through tmpfs |
Verified |
| Secret isolation | Verified |
| Protected mounted Flask secret | Verified |
| Secret mount read-only | Verified |
| Production Flask secret rotation | Verified |
| Gunicorn deployment | Verified |
| HTTPS reverse proxy | Verified |
| Security headers | Verified |
| Client authentication | Verified |
| Client token lifecycle | Verified |
| Client token rotation | Verified |
| Interrupted-job recovery | Verified |
| SQLite-aware backup | Verified |
| Backup integrity verification | Verified |
| Production frontend deployment | Verified |
| Frontend token rotation workflow | Verified |
Remaining hardening:
Update execution hardening
Full backup / restore test
Automated security tests
Final security review
The LUMS application runs as a dedicated non-root user.
The container user is:
lums
UID 10001
The Docker image must not run as root.
Verify:
sudo docker inspect lums \
--format 'User={{.Config.User}}'Expected:
User=lums
The application data directory is:
/var/lib/lums
The persistent Docker volume is:
lums-data
The LUMS application does not require Linux capabilities.
Production therefore uses:
--cap-drop=ALLVerify:
sudo docker inspect lums \
--format 'CapDrop={{json .HostConfig.CapDrop}}'Expected:
CapDrop=["ALL"]
The container must also not be privileged:
sudo docker inspect lums \
--format 'Privileged={{.HostConfig.Privileged}}'Expected:
Privileged=false
The application continues to function without additional Linux capabilities.
The production container uses:
--read-onlyThe root filesystem is therefore not writable.
Writable state is deliberately limited to:
/var/lib/lums
/tmp
The /tmp directory is provided through:
--tmpfs /tmp:rw,nosuid,nodev,noexecThe persistent database remains on:
lums-data:/var/lib/lums
Verify:
sudo docker inspect lums \
--format 'ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}}'Expected:
ReadonlyRootfs=true
The Flask secret is security-sensitive.
The secret must never be:
- committed to Git,
- documented in plaintext,
- printed to logs,
- included in screenshots,
- included in bug reports,
- passed through normal container environment variables in production.
Production uses:
/etc/lums/secrets/lums_secret
The host secret directory is protected.
Expected:
/etc/lums/secrets
root:root
0700
The secret file is:
root:10001
0640
The container receives it through a read-only mount:
/etc/lums/secrets/lums_secret
↓
/run/secrets/lums_secret
The application is configured through:
LUMS_SECRET_KEY_FILE=/run/secrets/lums_secret
The normal environment variable:
LUMS_SECRET_KEY
must not contain the production secret.
Verify:
sudo docker exec lums sh -c '
if [ -n "${LUMS_SECRET_KEY:-}" ]; then
echo "PRESENT"
else
echo "ABSENT"
fi
'Expected:
ABSENT
Verify the file:
sudo docker exec lums sh -c '
if [ -r /run/secrets/lums_secret ]; then
echo "READABLE"
else
echo "NOT READABLE"
fi
'Expected:
READABLE
Secret isolation and secret rotation are separate controls.
The production Flask secret was previously exposed through diagnostic output.
The value is intentionally not reproduced.
The replacement procedure was:
Identify exposure
↓
Move secret outside normal environment
↓
Test rotation in isolation
↓
Generate replacement secret
↓
Replace protected secret file
↓
Restart LUMS
↓
Verify old sessions invalidated
↓
Verify new authentication
↓
Remove temporary old-secret backup
Production secret rotation was successfully completed.
The previous secret is no longer active.
A Flask secret change invalidates existing sessions.
This behavior was explicitly tested before production rotation.
LUMS uses administrator authentication for the web interface.
Passwords are protected using Argon2.
Client authentication uses Bearer tokens.
Example:
Authorization: Bearer <CLIENT_TOKEN>
The server hashes the supplied token before comparing it against the stored client token hash.
Client tokens are generated cryptographically.
The current token generation uses:
secrets.token_urlsafe(32)The token is never intentionally stored in plaintext.
Client authentication is performed server-side.
The authentication flow is:
Client
│
│ Authorization: Bearer <token>
▼
LUMS API
│
▼
Hash supplied token
│
▼
Lookup client token hash
│
▼
Check enabled
│
▼
Check token revocation state
│
▼
Authenticated client
Protected endpoints use the authenticated client identity.
The client ID supplied by a request must not override the identity established by authentication.
Client tokens are treated as credentials.
The current lifecycle includes:
Generate
↓
Hash
↓
Store hash
↓
Authenticate
↓
Rotate
↓
Invalidate old token
↓
Issue replacement
↓
Update agent
↓
Verify communication
The plaintext token is returned only during token creation or rotation.
The token is not written to the audit log.
Administrative token rotation is implemented through:
POST /api/clients/<client_id>/token/rotate
The endpoint requires:
Administrator session
+
CSRF validation
The endpoint:
- Finds the client.
- Generates a new cryptographically random token.
- Hashes the token.
- Replaces the stored token hash.
- Updates
token_created_at. - Clears the previous revocation timestamp.
- Creates an audit event.
- Returns the new token once.
The previous token becomes invalid immediately.
The rotation endpoint rejects:
No administrator session
with:
401
A missing or invalid CSRF token is rejected with:
400
An unknown client returns:
404
Only a valid administrator session with valid CSRF protection can rotate a client token.
Successful token rotation produces an audit event:
action:
client.token.rotate
target:
client:<CLIENT_ID>
result:
success
The audit record may contain non-sensitive client information such as:
hostname=<HOSTNAME>
The actual token is never stored in the audit event.
The audit log therefore records:
that a rotation happened
rather than:
what the new token was
The client detail page provides:
🔐 Token rotieren
Before rotation, the administrator receives a confirmation dialog.
The warning explains:
- the previous token becomes invalid,
- the LUMS agent must be updated,
- the operation continues only after confirmation.
After successful rotation:
Neuer Client-Token
is displayed.
The interface explicitly warns:
Dieser Token wird nur jetzt angezeigt.
The token can be copied using:
📋 Token kopieren
The token is not stored in the page as persistent application state.
The token rotation lifecycle was tested in an isolated environment.
Verified:
Token A
↓
authenticated
↓
Rotate
↓
Token B
Then:
Token A → 401
Token B → 200
The test also verified:
No admin session
→ 401
Admin session without CSRF
→ 400
Unknown client
→ 404
The audit event was created successfully.
The token value was not written to the audit log.
The frontend was tested for:
Rotation confirmation
Token display
Copy-to-clipboard
Successful rotation
After the token lifecycle was tested, the production agent was updated with the replacement client token.
The production agent subsequently reported successfully:
✓ REPORT ACCEPTED
✓ CLIENT AUTHENTICATED
✓ NO UPDATE JOB
LUMS // Agent cycle complete.
status=0/SUCCESS
The client appeared online again in the LUMS interface.
This confirms:
Token rotation
↓
Agent reconfiguration
↓
Authentication
↓
Inventory reporting
Authentication answers:
Who are you?
Authorization answers:
What are you allowed to access?
LUMS validates authenticated client ownership for client-specific job operations.
A client cannot simply provide another client ID and access that client's job.
Protected job operations verify the relationship between:
Authenticated client
+
Requested job
+
Owning client
Job claiming is performed atomically.
This prevents multiple agents from claiming the same job under normal concurrent execution.
The claim operation uses a conditional state transition.
Conceptually:
pending
↓
running
Only the client that successfully claims the job can continue execution.
Job result reporting validates that the authenticated client owns the job.
A client must not be able to submit a result for another client's job.
The server therefore checks:
authenticated_client.id
==
job.client_id
before accepting the result.
An update job can become stuck in:
running
if the agent terminates unexpectedly.
LUMS provides controlled recovery.
The recovery endpoint is:
POST /api/update-jobs/<job_id>/abandon
The operation:
- verifies the job exists,
- verifies client ownership,
- only permits recovery from
running, - marks the job
abandoned, - records
finished_at, - records a recovery reason,
- writes update history,
- preserves package statistics,
- prevents accidental new job execution when recovery fails.
Recovered jobs use:
Agent did not submit a final result.
This distinguishes:
success
failed
abandoned
rather than incorrectly representing an interrupted job as successful.
Recovery uses a conditional state transition.
If another operation has already changed the job state, the recovery operation returns a conflict rather than overwriting the newer state.
Conceptually:
running
│
├── agent result
│
└── recovery
Only one valid transition should win.
A controlled artificial job was created.
The agent encountered the running job and initiated recovery.
Verified:
status:
abandoned
finished_at:
populated
recovery_reason:
Agent did not submit a final result.
update_history:
abandoned
package count:
preserved
successful:
0
failed:
0
The artificial test data was removed afterwards.
A real update job was subsequently executed successfully.
This confirmed that recovery did not break normal job execution.
The LUMS agent communicates with the server through HTTPS.
The agent configuration includes:
LUMS_BASE
LUMS_TOKEN
LUMS_CA
The agent uses the configured CA for TLS verification.
TLS verification must not be disabled merely to work around certificate problems.
The agent token must be protected as a credential.
The agent configuration is stored outside the Git repository.
Example:
/etc/default/lums-agent
Sensitive values must not be committed.
Permissions should restrict unauthorized access.
The agent should run under its intended system account and only receive the permissions required for its functions.
The execution watcher is separate from normal inventory reporting.
The separation provides:
Reporting
≠
Execution supervision
The watcher is responsible for detecting and handling execution state.
It must not bypass:
- authentication,
- authorization,
- job ownership,
- execution state,
- audit requirements.
Package installation is performed on the client.
The client uses:
APT
dpkg
Package management is privileged and therefore represents a security-sensitive execution boundary.
LUMS must avoid uncontrolled concurrent package operations.
The current execution lock reduces collisions within LUMS-controlled operations.
However, complete collision prevention against arbitrary manually started package-manager processes is not yet fully implemented.
This remains a hardening task.
Update execution should:
- run only authorized jobs,
- validate package information,
- preserve job ownership,
- record execution results,
- handle failures explicitly,
- detect interrupted jobs,
- avoid unnecessary reboot operations,
- maintain an auditable history.
The agent must never treat an invalid or unauthenticated job as executable.
A reboot should only occur when the update operation explicitly indicates that it is required.
LUMS should not automatically reboot a client merely because packages were installed.
The job result must distinguish:
reboot required
from:
reboot not required
Nginx is the external HTTPS entry point.
The application itself is not directly exposed.
Expected architecture:
Internet / LAN
│
▼
Nginx
:443 HTTPS
│
▼
127.0.0.1:5050
│
▼
Docker :5000
Nginx should only proxy to the local application binding.
HTTP is redirected to HTTPS.
Expected:
HTTP :80
↓
301
↓
HTTPS :443
Test:
curl -I http://127.0.0.1/or against the server's configured HTTP address.
The redirect prevents normal browser access from remaining on plaintext HTTP.
The application currently provides security headers including:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
Permissions-Policy:
camera=(),
microphone=(),
geolocation=(),
payment=()
The Content Security Policy includes:
default-src 'self'
script-src 'self'
style-src 'self'
img-src 'self' data:
font-src 'self'
connect-src 'self'
object-src 'none'
base-uri 'self'
frame-ancestors 'none'
form-action 'self'
These headers should remain present after deployment changes.
TLS should support:
TLS 1.2
TLS 1.3
Older protocols must remain disabled.
TLS certificates must contain the correct Subject Alternative Name.
Private key permissions must be restricted.
Test:
sudo nginx -tThen:
curl -k -I https://127.0.0.1/The application uses:
127.0.0.1:5050:5000
This means:
Host localhost:5050
↓
Container :5000
The service is not directly reachable through the host's LAN interface.
Do not replace it with:
0.0.0.0:5050:5000
unless the network architecture is intentionally redesigned and protected by an appropriate firewall or proxy layer.
Expected external services:
22/tcp
80/tcp
443/tcp
Application ports:
5000
5050
must remain internal.
Check:
sudo ss -lntpExpected application binding:
127.0.0.1:5050
LUMS uses SQLite.
The database is stored in:
/var/lib/lums/lums.db
The directory is backed by:
lums-data
The database must not be stored inside the Git repository.
The database must not be deleted as a troubleshooting shortcut.
SQLite integrity can be checked using:
sudo docker exec lums \
python3 -c '
import sqlite3
db = sqlite3.connect("/var/lib/lums/lums.db")
print(db.execute("PRAGMA integrity_check").fetchone()[0])
db.close()
'Expected:
ok
SQLite backups must use a SQLite-aware backup method.
Example:
sudo docker run --rm \
--entrypoint python3 \
-v lums-data:/var/lib/lums:ro \
-v /tmp:/backup \
lums:latest \
-c '
import sqlite3
source = sqlite3.connect("/var/lib/lums/lums.db")
target = sqlite3.connect("/backup/lums.db.backup")
with target:
source.backup(target)
target.close()
source.close()
print("SQLite backup completed")
'The temporary backup can then be moved into:
/var/backups/lums/
and protected:
sudo chown root:root /var/backups/lums/lums.db.backup
sudo chmod 600 /var/backups/lums/lums.db.backupA backup is not considered valid merely because the file exists.
Verify:
sudo python3 - <<'PY'
import sqlite3
path = "/var/backups/lums/lums.db.backup"
db = sqlite3.connect(path)
print(
"integrity =",
db.execute("PRAGMA integrity_check").fetchone()[0]
)
db.close()
PYExpected:
integrity = ok
Before the current frontend production deployment, a SQLite-aware backup was created:
/var/backups/lums/lums.db.backup-token-rotation
The backup was verified:
integrity = ok
users = 1
clients = 1
audit_log = 22
update_jobs = 1
update_history = 1
The backup was protected as:
root:root
0600
This backup provides a rollback point for the frontend deployment.
A full restore procedure must be tested separately in an isolated environment.
The intended restore flow is:
Verified backup
↓
Isolated LUMS environment
↓
Restore SQLite database
↓
Integrity check
↓
Application startup
↓
Authentication test
↓
Client test
↓
Job data verification
A full restore test remains outstanding.
Never commit:
Passwords
API tokens
Client tokens
TLS private keys
Environment files
Database files
SQLite backups
Session secrets
Personal data
Review:
git status
git diff
git diff --checkBefore committing.
The configured project identity is:
Name:
xxxxx
Email:
xxxxx
The production image should be built from reviewed source.
Before deployment:
sudo docker build \
-t lums:latest \
.Inspect:
sudo docker image inspect \
lums:latestVerify:
User=lums
The running container must use the tested image.
A successful image build alone does not prove that the application is secure or functional.
Current production deployment:
sudo docker run -d \
--name lums \
--restart unless-stopped \
--read-only \
--cap-drop=ALL \
--tmpfs /tmp:rw,nosuid,nodev,noexec \
-e LUMS_SECRET_KEY_FILE=/run/secrets/lums_secret \
-v /etc/lums/secrets/lums_secret:/run/secrets/lums_secret:ro \
-v lums-data:/var/lib/lums \
-p 127.0.0.1:5050:5000 \
lums:latestThis configuration provides:
Non-root
+
No Linux capabilities
+
Read-only root filesystem
+
Read-only secret mount
+
Persistent database volume
+
Localhost-only application binding
After production deployment:
sudo docker restart lumsThen verify:
sudo docker ps \
--filter "name=^lums$"Logs:
sudo docker logs \
--tail 100 \
lumsThe expected startup sequence includes:
LUMS database initialization
Starting Gunicorn
Starting gunicorn 23.0.0
Listening at 0.0.0.0:5000
Booting worker
No crash loop should occur.
The browser stores the selected LUMS frontend theme locally.
Theme selection is presentation-only.
The theme value:
lums-theme
does not affect:
- authentication,
- authorization,
- database state,
- update jobs,
- agent communication,
- client permissions.
The browser must not be treated as a trusted authorization source.
Logs may contain:
- HTTP requests
- application errors
- job identifiers
- client identifiers
- operational information
Logs must not contain:
- passwords
- client tokens
- Flask secrets
- TLS private keys
When sharing logs, sensitive values must be removed.
Only required services should be exposed.
Typical allowed ports:
22/tcp SSH
80/tcp HTTP redirect
443/tcp HTTPS
Check:
sudo ufw status verboseApplication ports must not be publicly exposed:
5000
5050
Firewall rules must be reviewed after network or deployment changes.
Before deployment:
cd /opt/lums-public
git status --short
git fetch origin
git log --oneline --decorate -3Update:
git pull --ff-only origin mainReview:
git diff --checkBuild:
sudo docker build -t lums:latest .Before recreation:
sudo docker ps
sudo docker volume inspect lums-dataCreate a verified SQLite backup before replacing a production container.
The current production deployment uses:
sudo docker run -d \
--name lums \
--restart unless-stopped \
--read-only \
--cap-drop=ALL \
--tmpfs /tmp:rw,nosuid,nodev,noexec \
-e LUMS_SECRET_KEY_FILE=/run/secrets/lums_secret \
-v /etc/lums/secrets/lums_secret:/run/secrets/lums_secret:ro \
-p 127.0.0.1:5050:5000 \
-v lums-data:/var/lib/lums \
lums:latestStop and remove only the container:
sudo docker stop lums
sudo docker rm lumsDo not remove:
lums-data
Verify:
sudo docker ps
sudo docker logs --tail 100 lums
sudo nginx -tTest HTTPS:
curl -k -I https://127.0.0.1/A successful Docker build does not prove that the complete deployment is working.
After container recreation verify:
1. Container running
2. Correct image
3. Correct localhost binding
4. Persistent volume still mounted
5. Database initialization runs once
6. Gunicorn starts
7. Two workers start
8. HTTPS returns expected response
9. Security headers remain present
10. Login remains accessible
11. Client reporting remains functional
12. Update jobs remain functional
13. Container runs as non-root
14. Root filesystem is read-only
15. All Linux capabilities are dropped
16. /tmp is available through tmpfs
17. Secret path is configured
18. Secret is not present as LUMS_SECRET_KEY
19. Secret mount is read-only
20. Container restart succeeds
Check:
sudo docker ps \
--filter "name=^lums$" \
--format "table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}"Check volume:
sudo docker inspect lums \
--format '{{range .Mounts}}{{.Name}} -> {{.Destination}} ({{.RW}}){{"\n"}}{{end}}'Expected database volume:
lums-data -> /var/lib/lums (true)
Check the secret mount:
sudo docker inspect lums \
--format '{{range .Mounts}}{{.Source}} -> {{.Destination}} RW={{.RW}}{{"\n"}}{{end}}'Expected:
/etc/lums/secrets/lums_secret -> /run/secrets/lums_secret RW=false
If a client token is compromised:
- Identify the affected client.
- Revoke or replace the token.
- Review recent client activity.
- Check server and application logs.
- Issue a new token.
- Update client configuration.
- Verify authentication.
- Document the incident.
Never continue using a known-compromised token.
If the server secret is compromised:
- Restrict access to the server.
- Review application logs.
- Generate a new secret.
- Replace the protected secret file.
- Restart the Docker container.
- Verify authentication and sessions.
- Review related credentials.
- Document the incident.
Changing the Flask secret may invalidate existing sessions.
The production secret should be rotated separately from normal secret-isolation deployment when possible so that session impact is understood and tested.
If the TLS private key is compromised:
- Replace the certificate and private key.
- Update trusted certificates on clients.
- Reload Nginx.
- Verify certificate validation.
- Review possible unauthorized access.
- Document the incident.
If a database or backup becomes exposed:
- Restrict access immediately.
- Determine which information was exposed.
- Review authentication-related data.
- Rotate affected credentials or tokens.
- Replace compromised backups if necessary.
- Review access logs.
- Document the incident.
The Flask secret was previously exposed through diagnostic output.
The value is intentionally not reproduced here.
The incident was handled by:
Identify exposure
↓
Move secret out of normal Docker environment
↓
Test secret rotation in isolation
↓
Generate replacement secret
↓
Replace /etc/lums/secrets/lums_secret
↓
Restart production LUMS
↓
Verify old session invalidation
↓
Verify new authentication
↓
Remove temporary old-secret backup
The replacement secret is now active in production.
The previously exposed Flask secret has been rotated and is no longer the active production secret.
- Docker container is running
- Docker volume is mounted
- Application binds only to localhost
- Port
5000is not externally exposed - Port
5050is not externally exposed - Nginx configuration passes validation
- HTTPS is enabled
- HTTP redirects to HTTPS
- TLS certificate contains the correct SAN
- Private key permissions are restricted
- Environment file permissions are restricted
- Firewall rules reviewed as required
- Flask development server removed
- Gunicorn 23.0.0 deployed
- Gunicorn workers start successfully
- Application import works
- Database initialization runs before Gunicorn
- Database initialization runs once per container start
- Gunicorn receives container signals correctly
- Administrator authentication works
- Invalid credentials are rejected
- Client token authentication is implemented
- Invalid client tokens are rejected
- Client tokens are not intentionally logged
- Protected endpoints require authentication
- Token rotation lifecycle implemented and verified
- Flask secret rotation completed and verified
- Client identity is authenticated server-side
- Client/job relationships are validated
- Atomic job claiming is implemented
- Job result ownership is validated
- Recovery ownership is validated
- Full administrative role model implemented
- Agent uses HTTPS
- TLS verification is configured
- CA certificate is available
- Agent configuration is protected
- Reporting timer is configured
- Watcher timer is configured
- Inventory reporting works
- Update job retrieval works
- Atomic job claiming works
- Job result reporting works
- Interrupted-job recovery tested
- Complete APT/dpkg collision prevention
- Database integrity can be checked
- SQLite-aware backups are implemented
- Backups are protected
- Backup integrity verification tested
- Restore procedure is documented
- Full restore test completed
- Database files are excluded from Git
- Backup files are excluded from Git
- Container is not privileged
- Application ports are localhost-only
- Persistent volume is used
- Container runs as non-root user
- Root filesystem is read-only
- All Linux capabilities are dropped
- Secret is removed from normal container environment
- Secret is supplied through protected mounted file
- Secret mount is read-only
- Production restart with secret-file architecture verified
- Secrets excluded from documentation
- Private keys excluded
- Tokens excluded
- Database files excluded
- Backup files excluded
- Changes reviewed before deployment
- Documentation uses placeholders
- Historical Flask secret exposure identified
- Replacement Flask secret generated and deployed
Secret isolation and production secret rotation are complete and verified.
The previous secret was treated as exposed, replaced with a newly generated value, and the production application was restarted and tested.
Changing the Flask secret invalidates existing sessions. This behavior was explicitly tested before production rotation and confirmed again after production deployment.
Client token rotation is implemented and verified.
Current lifecycle controls include:
- Cryptographically random token generation.
- SHA-256 hexadecimal digest storage.
- Bearer authentication.
- Enabled/revoked checks.
- Administrative rotation.
- Immediate invalidation of the previous token.
- CSRF protection for the administrative rotation endpoint.
- Audit logging without the plaintext token.
- One-time presentation of the replacement token.
- Production client communication after rotation.
Token expiration and a more advanced token-storage model remain possible future enhancements, but token rotation itself is no longer a pending hardening task.
The current idle detection uses:
w -h
It is primarily suitable for server, terminal, console, and SSH-oriented environments.
It is not a universal desktop idle detection mechanism.
Complete collision prevention between LUMS and arbitrary user-issued APT or dpkg commands is not fully implemented.
The LUMS execution lock does not automatically force every external package manager process to honor it.
LUMS currently has a single administrator-oriented authentication model.
A full role-based administrative authorization model has not yet been implemented.
SQLite is suitable for the current project scope and laboratory development.
Larger deployments may require a different database architecture depending on:
- Number of clients
- Concurrent requests
- Job volume
- Audit log size
- Backup requirements
- High availability requirements
Self-signed certificates require explicit trust configuration on clients.
They can be suitable for controlled laboratory environments but may not be appropriate for every deployment scenario.
The remaining hardening work should be performed incrementally.
The Flask secret is stored outside the normal Docker environment and mounted read-only into the container.
The previously exposed secret was replaced with a newly generated production secret.
Verified:
- Isolated rotation test.
- Old session invalidation.
- New authentication.
- Production restart.
- HTTPS operation.
- Database integrity.
- Client communication.
The secret value itself must never be documented.
Implemented and verified:
- Cryptographically random token generation.
- SHA-256 digest storage.
- Administrative token rotation.
- Immediate invalidation of the previous token.
- CSRF protection.
- Audit event without token disclosure.
- One-time token presentation.
- Agent reconfiguration and production reporting verification.
Possible future enhancements:
- Token expiration.
- Token identifiers.
- More advanced token storage appropriate to a larger deployment threat model.
Continue improving:
- APT/dpkg collision prevention
- Job timeouts
- Recovery handling
- Execution auditing
- Package-manager state detection
- Integration testing
Perform an isolated restore test using a verified production backup.
Test:
Backup
↓
Isolated restore environment
↓
SQLite integrity
↓
Security schema
↓
Application startup
↓
Authentication
↓
Client/job data
Only after a successful restore test should the backup/restore control be considered fully validated.
Automate regression tests for:
- Authentication
- Authorization
- Token handling
- Job ownership
- Job claiming
- Job recovery
- Security headers
- Container hardening
- Database integrity
- Secret handling
After the individual hardening phases are complete:
Inspect
↓
Test
↓
Verify
↓
Production
↓
Verify
↓
Document
The final review should confirm that the documented security state matches the actual deployment.
Security issues should be reported responsibly.
A security report should contain:
- Short description
- Affected component
- Reproduction steps
- Expected behavior
- Actual behavior
- Potential impact
- Suggested mitigation
- Relevant logs with secrets removed
Never include:
- Passwords
- Client tokens
- Private keys
- Server secrets
- Personal information
- Complete production databases
- Unredacted inventory data
Always redact sensitive information before sharing logs or screenshots.
Security reviews should be performed after:
- Application changes
- Authentication changes
- Authorization changes
- Docker changes
- Nginx changes
- Certificate changes
- Database schema changes
- Agent changes
- Watcher changes
- Deployment changes
- Secret changes
Regularly review:
Docker images
Operating system updates
Python dependencies
Flask dependencies
Gunicorn
Nginx configuration
TLS certificates
File permissions
Database backups
Git history
Authentication behavior
Authorization behavior
Job execution behavior
Package manager coordination
Container privileges
Container capabilities
Secret handling
Secret rotation
The current hardening state is:
[x] Non-root container
[x] Drop ALL capabilities
[x] Read-only root filesystem
[x] SQLite backup verification
[x] Interrupted-job recovery
[x] Production restart verification
[x] Secret isolation
[x] Protected mounted secret file
[x] Read-only secret mount
[x] Production restart after secret isolation
[x] Flask secret rotation
[x] Production secret rotation verification
[x] Client token lifecycle
[x] Client token rotation
[x] Client token revocation through rotation
[x] Client token rotation audit event
[x] Frontend token rotation workflow
[x] Production frontend deployment
[x] Production client communication after token rotation
[ ] Update execution hardening
[ ] Full backup / restore test
[ ] Automated security tests
[ ] Final security review
The following controls are therefore considered complete for the current implementation:
Container hardening
Secret isolation
Secret rotation
Client authentication
Client token rotation
Interrupted-job recovery
Production frontend deployment
The remaining work is intentionally separated into independent hardening phases:
Update execution hardening
↓
Full backup / restore test
↓
Automated security tests
↓
Final security review
The established workflow remains:
inspect
↓
design
↓
test
↓
verify
↓
production
↓
verify
↓
document
No security-sensitive change should be deployed blindly.
The following principles apply to LUMS:
- Never store secrets in Git.
- Never expose the Flask/Gunicorn application directly to the network.
- Use HTTPS for client communication.
- Keep TLS verification enabled.
- Separate authentication from authorization.
- Validate client identity server-side.
- Protect the Docker environment file.
- Protect client tokens.
- Protect TLS private keys.
- Protect the mounted Flask secret.
- Keep database backups secure.
- Do not remove persistent volumes during troubleshooting.
- Do not automatically reboot clients.
- Review changes before deployment.
- Test security-sensitive changes.
- Document incidents and configuration changes.
- Do not claim that incomplete security controls are fully implemented.
- Keep update execution controlled and auditable.
- Harden the container incrementally and test each change independently.
- Preserve persistent application data during frontend and container deployments.
- Treat secret isolation and secret rotation as separate controls.
- Treat previously exposed secrets as compromised until rotated.
- Rotate client tokens through the authenticated administrative workflow and verify the replacement token before closing the change.
- Keep production secrets outside Git and outside normal container environment variables where practical.
- Treat security hardening as a continuous process rather than a one-time configuration.
The production deployment was rebuilt from the tested frontend image after a verified SQLite backup.
The final production checks confirmed:
Image:
lums:latest
Container user:
lums / UID 10001
ReadonlyRootfs:
true
Capabilities:
ALL dropped
Privileged:
false
Secret environment variable:
LUMS_SECRET_KEY absent
Secret path:
/run/secrets/lums_secret
Secret mount:
read-only
Database integrity:
ok
HTTPS:
302 → /login
Unauthenticated client API:
401 authentication_required
Frontend token rotation control:
visible in production
The production database remained intact:
users: 1
clients: 1
audit_log: 22
update_jobs: 1
update_history: 1
The backup used before the production frontend deployment was independently checked with SQLite integrity verification and returned:
integrity = ok
The token-rotation workflow was tested separately before production deployment, including old-token invalidation, new-token authentication, CSRF enforcement, audit logging, and frontend presentation/copy behavior.
Client Token Lifecycle: VERIFIED
Production Secret Rotation: VERIFIED
Container Hardening: VERIFIED
LUMS is designed to centralize Linux update management without removing operational control from the administrator.
The system should remain:
- Transparent
- Auditable
- Controlled
- Secure
- Documented
- Maintainable
The current architecture deliberately separates:
Management Plane
│
├── Nginx
├── Gunicorn
├── Flask
├── Authentication
├── Authorization
└── Database
from
Execution Plane
│
├── lums-agent
├── Execution Watcher
└── APT / dpkg
Security improvements are implemented one controlled layer at a time.
The current verified security architecture is:
Internet / LAN
│
▼
Nginx
HTTPS
│
▼
127.0.0.1:5050
│
▼
Docker
┌──────────────────────────────┐
│ non-root │
│ UID 10001 │
│ capabilities: NONE │
│ root filesystem: READ-ONLY │
│ │
│ /tmp → tmpfs │
│ /var/lib/lums → lums-data │
│ /run/secrets/lums_secret │
│ → READ-ONLY │
│ │
│ Gunicorn → Flask │
└──────────────────────────────┘
│
▼
SQLite
The hardening workflow remains:
Inspect
↓
Test
↓
Verify
↓
Production
↓
Verify
↓
Document
LUMS — Linux Update Management without the noise.
Secure the management plane. Keep execution controlled.
One change. One test. One verified result.