EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
MeshCentral Tenant Isolation

Overview

All MeshCentral endpoints enforce strict tenant isolation to ensure customers can only access their own devices, while the root tenant (tenant_id=1) maintains administrative access to all devices.

Security Model

Tenant Isolation Rules

  1. Regular Tenants (tenant_id > 1): Can only access agents where agent.tenant_id = user.tenant_id
  2. Root Tenant (tenant_id = 1): Can access ALL agents across all tenants (administrative access)
  3. Agent Self-Access: Agents can access their own data using agent_uuid (no tenant check needed)

SQL Pattern

All queries use this pattern:

WHERE (tenant_id = $user_tenant_id OR $user_tenant_id = 1)

This ensures:

  • Regular tenants see only their data
  • Root tenant sees everything
  • No cross-tenant data leakage

Protected Endpoints

1. POST /api/meshcentral/session/:agentId

Purpose: Create secure MeshCentral session

Tenant Protection:

// Line 48-50: Checks agent belongs to user's tenant
'SELECT ... FROM agents WHERE (agent_id::text = $1 OR agent_uuid = $1) AND tenant_id = $2',
[agentId, req.user.tenantId]

Behavior:

  • ✅ User can access agents in their tenant
  • ✅ Root tenant user can access any agent
  • ❌ User cannot access agents from other tenants

