Skip to content

Commit 03b7704

Browse files
committed
feat(satellite): validate GitHub deployment base directory permissions
1 parent 5957e5b commit 03b7704

File tree

3 files changed

+136
-0
lines changed

3 files changed

+136
-0
lines changed

services/satellite/Dockerfile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ RUN pip3 install --break-system-packages uv && \
2828
RUN mkdir -p /opt/deploystack/mcp-cache && \
2929
chown -R deploystack:deploystack /opt/deploystack
3030

31+
# Create GitHub deployment base directory for production tmpfs mounts
32+
# Required when NODE_ENV=production and platform=linux
33+
RUN mkdir -p /opt/mcp-deployments && \
34+
chown deploystack:deploystack /opt/mcp-deployments && \
35+
chmod 755 /opt/mcp-deployments
36+
3137
WORKDIR /app
3238

3339
# Create persistent_data directory with proper ownership
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { access, constants } from 'fs/promises';
2+
import { Logger } from 'pino';
3+
4+
/**
5+
* Validate GitHub deployment base directory permissions
6+
* Follows fail-fast pattern - satellite cannot start without proper permissions
7+
*
8+
* This check runs only in production on Linux where:
9+
* - Satellite runs as 'deploystack' user (UID 1001)
10+
* - /opt/mcp-deployments must exist with deploystack:deploystack ownership
11+
* - Directory must have read/write permissions for tmpfs mount operations
12+
*/
13+
export async function validateDeploymentDirectory(
14+
baseDir: string,
15+
logger: Logger
16+
): Promise<void> {
17+
logger.info({
18+
operation: 'deployment_directory_validation_start',
19+
path: baseDir
20+
}, 'Validating GitHub deployment base directory permissions...');
21+
22+
try {
23+
// Check if directory exists
24+
await access(baseDir, constants.F_OK);
25+
26+
logger.debug({
27+
operation: 'deployment_directory_exists',
28+
path: baseDir
29+
}, 'GitHub deployment base directory exists');
30+
} catch (error) {
31+
const accessError = error as NodeJS.ErrnoException;
32+
33+
if (accessError.code === 'ENOENT') {
34+
logger.fatal({
35+
operation: 'deployment_directory_missing',
36+
path: baseDir,
37+
error: 'Directory does not exist',
38+
fix_command: `sudo mkdir -p ${baseDir} && sudo chown deploystack:deploystack ${baseDir} && sudo chmod 755 ${baseDir}`,
39+
help: 'Create directory with correct ownership during satellite setup'
40+
}, `❌ FATAL: GitHub deployment base directory does not exist: ${baseDir}`);
41+
42+
throw new Error(
43+
`GitHub deployment base directory missing: ${baseDir}. ` +
44+
`Fix: sudo mkdir -p ${baseDir} && sudo chown deploystack:deploystack ${baseDir}`
45+
);
46+
} else {
47+
// Unexpected error accessing directory
48+
logger.fatal({
49+
operation: 'deployment_directory_access_error',
50+
path: baseDir,
51+
error: accessError.message
52+
}, `❌ FATAL: Cannot access GitHub deployment base directory: ${baseDir}`);
53+
54+
throw new Error(`Failed to access deployment base directory: ${accessError.message}`);
55+
}
56+
}
57+
58+
// Check read permission
59+
try {
60+
await access(baseDir, constants.R_OK);
61+
logger.debug({
62+
operation: 'deployment_directory_readable',
63+
path: baseDir
64+
}, 'GitHub deployment base directory is readable');
65+
} catch (error) {
66+
const readError = error as NodeJS.ErrnoException;
67+
68+
logger.fatal({
69+
operation: 'deployment_directory_not_readable',
70+
path: baseDir,
71+
error: readError.message,
72+
current_user: process.env.USER || 'unknown',
73+
fix_command: `sudo chown -R deploystack:deploystack ${baseDir} && sudo chmod 755 ${baseDir}`,
74+
help: 'Directory must be readable by deploystack user (UID 1001)'
75+
}, `❌ FATAL: No read permission for GitHub deployment base directory: ${baseDir}`);
76+
77+
throw new Error(
78+
`No read permission for ${baseDir}. ` +
79+
`Fix: sudo chown deploystack:deploystack ${baseDir} && sudo chmod 755 ${baseDir}`
80+
);
81+
}
82+
83+
// Check write permission
84+
try {
85+
await access(baseDir, constants.W_OK);
86+
logger.debug({
87+
operation: 'deployment_directory_writable',
88+
path: baseDir
89+
}, 'GitHub deployment base directory is writable');
90+
} catch (error) {
91+
const writeError = error as NodeJS.ErrnoException;
92+
93+
logger.fatal({
94+
operation: 'deployment_directory_not_writable',
95+
path: baseDir,
96+
error: writeError.message,
97+
current_user: process.env.USER || 'unknown',
98+
fix_command: `sudo chown -R deploystack:deploystack ${baseDir} && sudo chmod 755 ${baseDir}`,
99+
help: 'Directory must be writable by deploystack user (UID 1001) to create tmpfs mounts'
100+
}, `❌ FATAL: No write permission for GitHub deployment base directory: ${baseDir}`);
101+
102+
throw new Error(
103+
`No write permission for ${baseDir}. ` +
104+
`Fix: sudo chown deploystack:deploystack ${baseDir} && sudo chmod 755 ${baseDir}`
105+
);
106+
}
107+
108+
logger.info({
109+
operation: 'deployment_directory_validation_success',
110+
path: baseDir
111+
}, `✅ GitHub deployment base directory validated: ${baseDir}`);
112+
}

services/satellite/src/server.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,24 @@ export async function createServer() {
237237
process.exit(1);
238238
}
239239

240+
// Validate GitHub deployment base directory permissions (production Linux only)
241+
if (process.env.NODE_ENV === 'production' && process.platform === 'linux') {
242+
const { githubDeploymentBaseDir } = await import('./config/nsjail');
243+
const { validateDeploymentDirectory } = await import('./lib/deployment-directory-validator');
244+
245+
try {
246+
await validateDeploymentDirectory(githubDeploymentBaseDir, server.log as any);
247+
} catch (error) {
248+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
249+
server.log.fatal({
250+
operation: 'deployment_directory_validation_failed',
251+
path: githubDeploymentBaseDir,
252+
error: errorMessage
253+
}, 'Failed to validate GitHub deployment base directory - cannot continue');
254+
process.exit(1);
255+
}
256+
}
257+
240258
// Initialize MCP Activity Tracker for personal dashboard feature
241259
const activityTracker = new McpActivityTracker(server.log);
242260
server.decorate('activityTracker', activityTracker);

0 commit comments

Comments
 (0)