diff --git a/CHANGELOG.md b/CHANGELOG.md
index 32e149c1..9c50393f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -41,6 +41,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+### Changed
+
+* **host-console:** moved Host Console from Community tier to Admiral tier. The interactive host terminal now requires a Sencho Admiral license — enforced in the UI (nav item hidden, AdmiralGate upgrade prompt), the REST API (`POST /api/system/console-token` returns 403), and the WebSocket upgrade handler (`/api/system/host-console` rejects connections). Community and Skipper users see an upgrade prompt.
+
### Security
* **docker:** upgrade base image from `node:20-alpine` to `node:22-alpine` (Node 22.22.2 on Alpine 3.23.3) to remediate 31 CVEs (1 Critical, multiple High/Medium/Low) flagged by Docker Scout against the previous `node:20-alpine` base.
diff --git a/backend/src/__tests__/auth.test.ts b/backend/src/__tests__/auth.test.ts
index 9c4263c1..01a4fbfa 100644
--- a/backend/src/__tests__/auth.test.ts
+++ b/backend/src/__tests__/auth.test.ts
@@ -1,7 +1,7 @@
/**
* Tests for authentication: login, rate limiting, and auth middleware.
*/
-import { describe, it, expect, beforeAll, afterAll } from 'vitest';
+import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
import request from 'supertest';
import jwt from 'jsonwebtoken';
import { setupTestDb, cleanupTestDb, TEST_USERNAME, TEST_PASSWORD, TEST_JWT_SECRET } from './helpers/setupTestDb';
@@ -96,6 +96,17 @@ describe('authMiddleware', () => {
// ─── Protected endpoint: console-token ───────────────────────────────────────
describe('POST /api/system/console-token', () => {
+ // Console-token requires Admiral tier — mock LicenseService for the happy-path test
+ beforeAll(async () => {
+ const { LicenseService } = await import('../services/LicenseService');
+ vi.spyOn(LicenseService.getInstance(), 'getTier').mockReturnValue('pro');
+ vi.spyOn(LicenseService.getInstance(), 'getVariant').mockReturnValue('team');
+ });
+
+ afterAll(() => {
+ vi.restoreAllMocks();
+ });
+
it('returns 401 without authentication (was a security bug - C1 fix)', async () => {
const res = await request(app).post('/api/system/console-token');
expect(res.status).toBe(401);
diff --git a/backend/src/index.ts b/backend/src/index.ts
index 545072f3..e7a7fbed 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -2316,6 +2316,13 @@ server.on('upgrade', async (req, socket, head) => {
socket.destroy();
return;
}
+ // Admiral license gate — host console requires Pro (team variant)
+ const ls = LicenseService.getInstance();
+ if (ls.getTier() !== 'pro' || ls.getVariant() !== 'team') {
+ socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
+ socket.destroy();
+ return;
+ }
const hostConsoleWss = new WebSocketServer({ noServer: true });
hostConsoleWss.handleUpgrade(req, socket, head, (ws) => {
hostConsoleWss.close();
@@ -3362,6 +3369,7 @@ app.post('/api/system/console-token', authMiddleware, (req: Request, res: Respon
return;
}
if (!requireAdmin(req, res)) return;
+ if (!requireAdmiral(req, res)) return;
try {
const settings = DatabaseService.getInstance().getGlobalSettings();
const jwtSecret = settings.auth_jwt_secret;
diff --git a/docs/features/host-console.mdx b/docs/features/host-console.mdx
index a6e811a2..ff25f3ec 100644
--- a/docs/features/host-console.mdx
+++ b/docs/features/host-console.mdx
@@ -5,6 +5,10 @@ description: An interactive terminal on your host OS, directly in the browser -
The **Console** tab opens a full interactive terminal session on the machine running Sencho. It behaves exactly like an SSH session, but without needing an SSH server, client, or key management.
+
+ The Host Console requires a **Sencho Admiral** license. Community and Skipper users see an upgrade prompt instead. [Learn more about licensing](/features/licensing).
+
+
@@ -34,12 +38,25 @@ The shell depends on the host OS:
- **Linux/macOS hosts:** `bash` or `sh`
- **Windows hosts (Docker Desktop):** PowerShell (as shown in the screenshot - the console opens inside the Sencho container's Windows environment)
+## Availability
+
+The Host Console is available to **admin** users on the **Admiral** tier. Access is enforced at every layer:
+
+- **UI:** The Console tab only appears in the navigation bar for Admiral-licensed admin users.
+- **API:** The `POST /api/system/console-token` endpoint returns `403 Forbidden` without an Admiral license.
+- **WebSocket:** The `/api/system/host-console` upgrade handler rejects connections without an Admiral license.
+
+### What Community and Skipper users see
+
+If you are on the Community or Skipper tier, navigating to the Console view shows an upgrade prompt with a link to the [pricing page](https://sencho.io/pricing). The Console tab is hidden from the navigation bar entirely.
+
## Security model
The host console has a stricter authentication requirement than other features:
- Requires a valid **browser session** (httpOnly cookie). Node-proxy tokens used for multi-node communication are explicitly blocked.
- Each session is issued a short-lived **console token** (60-second TTL) before the WebSocket is established.
+- Access is restricted to the **Admiral** license tier, ensuring only teams with appropriate licensing can use interactive shell access.
- Because the console gives full shell access to the host, you should secure your Sencho instance with HTTPS and a strong password if it is exposed to a network.
diff --git a/docs/features/licensing.mdx b/docs/features/licensing.mdx
index 36c83c95..ad3f6eab 100644
--- a/docs/features/licensing.mdx
+++ b/docs/features/licensing.mdx
@@ -14,14 +14,14 @@ Sencho uses an open-core model. The **Community** tier is free forever with unli
| Tier | Price | Accounts |
|------|-------|----------|
| **Community** | Free | 1 admin |
-| **Skipper** | $7.99/month, $69.99/year, or $249 lifetime | 1 admin + 3 viewers |
-| **Admiral** | $49.99/month, $499.99/year, or $1,499 lifetime | Unlimited |
+| **Skipper** | $5.99/month billed annually, $7.99/month, or $249 lifetime | 1 admin + 3 viewers |
+| **Admiral** | $41.99/month billed annually, $49.99/month, or $1,499 lifetime | Unlimited |
Lifetime pricing is an early-adopter offer available for a limited time only.
## Free trial
-Every new Sencho installation starts with a **14-day Skipper trial** - no license key or credit card required. Skipper features like fleet management, RBAC viewer accounts, webhooks, and atomic deployments are unlocked during the trial so you can evaluate them with your real infrastructure. Admiral features (SSO, audit log, unlimited accounts) require an Admiral license.
+Every new Sencho installation starts with a **14-day Skipper trial** - no license key or credit card required. Skipper features like fleet management, RBAC viewer accounts, webhooks, and atomic deployments are unlocked during the trial so you can evaluate them with your real infrastructure. Admiral features (SSO, audit log, host console, unlimited accounts) require an Admiral license.
When the trial expires, Sencho automatically reverts to the Community tier. No data is lost.
diff --git a/docs/images/host-console/host-console-overview.png b/docs/images/host-console/host-console-overview.png
index 5d776562..1a23242a 100644
Binary files a/docs/images/host-console/host-console-overview.png and b/docs/images/host-console/host-console-overview.png differ
diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx
index 78ea6744..b3de34ba 100644
--- a/frontend/src/components/EditorLayout.tsx
+++ b/frontend/src/components/EditorLayout.tsx
@@ -7,6 +7,7 @@ import ErrorBoundary from './ErrorBoundary';
import HomeDashboard from './HomeDashboard';
import BashExecModal from './BashExecModal';
import HostConsole from './HostConsole';
+import { AdmiralGate } from './AdmiralGate';
import ResourcesView from './ResourcesView';
import { Button } from './ui/button';
import { Input } from './ui/input';
@@ -161,13 +162,13 @@ export default function EditorLayout() {
{ value: 'dashboard', label: 'Home', icon: Home },
{ value: 'fleet', label: 'Fleet', icon: Radar },
];
- if (isAdmin) items.push({ value: 'host-console', label: 'Console', icon: Terminal });
items.push(
{ value: 'resources', label: 'Resources', icon: HardDrive },
{ value: 'templates', label: 'App Store', icon: CloudDownload },
{ value: 'global-observability', label: 'Logs', icon: Activity },
);
if (isPro && license?.variant === 'team') {
+ if (isAdmin) items.push({ value: 'host-console', label: 'Console', icon: Terminal });
if (can('system:audit')) items.push({ value: 'audit-log', label: 'Audit', icon: ScrollText });
if (isAdmin) items.push({ value: 'scheduled-ops', label: 'Schedules', icon: Clock });
}
@@ -1479,7 +1480,9 @@ export default function EditorLayout() {
) : activeView === 'resources' ? (
) : activeView === 'host-console' ? (
- setActiveView(selectedFile ? 'editor' : 'dashboard')} />
+
+ setActiveView(selectedFile ? 'editor' : 'dashboard')} />
+
) : !isLoading && selectedFile && activeView === 'editor' ? (