harden runtime config and automate first-run permissions
Run the container as a non-root user in production to fail fast on insecure deployments. Add DEBUG env-based request/response logging for container diagnostics, and introduce a one-shot init-permissions service in docker-compose so fresh installs automatically fix data/uploads ownership for SQLite write access.
This commit is contained in:
@@ -30,6 +30,9 @@ COPY --from=client-builder /app/client/public/fonts ./public/fonts
|
|||||||
RUN mkdir -p /app/data /app/uploads/files /app/uploads/covers /app/uploads/avatars /app/uploads/photos && \
|
RUN mkdir -p /app/data /app/uploads/files /app/uploads/covers /app/uploads/avatars /app/uploads/photos && \
|
||||||
mkdir -p /app/server && ln -s /app/uploads /app/server/uploads && ln -s /app/data /app/server/data
|
mkdir -p /app/server && ln -s /app/uploads /app/server/uploads && ln -s /app/data /app/server/data
|
||||||
|
|
||||||
|
RUN chown -R node:node /app
|
||||||
|
USER node
|
||||||
|
|
||||||
# Umgebung setzen
|
# Umgebung setzen
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
services:
|
services:
|
||||||
|
init-permissions:
|
||||||
|
image: alpine:3.20
|
||||||
|
container_name: trek-init-permissions
|
||||||
|
user: "0:0"
|
||||||
|
command: >
|
||||||
|
sh -c "mkdir -p /app/data /app/uploads &&
|
||||||
|
chown -R 1000:1000 /app/data /app/uploads &&
|
||||||
|
chmod -R u+rwX /app/data /app/uploads"
|
||||||
|
volumes:
|
||||||
|
- ./data:/app/data
|
||||||
|
- ./uploads:/app/uploads
|
||||||
|
restart: "no"
|
||||||
|
|
||||||
app:
|
app:
|
||||||
image: mauriceboe/trek:latest
|
image: mauriceboe/trek:latest
|
||||||
container_name: trek
|
container_name: trek
|
||||||
|
depends_on:
|
||||||
|
init-permissions:
|
||||||
|
condition: service_completed_successfully
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
PORT=3001
|
PORT=3001
|
||||||
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
JWT_SECRET=your-super-secret-jwt-key-change-in-production
|
||||||
NODE_ENV=development
|
NODE_ENV=development
|
||||||
|
DEBUG=false
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import path from 'path';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const DEBUG = String(process.env.DEBUG || 'false').toLowerCase() === 'true';
|
||||||
|
|
||||||
// Trust first proxy (nginx/Docker) for correct req.ip
|
// Trust first proxy (nginx/Docker) for correct req.ip
|
||||||
if (process.env.NODE_ENV === 'production' || process.env.TRUST_PROXY) {
|
if (process.env.NODE_ENV === 'production' || process.env.TRUST_PROXY) {
|
||||||
@@ -79,6 +80,34 @@ if (shouldForceHttps) {
|
|||||||
app.use(express.json({ limit: '100kb' }));
|
app.use(express.json({ limit: '100kb' }));
|
||||||
app.use(express.urlencoded({ extended: true }));
|
app.use(express.urlencoded({ extended: true }));
|
||||||
|
|
||||||
|
if (DEBUG) {
|
||||||
|
app.use((req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const startedAt = Date.now();
|
||||||
|
const requestId = Math.random().toString(36).slice(2, 10);
|
||||||
|
const redact = (value: unknown): unknown => {
|
||||||
|
if (!value || typeof value !== 'object') return value;
|
||||||
|
if (Array.isArray(value)) return value.map(redact);
|
||||||
|
const hidden = new Set(['password', 'token', 'jwt', 'authorization', 'cookie', 'client_secret', 'mfa_token', 'code']);
|
||||||
|
const out: Record<string, unknown> = {};
|
||||||
|
for (const [k, v] of Object.entries(value as Record<string, unknown>)) {
|
||||||
|
out[k] = hidden.has(k.toLowerCase()) ? '[REDACTED]' : redact(v);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
const safeQuery = redact(req.query);
|
||||||
|
const safeBody = redact(req.body);
|
||||||
|
console.log(`[DEBUG][REQ ${requestId}] ${req.method} ${req.originalUrl} ip=${req.ip} query=${JSON.stringify(safeQuery)} body=${JSON.stringify(safeBody)}`);
|
||||||
|
|
||||||
|
res.on('finish', () => {
|
||||||
|
const elapsedMs = Date.now() - startedAt;
|
||||||
|
console.log(`[DEBUG][RES ${requestId}] ${req.method} ${req.originalUrl} status=${res.statusCode} elapsed_ms=${elapsedMs}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Avatars are public (shown on login, sharing screens)
|
// Avatars are public (shown on login, sharing screens)
|
||||||
app.use('/uploads/avatars', express.static(path.join(__dirname, '../uploads/avatars')));
|
app.use('/uploads/avatars', express.static(path.join(__dirname, '../uploads/avatars')));
|
||||||
|
|
||||||
@@ -181,6 +210,7 @@ const PORT = process.env.PORT || 3001;
|
|||||||
const server = app.listen(PORT, () => {
|
const server = app.listen(PORT, () => {
|
||||||
console.log(`TREK API running on port ${PORT}`);
|
console.log(`TREK API running on port ${PORT}`);
|
||||||
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||||
|
console.log(`Debug logs: ${DEBUG ? 'ENABLED' : 'disabled'}`);
|
||||||
if (process.env.DEMO_MODE === 'true') console.log('Demo mode: ENABLED');
|
if (process.env.DEMO_MODE === 'true') console.log('Demo mode: ENABLED');
|
||||||
if (process.env.DEMO_MODE === 'true' && process.env.NODE_ENV === 'production') {
|
if (process.env.DEMO_MODE === 'true' && process.env.NODE_ENV === 'production') {
|
||||||
console.warn('[SECURITY WARNING] DEMO_MODE is enabled in production! Demo credentials are publicly exposed.');
|
console.warn('[SECURITY WARNING] DEMO_MODE is enabled in production! Demo credentials are publicly exposed.');
|
||||||
|
|||||||
Reference in New Issue
Block a user