
2024-11
Enterprise Access Control System
Locally secure, globally accessible — SSH tunneling for on-premises door controllers
Overview
Physical access control systems are typically air-gapped or VPN-dependent — both approaches carry significant operational overhead. This project delivers a cloud-connected door management interface that allows authorized users to lock, unlock, and audit physical doors from anywhere, without a VPN, by routing traffic through a persistent SSH reverse tunnel.
The architecture keeps the attack surface minimal: the door controllers are never directly exposed to the internet, all traffic is encrypted end-to-end, and the cloud server acts only as a relay.
System Architecture
Browser (HTTPS)
│
▼
Nginx (Cloud Server)
│ reverse proxy → localhost:3001
▼
SSH Tunnel endpoint
│ encrypted tunnel
▼
Python Tunnel Manager (On-premises)
│ forwards to
▼
Node.js / Express API (On-premises)
│ UHPPOTED SDK
▼
Door Controllers (LAN)The cloud server knows nothing about the door hardware. It only receives authenticated requests and forwards them through the tunnel. The on-premises Express server handles all controller communication and authorization logic.
SSH Tunnel Manager
The Python component maintains the tunnel and handles reconnection automatically. A health monitor thread pings the tunnel connection every 30 seconds and re-establishes it if the connection has dropped.
import subprocess
import threading
import time
import logging
class TunnelManager:
def __init__(self, remote_host, remote_port, local_port, ssh_key):
self.remote_host = remote_host
self.remote_port = remote_port
self.local_port = local_port
self.ssh_key = ssh_key
self.process = None
self.stats = {"reconnects": 0, "uptime_start": time.time()}
def start(self):
cmd = [
"ssh", "-N", "-R",
f"{self.remote_port}:localhost:{self.local_port}",
"-i", self.ssh_key,
"-o", "ServerAliveInterval=30",
"-o", "ServerAliveCountMax=3",
"-o", "ExitOnForwardFailure=yes",
f"tunnel@{self.remote_host}"
]
self.process = subprocess.Popen(cmd)
logging.info(f"Tunnel established → {self.remote_host}:{self.remote_port}")
def health_monitor(self):
while True:
time.sleep(30)
if self.process.poll() is not None:
logging.warning("Tunnel dropped — reconnecting")
self.stats["reconnects"] += 1
self.start()
def run(self):
self.start()
monitor = threading.Thread(target=self.health_monitor, daemon=True)
monitor.start()
self.process.wait()Express API
The Node.js backend manages sessions, rate limiting, and communicates with physical controllers via the UHPPOTED SDK.
import express from "express";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import session from "express-session";
import { uhppoted } from "./controllers.js";
const app = express();
app.use(helmet());
app.use(express.json());
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, maxAge: 3600000 }
}));
const limiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 100 });
app.use("/api", limiter);
// Unlock a door (authenticated)
app.post("/api/doors/:id/unlock", requireAuth, async (req, res) => {
const { id } = req.params;
try {
await uhppoted.openDoor(id);
logger.info({ user: req.session.user, door: id, action: "unlock" });
res.json({ success: true });
} catch (err) {
logger.error({ err, door: id });
res.status(500).json({ error: "Controller unreachable" });
}
});Security Considerations
No inbound ports on-premises. The SSH tunnel is outbound-only from the site. There is no firewall rule allowing inbound connections to the door controllers or the Express server.
Dedicated tunnel user. The tunnel SSH account on the cloud server has no shell access (/bin/false), no home directory write access, and can only bind to the designated forwarding port.
Rate limiting + session auth. Every API endpoint requires an active session. Rate limiting prevents brute-force attempts against the authentication endpoint even if someone gains access to the cloud server's URL.
Structured audit log. Every door event is logged with the authenticated user, timestamp, door ID, and action. This log is immutable from the web interface and replicated off-site nightly.
Outcome
Replaced a manual on-site process (physically driving to unlock a door for a late technician) with a 10-second remote action. The system has maintained 99.9%+ tunnel uptime across several months, with automatic reconnection handling the handful of ISP-side drops without any intervention.
The architecture proved reusable — the same tunnel pattern has since been applied to remote-monitor two other on-premises systems at the same company.