Linux Update Management Server
Security principles, operational requirements, hardening status, and responsible handling of sensitive information in LUMS.
Version: 3.0 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, reports update status, and distributes update jobs.
The actual package-management operation takes place on the managed Linux client.
LUMS therefore separates the management plane from the execution plane.
┌──────────────────────────┐
│ LUMS Server │
│ │
│ Nginx │
│ HTTPS │
│ Gunicorn │
│ Flask │
│ Authentication │
│ Authorization │
│ Inventory │
│ Update Jobs │
│ SQLite │
└────────────┬─────────────┘
│
HTTPS + Token
│
┌──────────────────┴──────────────────┐
│ │
┌───────▼────────┐ ┌────────▼───────┐
│ Debian Client │ │ Arch Client │
│ │ │ │
│ lums-agent │ │ lums-agent │
│ APT / dpkg │ │ pacman │
│ systemd │ │ systemd │
└────────────────┘ └────────────────┘
Security is implemented in multiple layers:
Network
↓
TLS
↓
Nginx
↓
Authentication
↓
Authorization
↓
Application
↓
Gunicorn
↓
Docker
↓
Database
↓
Operating System
↓
Package Manager
A weakness in one layer must never be used as a reason to disable another security layer.
This document covers:
- Docker deployment security
- Container privilege reduction
- Linux capabilities
- Read-only filesystems
- Secret management
- Secret rotation
- Administrator authentication
- Client authentication
- Client token lifecycle
- Authorization
- Job ownership
- Atomic job claiming
- Interrupted-job recovery
- TLS
- Nginx
- Security headers
- SQLite protection
- Backup handling
- Restore requirements
- systemd agent services
- Idle detection
- Package-manager abstraction
- Update execution
- Logging
- Git security
- Deployment security
- Incident handling
- Security testing
- Remaining hardening work
This document does not replace the official security documentation of:
- Ubuntu
- Debian
- Arch Linux
- Docker
- Python
- Flask
- Gunicorn
- Nginx
- SQLite
- APT
- dpkg
- pacman
- systemd
The current LUMS server uses:
- Flask
- Gunicorn
- SQLite
- Docker
- Nginx
- HTTPS
- Bearer-token client authentication
- Argon2 administrator password hashing
- systemd-managed Linux agents
- package-manager abstraction
- audit logging
The current tested client package managers are:
APT
pacman
The application container is not directly exposed to the network.
Current network flow:
Client
│
│ HTTPS :443
▼
Nginx
│
│ HTTP localhost
▼
127.0.0.1:5050
│
│ Docker port mapping
▼
Docker :5000
│
▼
Gunicorn
│
├── Worker
├── Worker
└── Flask application
│
├── Authentication
├── Authorization
├── Inventory
├── Update Jobs
└── Audit Logging
│
▼
SQLite
The current production application binding is:
127.0.0.1:5050 → container:5000
The application is therefore only reachable locally through the Docker port mapping.
External HTTPS access is provided by Nginx.
LUMS deliberately separates two operational areas.
Nginx
↓
Gunicorn
↓
Flask
↓
Authentication
↓
Authorization
↓
SQLite
↓
Job management
The management plane is responsible for:
- administrator access,
- client authentication,
- inventory,
- job creation,
- job assignment,
- job status,
- audit information,
- client management.
lums-agent
↓
package_manager.py
↓
APT / dpkg
or
pacman
The execution plane is responsible for:
- collecting package information,
- detecting available updates,
- claiming authorized jobs,
- executing package operations,
- reporting results.
The server does not directly execute package-management commands on clients.
The current package-manager abstraction supports:
APT
pacman
The tested environments include:
Debian 13
Arch Linux
The agent detects the available package manager.
Current detection logic:
apt available
↓
APT package manager
otherwise
pacman available
↓
pacman package manager
otherwise
unsupported system
The purpose of the abstraction is to keep distribution-specific package-management behavior outside the main agent execution logic.
The current LUMS agent version is:
1.6.0
The agent version is defined centrally in:
agent/agent.py
The tested Arch client and repository copy were verified byte-for-byte identical.
The agent therefore uses the same tested implementation across the current Debian and Arch environments.
The following controls are currently implemented and verified:
| Security control | Status |
|---|---|
| Non-root Docker container | Verified |
| UID 10001 container user | Verified |
CAP_DROP=ALL |
Verified |
| Privileged container disabled | Verified |
| Read-only root filesystem | Verified |
/tmp isolated through tmpfs |
Verified |
| Secret supplied through protected file | Verified |
| Secret mount read-only | Verified |
| Production Flask secret rotation | Verified |
| Gunicorn deployment | Verified |
| HTTPS reverse proxy | Verified |
| Localhost-only application binding | Verified |
| Security headers | Verified |
| Administrator authentication | Verified |
| Client Bearer authentication | Verified |
| Client token rotation | Verified |
| Token invalidation | Verified |
| Token rotation audit logging | Verified |
| CSRF protection for administrative rotation | Verified |
| Atomic job claiming | Verified |
| Job ownership validation | Verified |
| Interrupted-job recovery | Verified |
| Debian client reporting | Verified |
| Arch client reporting | Verified |
| Debian update job execution | Verified |
| Arch update job execution | Verified |
| APT package-manager abstraction | Verified |
| pacman package-manager abstraction | Verified |
| systemd-logind idle detection | Verified |
Remaining hardening work:
Update execution hardening
Full backup / restore test
Automated security regression tests
Final security review
The LUMS application runs as a dedicated non-root user.
Current container identity:
User:
lums
UID:
10001
The container must not run as root.
Verify:
sudo docker inspect lums \
--format 'User={{.Config.User}}'Expected:
User=lums
Running the application as a dedicated non-root user reduces the impact of an application-level compromise.
LUMS does not require additional Linux capabilities.
Production therefore uses:
--cap-drop=ALL
Verify:
sudo docker inspect lums \
--format 'CapDrop={{json .HostConfig.CapDrop}}'Expected:
["ALL"]
The container must also not be privileged.
Verify:
sudo docker inspect lums \
--format 'Privileged={{.HostConfig.Privileged}}'Expected:
false
The application must continue functioning without additional Linux capabilities.
The production container uses:
--read-only
The container root filesystem is therefore not writable during normal operation.
Persistent application state is stored separately:
lums-data
↓
/var/lib/lums
Temporary writable data uses:
/tmp
↓
tmpfs
The current tmpfs configuration is:
/tmp:rw,nosuid,nodev,noexec
Verify:
sudo docker inspect lums \
--format 'ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}}'Expected:
true
This prevents normal application writes from modifying the container image filesystem.
The security-relevant filesystem layout is:
Container
│
├── / READ-ONLY
│
├── /tmp tmpfs
│
├── /var/lib/lums persistent Docker volume
│
└── /run/secrets/
└── lums_secret READ-ONLY
This deliberately separates:
Application image
≠
Persistent database
≠
Temporary files
≠
Security secrets
The Flask application secret is security-sensitive.
Production secrets must never be:
- committed to Git,
- written into documentation,
- printed to logs,
- included in screenshots,
- included in bug reports,
- exposed through normal container environment variables,
- copied into source files.
Production uses:
/etc/lums/secrets/lums_secret
The secret is mounted into the container:
Host
/etc/lums/secrets/lums_secret
│
│ read-only bind mount
▼
Container
/run/secrets/lums_secret
The application is configured with:
LUMS_SECRET_KEY_FILE=/run/secrets/lums_secret
The actual secret value is intentionally never documented.
The host secret directory should be restricted:
/etc/lums/secrets
Expected:
root:root
0700
The secret file itself is restricted to the required host and container identities.
A typical configuration is:
root:10001
0640
Verify:
sudo stat /etc/lums/secrets/lums_secretThe secret mount inside the container must be read-only.
Verify:
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
The production Flask secret must not be supplied through:
LUMS_SECRET_KEY
Verify:
sudo docker exec lums sh -c '
if [ -n "${LUMS_SECRET_KEY:-}" ]; then
echo "PRESENT"
else
echo "ABSENT"
fi
'Expected:
ABSENT
The application instead reads:
/run/secrets/lums_secret
Verify:
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 security controls.
The Flask secret was previously exposed through diagnostic output.
The previous value is intentionally not reproduced.
The exposure was treated as a credential compromise.
The rotation process was:
Identify exposure
↓
Isolate secret from normal environment
↓
Test rotation in isolation
↓
Generate replacement secret
↓
Replace protected secret file
↓
Restart LUMS
↓
Verify old sessions
↓
Verify new authentication
↓
Remove temporary old-secret backup
The replacement production secret is now active.
The old value must not be reused.
Changing the Flask secret affects existing application sessions.
This behavior was explicitly tested before production rotation.
The expected behavior is:
Old secret
↓
Existing session
↓
Secret rotation
↓
Existing session invalidated
A new login establishes a valid session using the new secret.
This makes secret rotation an operational event that must be planned and verified.
LUMS provides administrator authentication for the web interface.
Administrator passwords are protected using Argon2.
The password itself must never be stored in plaintext.
The authentication boundary is:
Browser
↓
HTTPS
↓
Nginx
↓
Gunicorn
↓
Flask
↓
Authentication
Authentication and authorization are treated as separate concepts.
Linux clients authenticate against the LUMS API using Bearer tokens.
Example:
Authorization: Bearer <CLIENT_TOKEN>
The server hashes the supplied token and compares the resulting digest against the stored token digest.
The plaintext client token is not stored as normal database state.
The current token generation uses cryptographically secure randomness.
Conceptually:
Generate random token
↓
Hash token
↓
Store hash
↓
Return token once
The client-token lifecycle is:
Generate
↓
Hash
↓
Store hash
↓
Authenticate
↓
Rotate
↓
Invalidate previous token
↓
Issue replacement
↓
Update agent
↓
Verify communication
The plaintext token is only presented when required for initial configuration or rotation.
It must not be written to:
- audit logs,
- application logs,
- Git,
- documentation,
- screenshots.
Administrative token rotation is implemented through the client-management workflow.
The rotation operation:
- identifies the client,
- generates a new cryptographically random token,
- hashes the token,
- replaces the stored token hash,
- updates token metadata,
- invalidates the previous credential,
- creates an audit event,
- returns the new token once.
The previous token becomes invalid immediately.
Token rotation is an administrative operation.
It requires:
Valid administrator session
+
Valid CSRF protection
Expected failure behavior includes:
No administrator session
→ 401
Missing/invalid CSRF token
→ 400
Unknown client
→ 404
A client token itself must never be sufficient to authorize administrative token rotation.
Successful token rotation creates an audit event.
The audit event records that the operation occurred without storing the replacement credential.
Conceptually:
action:
client.token.rotate
target:
client:<CLIENT_ID>
result:
success
The actual token value is never written to the audit event.
This provides accountability without turning the audit log into a credential store.
The client detail interface provides a token rotation control.
Before rotation, the administrator is warned that:
- the existing token becomes invalid,
- the LUMS agent must be updated,
- the new token will only be displayed once.
After successful rotation, the replacement token can be copied.
The token is not intended to remain permanently visible in the interface.
The browser is treated as a presentation layer and not as a trusted authorization source.
The token lifecycle was tested independently.
The verified flow was:
Token A
↓
Authentication succeeds
↓
Rotate
↓
Token B
Then:
Token A → rejected
Token B → accepted
The tests also covered:
No administrator session
→ rejected
Invalid/missing CSRF protection
→ rejected
Unknown client
→ rejected
The audit event was created successfully.
The token value was not stored in the audit event.
After token rotation testing, the production agent was updated with the replacement credential.
The agent successfully reported to LUMS.
The verified flow was:
Agent
↓
HTTPS
↓
Bearer authentication
↓
Client identification
↓
System report
↓
LUMS API
↓
Database
The production client subsequently appeared online again.
This verified the complete credential-rotation path.
Authentication answers:
Who are you?
Authorization answers:
What are you allowed to do?
LUMS validates authenticated client ownership for client-specific operations.
A client must not be able to access another client's jobs merely by changing a client ID in a request.
The server therefore derives the authenticated client identity from the authentication layer.
Client-specific job operations validate:
Authenticated client
+
Requested job
+
Job owner
Conceptually:
authenticated_client.id
==
job.client_id
If the relationship does not match, the operation must be rejected.
This prevents cross-client job access.
Job claiming is performed using an atomic state transition.
The intended transition is:
pending
↓
running
The operation must ensure that two clients cannot successfully claim the same pending job through normal concurrent execution.
Conceptually:
Client A ──┐
├── claim
Client B ──┘
↓
one winner
The winning client becomes responsible for executing and reporting the job.
A job result must be associated with the authenticated client that owns the job.
The server therefore validates:
authenticated client
==
job owner
before accepting a result.
A client cannot legitimately submit a result for another client's job.
An update operation can be interrupted by:
- agent termination,
- system shutdown,
- network interruption,
- process failure,
- unexpected client failure.
A job may therefore remain in:
running
without receiving its expected final result.
LUMS provides controlled recovery for this situation.
Recovery changes an interrupted job from:
running
to:
abandoned
The recovery operation validates:
- job existence,
- client ownership,
- current job state,
- valid recovery transition.
The operation records:
finished_at
recovery reason
update history
The job is not falsely reported as successful.
The recovery reason distinguishes an interrupted execution from a normal failure or success.
The current recovery reason is:
Agent did not submit a final result.
This allows the history to distinguish:
success
failed
abandoned
Recovery uses a conditional state transition.
If another operation changes the job state first, the recovery operation must not overwrite the newer state.
Conceptually:
running
│
├── agent result
│
└── recovery
Only one valid transition should win.
This prevents stale recovery requests from silently modifying already-completed jobs.
Controlled recovery testing verified:
status:
abandoned
finished_at:
populated
recovery_reason:
Agent did not submit a final result.
The associated history entry was preserved.
Package statistics were preserved.
The test data was removed afterwards.
A real update job was subsequently executed successfully.
This confirmed that recovery did not break normal update execution.
The LUMS agent communicates with the server using HTTPS.
The agent configuration contains the server endpoint, client credential, and CA configuration.
Typical configuration:
LUMS_BASE
LUMS_TOKEN
LUMS_CA_FILE
Example:
LUMS_BASE="https://Server IP"
LUMS_TOKEN="<REDACTED>"
LUMS_CA_FILE="/opt/lums-agent/lums-ca.crt"
The actual credential is never documented.
TLS verification must remain enabled.
Certificate problems must be fixed rather than bypassed by disabling verification.
The agent configuration is stored outside the Git repository.
Typical location:
/etc/default/lums-agent
The configuration should be readable only by the identities that require it.
Sensitive values must never be committed.
Verify:
sudo stat /etc/default/lums-agentThe actual token value must not appear in diagnostic output shared publicly.
The LUMS agent runs as a systemd service.
The service is designed as a oneshot operation.
A successful execution therefore normally ends with:
inactive (dead)
after the agent process exits successfully.
This is expected behavior.
The recurring execution is handled by the corresponding timer.
The agent timer periodically starts the service.
Current scheduling uses a recurring systemd timer.
Conceptually:
systemd timer
↓
lums-agent.service
↓
agent.py
↓
report / job execution
↓
exit
↓
wait for next timer
The timer remains active while the oneshot service starts and exits for each cycle.
The current agent does not rely on:
w -h
for idle detection.
Idle detection uses:
systemd-logind
through:
loginctl
The agent examines relevant user sessions and uses:
Class
Type
TTY
State
IdleHint
IdleSinceHintMonotonic
to determine whether an interactive user session is currently idle.
The purpose of idle detection is to avoid unnecessarily disruptive package operations while a user is actively working.
The current implementation:
- ignores irrelevant system sessions,
- considers user sessions,
- recognizes graphical and TTY sessions,
- detects active sessions through
IdleHint, - calculates idle duration using the logind monotonic timestamp,
- reports whether idle detection is supported.
The result includes:
idle
idle_seconds
threshold_seconds
idle_source
idle_supported
The current source is:
loginctl
If logind information cannot be retrieved reliably, the agent does not pretend to know that the system is idle.
Failure is treated conservatively.
The agent therefore avoids using an unreliable idle state as permission to perform potentially disruptive operations.
This is preferable to treating an unknown state as confirmed user inactivity.
Package management is implemented through a dedicated abstraction.
The agent detects:
APT
pacman
and selects the appropriate implementation.
Conceptually:
package_manager.py
│
┌───────────┴───────────┐
│ │
AptPackageManager PacmanPackageManager
│ │
apt / dpkg pacman
This prevents distribution-specific package-manager commands from being duplicated throughout the main agent.
On Debian-based clients, LUMS uses:
apt
apt-get
apt-cache
dpkg-query
Examples of supported operations include:
Package inventory
Update detection
Package installation
Package removal
Package updates
System updates
The actual package operation occurs on the client.
On Arch Linux clients, LUMS uses:
pacman
Examples of supported operations include:
Package inventory
Update detection
Package installation
Package removal
Package updates
System updates
System update operations use the native Arch package-management mechanism.
The LUMS agent does not attempt to translate Arch package management into APT semantics.
Update execution is a privileged client-side operation.
LUMS therefore treats package management as a security-sensitive execution boundary.
Update jobs must:
- be authenticated,
- belong to the correct client,
- be valid jobs,
- transition through controlled states,
- be executed by the intended agent,
- produce an explicit result,
- remain auditable.
The agent must not execute arbitrary unauthenticated commands received from the network.
The supported package operations are represented through the package-manager abstraction.
Examples include:
INSTALL_PACKAGE
REMOVE_PACKAGE
UPDATE_PACKAGE
UPDATE_SYSTEM
The action is interpreted by the client agent and passed to the selected package-manager implementation.
This separates:
LUMS job semantics
from:
Distribution-specific package commands
Package-manager operations are security-sensitive because package managers generally require elevated privileges.
LUMS therefore uses controlled execution for its own package operations.
However, LUMS cannot automatically prevent every manually started package-manager process on the operating system from running concurrently.
For example:
LUMS
↓
apt
User
↓
apt
may still represent an external coordination problem.
The current LUMS execution lock reduces collisions within LUMS-controlled operations.
Complete coordination with arbitrary external package-manager processes remains a hardening task.
LUMS must not reboot a client merely because packages were installed.
A reboot requirement is separate from successful package installation.
The system should distinguish:
Update successful
from:
Reboot required
Any future automatic reboot mechanism must be explicitly designed, authorized, logged, and tested.
Nginx is the external HTTPS entry point.
The Flask/Gunicorn application is not directly exposed.
Current architecture:
Network
│
▼
Nginx :443
│
▼
127.0.0.1:5050
│
▼
Docker :5000
│
▼
Gunicorn
The application port must remain internal.
HTTP access is redirected to HTTPS.
Conceptually:
HTTP :80
↓
redirect
↓
HTTPS :443
Test:
curl -I http://127.0.0.1/The expected result is an HTTP redirect to the HTTPS endpoint.
LUMS uses HTTPS for client and browser communication.
TLS verification must remain enabled for agents.
Supported modern TLS versions should include:
TLS 1.2
TLS 1.3
Older insecure protocols must remain disabled.
Certificates must contain the correct Subject Alternative Name.
Private key permissions must be restricted.
Validate the Nginx configuration:
sudo nginx -tSelf-signed certificates may be used in controlled laboratory environments.
In that case, the client must explicitly trust the appropriate CA or certificate.
The LUMS agent must still verify the certificate chain.
A certificate warning must not be solved by disabling TLS verification.
For larger or externally accessible deployments, a certificate infrastructure appropriate to the deployment environment should be used.
The application provides security headers intended to reduce common browser-side attack surfaces.
The current policy includes:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: no-referrer
The permissions policy restricts unnecessary browser capabilities such as:
camera
microphone
geolocation
payment
The Content Security Policy restricts application resources to expected origins.
The policy includes restrictions equivalent to:
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'
Security headers must be rechecked after Nginx or frontend changes.
The application uses:
127.0.0.1:5050:5000
This means:
Host localhost:5050
↓
Container :5000
It must not be casually changed to:
0.0.0.0:5050:5000
Doing so would expose the application port on the network interface.
If the network architecture is changed, firewall and reverse-proxy controls must be redesigned and retested.
The externally required services are expected to be limited to those required by the deployment.
Typical services:
22/tcp
80/tcp
443/tcp
The application ports:
5000
5050
must remain internal.
Check listening sockets:
sudo ss -lntpThe LUMS application should appear bound to:
127.0.0.1:5050
rather than:
0.0.0.0:5050
Only required network services should be exposed.
For environments using UFW:
sudo ufw status verboseApplication ports should not be opened merely for troubleshooting.
If temporary access is required during testing, it should be removed afterwards.
Firewall configuration must be reviewed after:
- network changes,
- Nginx changes,
- Docker changes,
- new services,
- port changes.
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:
/opt/lums-public
or committed to Git.
The persistent volume must not be removed during ordinary troubleshooting.
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
An integrity check should be performed after major database-related deployment changes.
SQLite backups should use a SQLite-aware mechanism.
A backup must not simply be treated as a normal file copy while the database is actively changing.
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 resulting backup should be moved to a protected backup location.
Example:
/var/backups/lums/
Backup files contain sensitive application information.
They may contain:
- users,
- clients,
- audit records,
- job information,
- operational metadata.
Backups therefore require access control.
Example:
sudo chown root:root /var/backups/lums/lums.db.backup
sudo chmod 600 /var/backups/lums/lums.db.backupBackups must never be committed to Git.
A backup is not considered valid merely because the file exists.
Verify the SQLite integrity:
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
Backup verification should be performed before relying on the backup for recovery.
A complete restore test remains outstanding.
The intended procedure is:
Verified backup
↓
Isolated LUMS environment
↓
Restore database
↓
SQLite integrity check
↓
Application startup
↓
Authentication test
↓
Client data verification
↓
Job/history verification
A backup should not be considered fully validated until an actual restore has been tested.
The Git repository must never contain:
Passwords
Client tokens
Flask secrets
TLS private keys
Production environment files
Database files
SQLite backups
Session secrets
Unredacted production data
Before committing:
cd /opt/lums-public
git status
git diff
git diff --checkReview all changed files.
The Git identity for LUMS commits is:
Name:
NovaForgeCtrl
Email:
xxxxnoreply.github.com
The repository should be checked before deployment.
Example:
cd /opt/lums-public
git fetch origin
echo "=== STATUS ==="
git status -sb
echo "=== LOCAL HEAD ==="
git rev-parse HEAD
echo "=== GITHUB origin/main ==="
git rev-parse origin/main
echo "=== DIFFERENCE ==="
git log --oneline --left-right HEAD...origin/mainA clean synchronized repository should show:
## main...origin/main
with matching commit IDs and no left/right differences.
The production image must be built from reviewed source.
Build:
sudo docker build \
-t lums:latest \
.Inspect:
sudo docker image inspect \
lums:latestVerify that the image specifies the intended non-root user.
A successful image build does not prove:
- application correctness,
- deployment correctness,
- database integrity,
- security correctness,
- client communication.
Those properties must be tested separately.
The hardened container configuration 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 \
-v lums-data:/var/lib/lums \
-p 127.0.0.1:5050:5000 \
lums:latestThis provides:
Non-root execution
+
No Linux capabilities
+
Read-only root filesystem
+
Protected secret file
+
Read-only secret mount
+
Persistent database volume
+
Localhost-only application binding
After deployment:
sudo docker restart lumsThen:
sudo docker ps \
--filter "name=^lums$"Review logs:
sudo docker logs \
--tail 100 \
lumsThe application should start without a crash loop.
Gunicorn should successfully start the application workers.
Check the runtime configuration:
sudo docker inspect lums \
--format '
User={{.Config.User}}
ReadonlyRootfs={{.HostConfig.ReadonlyRootfs}}
Privileged={{.HostConfig.Privileged}}
CapDrop={{json .HostConfig.CapDrop}}
'Expected properties:
User=lums
ReadonlyRootfs=true
Privileged=false
CapDrop=["ALL"]
Check mounts:
sudo docker inspect lums \
--format '{{range .Mounts}}{{.Source}} -> {{.Destination}} RW={{.RW}}{{"\n"}}{{end}}'The database volume should be present:
lums-data -> /var/lib/lums RW=true
The secret mount should be present:
/etc/lums/secrets/lums_secret -> /run/secrets/lums_secret RW=false
Persistent application data must survive container recreation.
Before deployment:
cd /opt/lums-public
git status --short
git fetch origin
git log --oneline --decorate -3
git diff --checkUpdate only when the working tree is in the expected state:
git pull --ff-only origin mainBuild:
sudo docker build -t lums:latest .Before replacing a production container:
Verify source
↓
Verify image
↓
Verify database
↓
Create SQLite backup
↓
Verify backup
↓
Replace container
↓
Verify application
↓
Verify HTTPS
↓
Verify client communication
When only the container must be replaced:
sudo docker stop lums
sudo docker rm lumsThe persistent volume must remain.
Do not remove:
lums-data
unless intentionally performing a complete data-destruction operation with a verified backup and explicit recovery plan.
Recreate the container using the tested hardened configuration.
After deployment verify:
1. Container running
2. Correct image
3. Correct container user
4. Localhost-only application binding
5. Persistent volume mounted
6. Database accessible
7. Database integrity valid
8. Gunicorn starts
9. HTTPS works
10. HTTP redirects to HTTPS
11. Security headers remain present
12. Administrator login works
13. Client authentication works
14. Client reporting works
15. Update jobs remain available
16. Job execution works
17. Root filesystem is read-only
18. All capabilities are dropped
19. Secret environment variable is absent
20. Secret file is readable
21. Secret mount is read-only
22. Container restart works
Logs may contain operational information such as:
- HTTP requests,
- timestamps,
- job identifiers,
- client identifiers,
- application errors,
- package statistics,
- execution status.
Logs must not intentionally contain:
- passwords,
- client tokens,
- Flask secrets,
- TLS private keys.
Before sharing logs externally:
Review
↓
Redact
↓
Review again
↓
Share
Security-relevant administrative actions should produce audit information.
Examples include:
Authentication events
Client changes
Token rotation
Job operations
Recovery operations
Security-sensitive administrative actions
Audit information should contain enough information to understand:
what happened
when it happened
which object was affected
whether the operation succeeded
Sensitive credentials must not be stored in audit records.
Security incidents should be handled systematically.
General workflow:
Detect
↓
Contain
↓
Investigate
↓
Rotate affected credentials
↓
Recover
↓
Verify
↓
Document
Do not destroy evidence unnecessarily during troubleshooting.
If a client token is compromised:
- Identify the affected client.
- Rotate the token.
- Verify the old token is invalid.
- Update the client configuration.
- Verify the new token.
- Review recent client activity.
- Review relevant logs.
- Document the incident.
The known-compromised token must not remain active.
If the Flask secret is compromised:
- Restrict access if necessary.
- Treat the existing secret as compromised.
- Generate a replacement.
- Replace the protected secret file.
- Restart LUMS.
- Verify old sessions are invalid.
- Verify new authentication.
- Review related credentials.
- Remove temporary copies of the old secret.
- Document the incident.
The compromised secret must not be reused.
If a TLS private key is compromised:
- Replace the certificate/private-key pair.
- Update trusted certificates where required.
- Reload Nginx.
- Verify certificate validation.
- Review access logs.
- Determine the affected period.
- Document the incident.
If a database or backup becomes exposed:
- Restrict access.
- Determine what data was exposed.
- Review authentication-related information.
- Rotate affected credentials.
- Review client tokens where appropriate.
- Replace compromised backups if necessary.
- Review access logs.
- Document the incident.
The previous Flask secret exposure is treated as a completed security incident.
The value is intentionally not reproduced.
The response was:
Exposure identified
↓
Secret isolation implemented
↓
Rotation tested
↓
Replacement secret generated
↓
Protected secret file updated
↓
Production restarted
↓
Old sessions invalidated
↓
New authentication verified
↓
Temporary old-secret material removed
The previous value is no longer the active production secret.
The Debian client has successfully completed the LUMS agent communication flow.
Verified:
Agent:
1.6.0
OS:
Debian
Architecture:
x86_64
Package manager:
APT
Report:
accepted
Authentication:
successful
Update job:
successful
The client successfully completed both inventory reporting and update-job execution.
The Arch Linux client has successfully completed the same LUMS management flow.
Verified:
Agent:
1.6.0
OS:
Arch Linux
Architecture:
x86_64
Kernel:
7.2.6-arch2-1
Package manager:
pacman
systemd:
261.3-1
Python:
3.14.7
pacman:
7.1.0
Report:
accepted
Authentication:
successful
Update job:
successful
The Arch agent was additionally verified against the repository copy.
The SHA-256 checksum matched the repository version.
The current architecture supports:
LUMS
│
UPDATE_SYSTEM
│
┌──────────┴──────────┐
│ │
Debian Arch
│ │
lums-agent lums-agent
│ │
APT pacman
│ │
result result
│ │
└──────────┬──────────┘
│
▼
LUMS
The management layer therefore remains distribution-independent while the client package-manager layer remains distribution-specific.
- Container runs as non-root
- UID 10001 verified
- Container is not privileged
- All Linux capabilities dropped
- Root filesystem is read-only
-
/tmpuses tmpfs -
/tmpusesnosuid -
/tmpusesnodev -
/tmpusesnoexec - Persistent database volume is separate
- Secret mount is separate
- Secret mount is read-only
- Application binding is localhost-only
- Flask secret isolated from normal environment
- Secret stored outside Git
- Secret mounted read-only
- Secret rotation tested
- Production secret rotated
- Old production secret invalidated
- Temporary old-secret material removed
- Client token generation uses cryptographically secure randomness
- Client token hash stored instead of plaintext
- Client token rotation implemented
- Previous client token invalidated
- Token rotation audit event implemented
- Token values excluded from audit records
- Administrator authentication implemented
- Argon2 password hashing
- Invalid credentials rejected
- Client Bearer authentication implemented
- Invalid client token rejected
- Protected endpoints require authentication
- Token rotation requires administrator authentication
- CSRF protection for administrative token rotation
- Client identity established server-side
- Client/job ownership checked
- Job result ownership checked
- Recovery ownership checked
- Atomic job claiming
- Full role-based administrative authorization model
- Agent uses HTTPS
- TLS verification enabled
- CA configuration supported
- Agent credentials stored outside Git
- systemd timer configured
- Inventory reporting works
- Update detection works
- Job retrieval works
- Atomic job claiming works
- Job result reporting works
- Interrupted-job recovery tested
- APT package-manager support
- pacman package-manager support
- Debian tested
- Arch tested
- systemd-logind idle detection
- Complete external APT/dpkg collision prevention
- SQLite database isolated from Git
- Integrity check available
- SQLite-aware backup method
- Backup permissions restricted
- Backup integrity verification
- Full restore test
- Automated backup verification
- Nginx reverse proxy
- HTTPS
- HTTP redirect
- Localhost-only application port
- Security headers
- TLS verification for agent communication
- Application ports not intentionally exposed externally
- Final external network review
- Production secrets excluded
- Tokens excluded
- TLS private keys excluded
- Database excluded
- Backups excluded
- Documentation uses placeholders
- Changes reviewed before deployment
- Git working tree checked before deployment
LUMS controls its own package-management operations but cannot automatically prevent arbitrary manually started package-manager processes from running simultaneously.
Further coordination remains a hardening task.
The current administrative model is intentionally simple.
A full role-based access-control model has not yet been implemented.
SQLite is appropriate for the current project scope and laboratory deployment.
Larger environments may eventually require a dedicated database service depending on:
- client count,
- concurrency,
- job volume,
- audit volume,
- availability requirements,
- backup requirements.
SQLite backup creation and integrity verification are implemented.
A complete isolated restore test remains outstanding.
Security checks currently exist at multiple manual and operational layers.
A comprehensive automated security regression suite remains future work.
Status: Complete
Implemented and verified:
- non-root container,
- UID 10001,
- dropped capabilities,
- non-privileged runtime,
- read-only root filesystem,
- tmpfs
/tmp, - persistent database volume,
- protected secret mount,
- localhost-only application binding.
Status: Complete
Implemented and verified:
- protected host secret,
- read-only secret mount,
- no production secret in normal environment,
- isolated rotation testing,
- production secret replacement,
- old session invalidation,
- new authentication,
- production restart.
Status: Complete
Implemented and verified:
- cryptographically secure token generation,
- token hashing,
- Bearer authentication,
- administrative rotation,
- immediate old-token invalidation,
- CSRF protection,
- audit event,
- one-time replacement-token presentation,
- production agent reconfiguration,
- successful production communication.
Status: Complete
Implemented and tested:
APT
pacman
The package-manager abstraction is used by the agent rather than embedding distribution-specific commands throughout the update engine.
Verified:
Debian
Arch Linux
Status: Complete
The previous w -h implementation was replaced by systemd-logind-based detection.
Current mechanism:
loginctl
The implementation considers relevant interactive user sessions and logind idle state.
Status: Complete
Implemented and tested:
- ownership validation,
- state validation,
- controlled recovery,
abandonedstate,- recovery reason,
- history preservation,
- package-statistic preservation,
- race protection.
Status: In progress
Remaining work includes:
- stronger APT/dpkg coordination,
- job timeout handling,
- additional package-manager state validation,
- broader execution regression testing,
- additional failure-path testing.
Status: Partially complete
Implemented:
- SQLite-aware backup,
- protected backup storage,
- integrity verification.
Remaining:
Full isolated restore test
Status: Planned
Potential automated tests:
- authentication,
- authorization,
- token rotation,
- token invalidation,
- CSRF,
- job ownership,
- job claiming,
- recovery,
- security headers,
- container hardening,
- secret handling,
- database integrity.
Status: Planned
The final review should occur after the remaining hardening phases.
The review should compare:
Documentation
↕
Implementation
↕
Production
The final security state should only be documented as verified after the actual deployment has been tested.
Security issues should be reported responsibly.
A useful security report contains:
- short description,
- affected component,
- reproduction steps,
- expected behavior,
- actual behavior,
- potential impact,
- suggested mitigation,
- relevant redacted logs.
Never include:
- passwords,
- client tokens,
- private keys,
- Flask secrets,
- personal information,
- complete production databases,
- unredacted inventory data.
Sensitive information must be removed before logs, screenshots, or configuration files are shared.
Security reviews should be performed after:
- application changes,
- authentication changes,
- authorization changes,
- Docker changes,
- Nginx changes,
- certificate changes,
- database schema changes,
- agent changes,
- package-manager changes,
- watcher changes,
- deployment changes,
- secret changes.
Regularly review:
Operating system updates
Docker images
Python dependencies
Flask dependencies
Gunicorn
Nginx
TLS configuration
File permissions
Database backups
Git history
Authentication
Authorization
Job execution
Package-manager coordination
Container privileges
Container capabilities
Secret handling
Secret rotation
Agent configuration
systemd services
systemd timers
Security-sensitive changes should follow a controlled workflow:
Inspect
↓
Understand current behavior
↓
Design change
↓
Implement
↓
Test locally
↓
Test integration
↓
Verify production configuration
↓
Deploy
↓
Verify production
↓
Document
A failed test should not be hidden by changing the documentation to match the failure.
The implementation must be corrected or the limitation documented.
The current overall state is:
[x] Non-root container
[x] UID 10001
[x] Drop ALL capabilities
[x] Privileged container disabled
[x] Read-only root filesystem
[x] tmpfs /tmp
[x] Localhost-only application binding
[x] Protected secret file
[x] Read-only secret mount
[x] Secret isolation
[x] Flask secret rotation
[x] Administrator authentication
[x] Client authentication
[x] Client token hashing
[x] Client token rotation
[x] Client token invalidation
[x] Token rotation audit logging
[x] CSRF protection for token rotation
[x] Job ownership validation
[x] Atomic job claiming
[x] Interrupted-job recovery
[x] Debian client communication
[x] Arch client communication
[x] APT support
[x] pacman support
[x] systemd-logind idle detection
[x] Gunicorn deployment
[x] HTTPS reverse proxy
[x] Security headers
[x] SQLite integrity verification
[x] SQLite-aware backup
[x] Backup integrity verification
[ ] Complete package-manager collision prevention
[ ] Full backup / restore test
[ ] Automated security regression tests
[ ] Final security review
The completed controls should not be interpreted as meaning that LUMS has no remaining security work.
Security hardening is continuous.
LUMS follows several operational rules.
A configuration file saying:
read-only
is not sufficient.
The running container must be inspected.
A running container does not prove:
- authentication works,
- HTTPS works,
- database integrity is valid,
- clients can authenticate,
- update jobs work.
Each layer must be verified.
A backup must be:
created
↓
protected
↓
integrity checked
↓
eventually restored in isolation
A secret that was exposed must not be considered safe merely because it is no longer visible.
It must be rotated.
A valid client token only establishes client identity.
The server must still verify whether that client is authorized for the requested resource.
Update jobs must be treated as privileged operations.
The system must control:
who
what
where
when
result
Documentation should describe the actual tested state.
If something is incomplete, it must be marked as incomplete.
The current verified security architecture is:
Network
│
▼
┌─────────┐
│ Nginx │
│ HTTPS │
└────┬────┘
│
localhost only
│
▼
┌────────────────┐
│ 127.0.0.1:5050│
└───────┬────────┘
│
Docker mapping
│
▼
┌─────────────────────────────┐
│ LUMS Container │
│ │
│ User: lums / UID 10001 │
│ Privileged: false │
│ Capabilities: ALL dropped │
│ Root FS: read-only │
│ │
│ /tmp → tmpfs │
│ /var/lib/lums → lums-data │
│ /run/secrets/lums_secret │
│ → read-only │
│ │
│ Gunicorn │
│ ↓ │
│ Flask │
│ ↓ │
│ SQLite │
└──────────────┬──────────────┘
│
│ HTTPS + Bearer Token
│
┌────────────┴────────────┐
│ │
▼ ▼
Debian 13 Client Arch Linux Client
│ │
lums-agent 1.6.0 lums-agent 1.6.0
│ │
APT / dpkg pacman
│ │
└────────────┬────────────┘
│
▼
Result
│
▼
LUMS API
The final architecture deliberately separates:
Management Plane
│
├── Nginx
├── HTTPS
├── Gunicorn
├── Flask
├── Authentication
├── Authorization
├── Inventory
├── Job Management
└── SQLite
from
Execution Plane
│
├── lums-agent
├── systemd
├── Package Manager Abstraction
├── APT / dpkg
└── pacman
The server manages the desired operation.
The client performs the actual package-management operation.
This separation reduces the need for the server to have direct operating-system privileges on managed clients.
The current LUMS implementation has verified security controls across:
Container
↓
Network
↓
TLS
↓
Authentication
↓
Authorization
↓
Secrets
↓
Tokens
↓
Jobs
↓
Recovery
↓
Agent
↓
Package Manager
↓
Database
↓
Backups
The remaining work is explicitly limited to the documented open hardening areas.
The project should not claim a final security review until those remaining areas have been tested.
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 administrator credentials.
- Protect client tokens.
- Protect TLS private keys.
- Protect the mounted Flask secret.
- Keep database backups secure.
- Verify backups rather than trusting file existence.
- Do not remove persistent volumes during ordinary troubleshooting.
- Do not automatically reboot clients.
- Review changes before deployment.
- Test security-sensitive changes.
- Document incidents and configuration changes.
- Do not claim incomplete security controls are fully implemented.
- Keep update execution controlled and auditable.
- Harden the container incrementally.
- 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.
- Verify replacement credentials before closing a credential change.
- Keep production secrets outside Git and outside normal container environment variables where practical.
- Use distribution-specific package-manager implementations behind a controlled abstraction.
- Validate job ownership on the server.
- Use atomic state transitions for job claiming and recovery.
- Prefer conservative behavior when system state cannot be reliably determined.
- Keep production documentation synchronized with verified implementation state.
- Test recovery paths, not only successful paths.
- Treat security hardening as a continuous process rather than a one-time configuration.
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 architecture deliberately separates:
Management Plane
│
├── Nginx
├── Gunicorn
├── Flask
├── Authentication
├── Authorization
├── Inventory
├── Job Management
└── Database
from
Execution Plane
│
├── lums-agent
├── systemd
├── Package Manager Abstraction
└── APT / dpkg / pacman
Security improvements are implemented one controlled layer at a time.
The established 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.