2. ALL /api/meshcentral/proxy/:sessionToken/*

Purpose: Proxy MeshCentral requests securely

Tenant Protection:

// Lines 115-121: Validates session belongs to tenant's agent
FROM meshcentral_sessions s
JOIN agents a ON s.agent_id = a.agent_id
JOIN users u ON s.user_id = u.user_id
WHERE s.session_token = $1
AND s.expires_at > NOW()
AND (a.tenant_id = u.tenant_id OR u.tenant_id = 1)

Behavior:

  • ✅ Session only works if agent belongs to user's tenant
  • ✅ Root tenant sessions work for any agent
  • ❌ Cannot use another tenant's session token

3. GET /api/meshcentral/devices

Purpose: List all MeshCentral devices

Tenant Protection:

// Lines 288-301: Filters devices by tenant ownership
if (req.user.tenantId !== 1) {
const tenantAgents = await db.query(
'SELECT meshcentral_nodeid FROM agents WHERE tenant_id = $1',
[req.user.tenantId]
);
const tenantNodeIds = new Set(tenantAgents.rows.map(a => a.meshcentral_nodeid));
filteredNodes = parsedNodes.filter(node => tenantNodeIds.has(node.nodeId));
}

Behavior:

  • Regular tenants: Only see devices linked to their agents
  • Root tenant: Sees all devices
  • Empty list if tenant has no MeshCentral-linked agents

4. POST /api/meshcentral/sync

Purpose: Sync MeshCentral devices to agents table

Tenant Protection:

// Lines 332-338: Filters sync to tenant's devices
if (req.user.tenantId !== 1) {
const tenantAgents = await db.query(
'SELECT meshcentral_nodeid FROM agents WHERE tenant_id = $1',
[req.user.tenantId]
);
nodes = nodes.filter(node => tenantNodeIds.has(parsed.nodeId));
}

Behavior:

  • Regular tenants: Only sync their existing agents
  • Root tenant: Can sync all devices
  • Prevents creating agents in wrong tenant

5. POST /api/meshcentral/status/:agentId

Purpose: Get agent MeshCentral status

Tenant Protection:

// Line 221: Filters by tenant ownership
'SELECT ... FROM agents WHERE agent_id = $1 AND tenant_id = $2',
[agentId, req.user.tenantId]

Behavior:

  • ✅ User can check status of their agents
  • ✅ Root tenant can check any agent
  • ❌ Returns 404 for agents from other tenants

6. POST /api/meshcentral/connect/:agentId

Purpose: Generate MeshCentral login token (legacy)

Tenant Protection:

// Line 157: Filters by tenant ownership
'SELECT ... FROM agents WHERE agent_id = $1 AND tenant_id = $2',
[agentId, req.user.tenantId]

Behavior:

  • ✅ User can connect to their agents
  • ✅ Root tenant can connect to any agent
  • ❌ Returns 404 for agents from other tenants

7. POST /api/meshcentral/auto-link

Purpose: Auto-link MeshAgent by hostname

Tenant Protection:

// Lines 466-472: Optional authentication with tenant check
const tenantFilter = req.user ? ' AND (tenant_id = $2 OR $2 = 1)' : '';
const agentResult = await db.query(
`SELECT ... FROM agents WHERE agent_uuid = $1${tenantFilter}`,
params
);

Behavior:

  • Authenticated: Can only link agents in their tenant
  • Unauthenticated: Agent self-linking (no tenant check)
  • Root tenant: Can link any agent

8. GET /api/agent/:agentId/meshcentral-url

Purpose: Get MeshCentral remote desktop URL

Tenant Protection:

// Lines 195-213: Enforces tenant isolation for authenticated requests
if (req.user) {
query = 'SELECT ... WHERE agent_id = $1 AND (tenant_id = $2 OR $2 = 1)';
params = [agentId, req.user.tenantId];
} else {
// Only allow UUID lookup for unauthenticated (agent) calls
if (isNumeric) {
return res.status(401).json({ error: 'Authentication required' });
}
query = 'SELECT ... WHERE agent_uuid = $1';
}

Behavior:

  • Authenticated users: Can only access their tenant's agents
  • Agents: Can access their own data via UUID
  • Numeric IDs require authentication (prevents guessing agent_id values)

Testing Tenant Isolation

Test Case 1: Regular Tenant Access

# Login as tenant 2 user
curl -X POST https://backend/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"tenant2@example.com","password":"***"}'
# Try to access tenant 1's agent (should fail)
curl -X POST https://backend/api/meshcentral/session/115 \
-H "Authorization: Bearer $TENANT2_JWT"
# Expected: 404 Agent not found
# Access own agent (should succeed)
curl -X POST https://backend/api/meshcentral/session/200 \
-H "Authorization: Bearer $TENANT2_JWT"
# Expected: 200 Success with session token

Test Case 2: Root Tenant Access

# Login as root tenant user
curl -X POST https://backend/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@everydaytech.au","password":"***"}'
# Access any agent (should succeed)
curl -X POST https://backend/api/meshcentral/session/115 \
-H "Authorization: Bearer $ROOT_JWT"
# Expected: 200 Success
curl -X POST https://backend/api/meshcentral/session/200 \
-H "Authorization: Bearer $ROOT_JWT"
# Expected: 200 Success

Test Case 3: Session Token Validation

# Tenant 2 creates session for their agent
SESSION_TOKEN=$(curl -X POST https://backend/api/meshcentral/session/200 \
-H "Authorization: Bearer $TENANT2_JWT" | jq -r .sessionToken)
# Try to access proxy with Tenant 1's JWT (should fail)
curl https://backend/api/meshcentral/proxy/$SESSION_TOKEN/desktop \
-H "Authorization: Bearer $TENANT1_JWT"
# Expected: 401 Invalid or expired session
# Access with correct tenant (should succeed)
curl https://backend/api/meshcentral/proxy/$SESSION_TOKEN/desktop \
-H "Authorization: Bearer $TENANT2_JWT"
# Expected: 200 Success

Test Case 4: Device List Filtering

# Tenant 2 lists devices (should only see their own)
curl https://backend/api/meshcentral/devices \
-H "Authorization: Bearer $TENANT2_JWT"
# Expected: Only devices linked to tenant 2 agents
# Root tenant lists devices (should see all)
curl https://backend/api/meshcentral/devices \
-H "Authorization: Bearer $ROOT_JWT"
# Expected: All devices across all tenants

Database Verification

Check Tenant Ownership

-- See which tenants own which agents
SELECT
t.tenant_name,
COUNT(a.agent_id) as agent_count,
COUNT(CASE WHEN a.meshcentral_nodeid IS NOT NULL THEN 1 END) as meshcentral_linked
FROM tenants t
LEFT JOIN agents a ON t.tenant_id = a.tenant_id
GROUP BY t.tenant_id, t.tenant_name
ORDER BY t.tenant_id;

Find Cross-Tenant Leaks

-- This should return 0 rows (no sessions crossing tenant boundaries)
SELECT
s.session_id,
s.session_token,
u.user_id,
u.tenant_id as user_tenant,
a.agent_id,
a.tenant_id as agent_tenant
FROM meshcentral_sessions s
JOIN users u ON s.user_id = u.user_id
JOIN agents a ON s.agent_id = a.agent_id
WHERE u.tenant_id != a.tenant_id AND u.tenant_id != 1;

Verify Root Tenant Access

-- Root tenant (tenant_id=1) should be able to query any agent
SELECT agent_id, tenant_id, hostname, meshcentral_nodeid
FROM agents
WHERE (tenant_id = 1 OR 1 = 1); -- Root tenant sees all
-- Regular tenant should only see their own
SELECT agent_id, tenant_id, hostname, meshcentral_nodeid
FROM agents
WHERE (tenant_id = 2 OR 2 = 1); -- Tenant 2 sees only their agents

Security Best Practices

1. Always Use Parameterized Queries

BAD:

const query = `SELECT * FROM agents WHERE tenant_id = ${req.user.tenantId}`;

GOOD:

const query = 'SELECT * FROM agents WHERE tenant_id = $1';
const result = await db.query(query, [req.user.tenantId]);

2. Validate Tenant Ownership Before Updates

// Always check tenant ownership before updating
const result = await db.query(
'SELECT tenant_id FROM agents WHERE agent_id = $1',
[agentId]
);
if (result.rows[0].tenant_id !== req.user.tenantId && req.user.tenantId !== 1) {
return res.status(403).json({ error: 'Access denied' });
}
// Proceed with update

3. Log Suspicious Access Attempts

if (agentResult.rows.length === 0) {
console.warn(`⚠️ Tenant ${req.user.tenantId} attempted to access agent ${agentId} - not found or unauthorized`);
return res.status(404).json({ error: 'Agent not found' });
}

4. Session Token Validation

// Session tokens must validate:
// 1. Token exists and not expired
// 2. User still exists
// 3. Agent still exists
// 4. Agent belongs to user's tenant (or user is root)

Migration Notes

Existing Agents

When deploying tenant isolation:

  1. All existing agents should already have tenant_id set
  2. If any agents have NULL tenant_id, assign them to root tenant:
    UPDATE agents SET tenant_id = 1 WHERE tenant_id IS NULL;

Existing Sessions

After deploying, existing sessions may cross tenant boundaries:

-- Find and expire invalid sessions
UPDATE meshcentral_sessions
SET expires_at = NOW()
WHERE session_id IN (
SELECT s.session_id
FROM meshcentral_sessions s
JOIN users u ON s.user_id = u.user_id
JOIN agents a ON s.agent_id = a.agent_id
WHERE u.tenant_id != a.tenant_id AND u.tenant_id != 1
);

Root Tenant Special Privileges

The root tenant (tenant_id=1) has special administrative privileges:

Can Do:

  • ✅ View all agents from all tenants
  • ✅ Create MeshCentral sessions for any agent
  • ✅ Sync all MeshCentral devices
  • ✅ Link any agent to MeshCentral
  • ✅ View all sessions in database

Should Not Do:

  • ❌ Should not modify tenant_id of agents (breaks isolation)
  • ❌ Should not impersonate non-root users without proper audit logging
  • ❌ Should not disable tenant isolation checks

Compliance & Audit

Required Logging

All MeshCentral access should be logged:

console.log(`[MeshCentral] User ${req.user.userId} (tenant ${req.user.tenantId}) accessed agent ${agentId}`);

Audit Trail

The meshcentral_sessions table provides audit trail:

  • Who accessed which agent
  • When they accessed it
  • How long the session lasted
  • Which MeshCentral features were used (view_mode)

Data Retention

-- Cleanup old sessions (keep for 90 days for audit)
DELETE FROM meshcentral_sessions
WHERE created_at < NOW() - INTERVAL '90 days';

Summary

Implemented:

  • Tenant isolation on all MeshCentral endpoints
  • Root tenant (tenant_id=1) has administrative access
  • Session tokens validate tenant ownership
  • Device lists filtered by tenant
  • Sync operations respect tenant boundaries
  • Auto-link respects tenant scope

Security:

  • No cross-tenant data leakage
  • Parameterized queries prevent SQL injection
  • Session validation prevents unauthorized access
  • Audit trail for compliance

Root Tenant:

  • Full access to all agents and devices
  • Can perform administrative operations
  • Special privileges for support and monitoring