Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ RUN apt-get update && apt-get install -y \
libxrandr2 \
xdg-utils \
dumb-init \
gosu \
&& rm -rf /var/lib/apt/lists/*

# Set Chrome executable path for Puppeteer
Expand All @@ -73,9 +74,9 @@ COPY --from=builder /app/dist ./dist
RUN mkdir -p ./data/sessions ./data/media && \
chown -R openwa:openwa /app

# Note: Running as root to allow Docker socket access for orchestration
# For production with stricter security, consider using a Docker socket proxy
# USER openwa
# Copy entrypoint script (runs as root to fix volume permissions, then drops to openwa)
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

# Expose port
EXPOSE 2785
Expand All @@ -84,6 +85,6 @@ EXPOSE 2785
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD node -e "require('http').get('http://localhost:2785/api/health', (r) => process.exit(r.statusCode === 200 ? 0 : 1))"

# Start with dumb-init to handle signals properly
ENTRYPOINT ["dumb-init", "--"]
# Entrypoint: fixes volume permissions as root, then drops to openwa user via gosu
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD ["node", "dist/main"]
4 changes: 2 additions & 2 deletions dashboard/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ server {

# Proxy API requests to backend
location /api/ {
proxy_pass http://openwa:2785;
proxy_pass http://openwa-api:2785;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
Expand All @@ -25,7 +25,7 @@ server {

# WebSocket proxy
location /socket.io/ {
proxy_pass http://openwa:2785;
proxy_pass http://openwa-api:2785;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
Expand Down
24 changes: 23 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
# Smart Orchestration with Profiles

services:
# ===== CORE: Docker Socket Proxy (restricts Docker API access) =====
docker-proxy:
image: tecnativa/docker-socket-proxy
container_name: openwa-docker-proxy
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
CONTAINERS: 1
IMAGES: 1
VOLUMES: 1
INFO: 1
PING: 1
POST: 1
DELETE: 1
labels:
- 'com.openwa.service=docker-proxy'
- 'com.openwa.core=true'

# ===== CORE: Traefik Reverse Proxy =====
traefik:
image: traefik:v3.0
Expand Down Expand Up @@ -70,11 +89,14 @@ services:
- PLUGINS_DIR=/app/data/plugins
# Security
- API_MASTER_KEY=${API_MASTER_KEY:-}
# Docker socket proxy (replaces direct socket mount)
- DOCKER_HOST=tcp://docker-proxy:2375
volumes:
- openwa-data:/app/data
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./docker-compose.yml:/app/docker-compose.yml:ro
depends_on:
docker-proxy:
condition: service_started
postgres:
condition: service_healthy
required: false
Expand Down
8 changes: 8 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh
set -e

# Fix data directory permissions for named volume mounts (runs as root before dropping privileges)
mkdir -p /app/data/sessions /app/data/media /app/data/plugins
chown -R openwa:openwa /app/data

exec gosu openwa dumb-init "$@"
10 changes: 8 additions & 2 deletions src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module, DynamicModule, Type } from '@nestjs/common';
import { Module, DynamicModule, Type, NestModule, MiddlewareConsumer } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ThrottlerModule } from '@nestjs/throttler';
Expand All @@ -24,6 +24,7 @@ import { StatsModule } from './modules/stats/stats.module';
import { StatusModule } from './modules/status/status.module';
import { CatalogModule } from './modules/catalog/catalog.module';
import { HooksModule } from './core/hooks';
import { BullBoardAuthMiddleware } from './common/security/bull-board-auth.middleware';
import { PluginsModule } from './core/plugins';
import { PluginsApiModule } from './modules/plugins/plugins.module';

Expand All @@ -38,6 +39,7 @@ if (process.env.QUEUE_ENABLED === 'true') {
}

@Module({
providers: [BullBoardAuthMiddleware],
imports: [
// Configuration
ConfigModule.forRoot({
Expand Down Expand Up @@ -161,4 +163,8 @@ if (process.env.QUEUE_ENABLED === 'true') {
PluginsApiModule, // Phase 5: Plugins API
],
})
export class AppModule {}
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer): void {
consumer.apply(BullBoardAuthMiddleware).forRoutes('/admin/queues');
}
}
31 changes: 31 additions & 0 deletions src/common/security/bull-board-auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../../modules/auth/auth.service';
import { ApiKeyRole } from '../../modules/auth/entities/api-key.entity';

@Injectable()
export class BullBoardAuthMiddleware implements NestMiddleware {
constructor(private readonly authService: AuthService) {}

async use(req: Request, res: Response, next: NextFunction): Promise<void> {
const xApiKey = req.headers['x-api-key'] as string;
const authHeader = req.headers['authorization'];
const rawKey = xApiKey || (authHeader?.startsWith('Bearer ') ? authHeader.substring(7) : undefined);

if (!rawKey) {
res.status(401).json({ message: 'API key required' });
return;
}

try {
const apiKey = await this.authService.validateApiKey(rawKey);
if (!this.authService.hasPermission(apiKey, ApiKeyRole.ADMIN)) {
res.status(403).json({ message: 'Admin role required' });
return;
}
next();
} catch {
res.status(401).json({ message: 'Invalid API key' });
}
}
}
14 changes: 12 additions & 2 deletions src/modules/docker/docker.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,23 @@ export class DockerService implements OnModuleInit {

private async initializeDocker(): Promise<void> {
try {
this.docker = new Docker({ socketPath: '/var/run/docker.sock' });
const dockerHost = process.env.DOCKER_HOST;
if (dockerHost?.startsWith('tcp://')) {
const url = new URL(dockerHost);
this.docker = new Docker({
host: url.hostname,
port: parseInt(url.port, 10) || 2375,
protocol: 'http' as const,
});
} else {
this.docker = new Docker({ socketPath: '/var/run/docker.sock' });
}
await this.docker.ping();
this.isAvailable = true;
this.logger.log('Docker API connected successfully');
} catch (error) {
this.logger.warn(
'Docker socket not available. Container orchestration disabled.',
'Docker not available. Container orchestration disabled.',
error instanceof Error ? error.message : error,
);
this.isAvailable = false;
Expand Down