Add complete CyberRanger research archive — 200 files
- 86 modelfiles: Full system prompt evolution V1-V42.6 (54 extracted from Ollama backup + 32 original Modelfiles) - 30 training datasets: V6-V22 training JSONs + caring awareness data - 10 Colab notebooks: Training + merge scripts - 19 evaluation files: Drift results, ASR charts, verification - 5 test suites: Injection tests, regression tests - 4 observations: V24-V33 testing results + visual summaries - 38 identity files: Claude/Gemini/Ollama identity architecture - 7 security files: Injection research, manipulation analysis - 3 psychology files: Psychology Layer, Milgram chapter, David's thoughts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,638 @@
|
||||
# 🛡️ RANGERBLOCK SECURITY INTEGRATION PLAN v2.0
|
||||
## Project Codename: "SHEPHERD PROTOCOL"
|
||||
### Unified Identity, Registration & App Sync System
|
||||
|
||||
---
|
||||
|
||||
## REVISION NOTES (v2.0)
|
||||
- Added: ranger-chat-lite ↔ RangerPlex bidirectional sync
|
||||
- Added: On-chain identity registration
|
||||
- Added: Settings migration (lite → full app)
|
||||
- Added: First-app security considerations (comprehensive)
|
||||
- Added: Missing security layers David didn't know to ask for
|
||||
|
||||
---
|
||||
|
||||
## 1. EXECUTIVE SUMMARY
|
||||
|
||||
### The Vision
|
||||
```
|
||||
User Journey Option A (Chat First):
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Downloads │────>│ Identity │────>│ Later adds │
|
||||
│ Chat Lite │ │ Created + │ │ RangerPlex │
|
||||
│ (free/easy) │ │ On-Chain │ │ (full app) │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
│ │
|
||||
└────────────────────┘
|
||||
SEAMLESS SYNC!
|
||||
(settings, history, keys)
|
||||
|
||||
User Journey Option B (RangerPlex First):
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ Downloads │────>│ Full │────>│ Chat Lite │
|
||||
│ RangerPlex │ │ Identity │ │ auto-links │
|
||||
│ (power user)│ │ On-Chain │ │ to existing │
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
### Key Principles
|
||||
1. **Identity is PORTABLE** - One identity, all apps
|
||||
2. **Blockchain is TRUTH** - On-chain registration = verified
|
||||
3. **Local is FALLBACK** - Works offline, syncs when connected
|
||||
4. **Upgrade is SEAMLESS** - Lite → Full with zero friction
|
||||
5. **Security is INVISIBLE** - Users don't see complexity
|
||||
|
||||
---
|
||||
|
||||
## 2. ON-CHAIN IDENTITY REGISTRATION
|
||||
|
||||
### Why On-Chain?
|
||||
- **Proof of existence** - Timestamp when identity created
|
||||
- **Immutable record** - Can't be faked retroactively
|
||||
- **Cross-app verification** - Any app can verify identity
|
||||
- **Recovery mechanism** - Blockchain = backup
|
||||
|
||||
### Identity Block Structure
|
||||
```javascript
|
||||
{
|
||||
type: 'IDENTITY_REGISTRATION',
|
||||
version: '1.0.0',
|
||||
payload: {
|
||||
// Public data (visible on chain)
|
||||
publicKey: 'RSA-2048 public key (PEM)',
|
||||
hardwareIdHash: 'SHA-256 of hardware ID (not raw ID!)',
|
||||
nickname: 'IrishRanger',
|
||||
appOrigin: 'ranger-chat-lite', // Which app created this
|
||||
capabilities: ['chat', 'voice', 'files'],
|
||||
|
||||
// Timestamps
|
||||
createdAt: '2024-12-03T12:00:00.000Z',
|
||||
registeredOnChain: '2024-12-03T12:00:05.000Z',
|
||||
|
||||
// Signature
|
||||
signature: 'self-signed with private key'
|
||||
},
|
||||
metadata: {
|
||||
blockHeight: 12345,
|
||||
previousHash: 'abc123...',
|
||||
nonce: 42
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Registration Flow
|
||||
```
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ CLIENT │ │ RELAY/HUB │ │ BLOCKCHAIN │
|
||||
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
|
||||
│ │ │
|
||||
│ 1. Generate identity │ │
|
||||
│ locally first │ │
|
||||
│ │ │
|
||||
│ 2. Connect to relay │ │
|
||||
├───────────────────────>│ │
|
||||
│ │ │
|
||||
│ 3. Submit identity │ │
|
||||
│ registration block │ │
|
||||
├───────────────────────>│ │
|
||||
│ │ │
|
||||
│ │ 4. Validate & mine │
|
||||
│ ├───────────────────────>│
|
||||
│ │ │
|
||||
│ │ 5. Block confirmed │
|
||||
│ │<───────────────────────┤
|
||||
│ │ │
|
||||
│ 6. Registration │ │
|
||||
│ confirmed + block # │ │
|
||||
│<───────────────────────┤ │
|
||||
│ │ │
|
||||
│ 7. Store block # as │ │
|
||||
│ proof of identity │ │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. APP SYNC ARCHITECTURE
|
||||
|
||||
### Shared Identity Storage
|
||||
**Location**: `~/.rangerblock/` (cross-app shared folder)
|
||||
|
||||
```
|
||||
~/.rangerblock/
|
||||
├── identity/
|
||||
│ ├── master_identity.json # THE identity (shared)
|
||||
│ ├── hardware_fingerprint.json # Device binding
|
||||
│ ├── chain_registration.json # On-chain proof
|
||||
│ └── sync_state.json # Last sync timestamp
|
||||
│
|
||||
├── keys/
|
||||
│ ├── master_private_key.pem # RSA-2048 (NEVER leaves device)
|
||||
│ ├── master_public_key.pem # Shared with network
|
||||
│ └── session_keys/ # Per-session encryption keys
|
||||
│
|
||||
├── apps/
|
||||
│ ├── ranger-chat-lite/
|
||||
│ │ ├── settings.json # App-specific settings
|
||||
│ │ ├── chat_history.json # Message history
|
||||
│ │ └── contacts.json # Saved contacts
|
||||
│ │
|
||||
│ └── rangerplex/
|
||||
│ ├── settings.json
|
||||
│ ├── modules.json # Enabled modules
|
||||
│ └── workspace.json # UI state
|
||||
│
|
||||
├── sync/
|
||||
│ ├── pending_sync.json # Changes to sync
|
||||
│ ├── conflict_log.json # Sync conflicts
|
||||
│ └── last_sync.json # Sync metadata
|
||||
│
|
||||
└── security/
|
||||
├── trusted_devices.json # Other devices with same identity
|
||||
├── revocation_list.json # Compromised keys
|
||||
└── audit_log.json # Security events
|
||||
```
|
||||
|
||||
### App Detection & Sync
|
||||
```javascript
|
||||
// When ranger-chat-lite starts:
|
||||
class AppSyncManager {
|
||||
async detectRangerPlex() {
|
||||
const paths = [
|
||||
'~/.rangerplex', // Linux/macOS
|
||||
'~/Library/Application Support/RangerPlex', // macOS
|
||||
'%APPDATA%/RangerPlex' // Windows
|
||||
];
|
||||
|
||||
for (const path of paths) {
|
||||
if (await fs.exists(path)) {
|
||||
return { installed: true, path };
|
||||
}
|
||||
}
|
||||
return { installed: false };
|
||||
}
|
||||
|
||||
async syncWithRangerPlex() {
|
||||
const rangerplex = await this.detectRangerPlex();
|
||||
|
||||
if (rangerplex.installed) {
|
||||
// RangerPlex exists - sync to shared identity
|
||||
await this.mergeIdentities();
|
||||
await this.syncSettings();
|
||||
await this.notifyUser('Synced with RangerPlex!');
|
||||
} else {
|
||||
// First app - create shared identity
|
||||
await this.createSharedIdentity();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Settings Migration (Lite → Full)
|
||||
```javascript
|
||||
// When RangerPlex detects existing Chat Lite identity:
|
||||
async function migrateFromChatLite() {
|
||||
const chatLiteData = await loadChatLiteData();
|
||||
|
||||
if (chatLiteData) {
|
||||
// Import user's existing identity
|
||||
await importIdentity(chatLiteData.identity);
|
||||
|
||||
// Import chat history
|
||||
await importChatHistory(chatLiteData.messages);
|
||||
|
||||
// Import contacts
|
||||
await importContacts(chatLiteData.contacts);
|
||||
|
||||
// Import preferences
|
||||
await importPreferences(chatLiteData.settings);
|
||||
|
||||
// Notify user
|
||||
showWelcome(`
|
||||
Welcome to RangerPlex!
|
||||
|
||||
We found your Chat Lite identity:
|
||||
• Username: ${chatLiteData.identity.nickname}
|
||||
• Messages: ${chatLiteData.messages.length}
|
||||
• Contacts: ${chatLiteData.contacts.length}
|
||||
|
||||
Everything has been imported automatically!
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. SECURITY CONSIDERATIONS (FIRST APP CHECKLIST)
|
||||
|
||||
### Things You Didn't Know to Ask For:
|
||||
|
||||
#### A. Input Validation (CRITICAL)
|
||||
```javascript
|
||||
// NEVER trust user input!
|
||||
function sanitizeNickname(input) {
|
||||
// Remove dangerous characters
|
||||
const clean = input
|
||||
.replace(/[<>\"\'\\\/]/g, '') // No HTML/script injection
|
||||
.replace(/[\x00-\x1F]/g, '') // No control characters
|
||||
.trim()
|
||||
.substring(0, 32); // Max length
|
||||
|
||||
// Check against banned patterns
|
||||
const banned = ['admin', 'system', 'ranger', 'commander'];
|
||||
if (banned.some(b => clean.toLowerCase().includes(b))) {
|
||||
throw new Error('Reserved nickname');
|
||||
}
|
||||
|
||||
return clean;
|
||||
}
|
||||
|
||||
// Validate ALL WebSocket messages
|
||||
function validateMessage(data) {
|
||||
try {
|
||||
const msg = JSON.parse(data);
|
||||
|
||||
// Check required fields
|
||||
if (!msg.type || typeof msg.type !== 'string') {
|
||||
throw new Error('Invalid message type');
|
||||
}
|
||||
|
||||
// Check payload size (prevent DoS)
|
||||
if (JSON.stringify(msg).length > 65536) {
|
||||
throw new Error('Message too large');
|
||||
}
|
||||
|
||||
// Check for injection attempts
|
||||
if (containsInjection(msg)) {
|
||||
throw new Error('Injection detected');
|
||||
}
|
||||
|
||||
return msg;
|
||||
} catch (e) {
|
||||
logSecurityEvent('INVALID_MESSAGE', { error: e.message, data });
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### B. Rate Limiting (PREVENTS ABUSE)
|
||||
```javascript
|
||||
class RateLimiter {
|
||||
constructor() {
|
||||
this.limits = {
|
||||
messages: { max: 10, window: 10000 }, // 10 msgs per 10s
|
||||
connections: { max: 3, window: 60000 }, // 3 connects per min
|
||||
registrations: { max: 1, window: 86400000 } // 1 reg per day per IP
|
||||
};
|
||||
this.counters = new Map();
|
||||
}
|
||||
|
||||
check(type, identifier) {
|
||||
const key = `${type}:${identifier}`;
|
||||
const now = Date.now();
|
||||
const limit = this.limits[type];
|
||||
|
||||
if (!this.counters.has(key)) {
|
||||
this.counters.set(key, []);
|
||||
}
|
||||
|
||||
const timestamps = this.counters.get(key)
|
||||
.filter(t => now - t < limit.window);
|
||||
|
||||
if (timestamps.length >= limit.max) {
|
||||
return { allowed: false, retryAfter: limit.window - (now - timestamps[0]) };
|
||||
}
|
||||
|
||||
timestamps.push(now);
|
||||
this.counters.set(key, timestamps);
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### C. Secure Key Storage (DON'T STORE PLAIN!)
|
||||
```javascript
|
||||
const crypto = require('crypto');
|
||||
const os = require('os');
|
||||
|
||||
class SecureKeyStorage {
|
||||
// Derive encryption key from hardware + user password
|
||||
deriveStorageKey(password) {
|
||||
const hardwareId = this.getHardwareId();
|
||||
const salt = crypto.createHash('sha256')
|
||||
.update(hardwareId + os.userInfo().username)
|
||||
.digest();
|
||||
|
||||
return crypto.pbkdf2Sync(password, salt, 100000, 32, 'sha256');
|
||||
}
|
||||
|
||||
// Encrypt private key before storage
|
||||
encryptPrivateKey(privateKeyPem, password) {
|
||||
const key = this.deriveStorageKey(password);
|
||||
const iv = crypto.randomBytes(16);
|
||||
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
|
||||
let encrypted = cipher.update(privateKeyPem, 'utf8', 'base64');
|
||||
encrypted += cipher.final('base64');
|
||||
|
||||
return {
|
||||
encrypted,
|
||||
iv: iv.toString('base64'),
|
||||
authTag: cipher.getAuthTag().toString('base64')
|
||||
};
|
||||
}
|
||||
|
||||
// Decrypt on use
|
||||
decryptPrivateKey(encryptedData, password) {
|
||||
const key = this.deriveStorageKey(password);
|
||||
const decipher = crypto.createDecipheriv(
|
||||
'aes-256-gcm',
|
||||
key,
|
||||
Buffer.from(encryptedData.iv, 'base64')
|
||||
);
|
||||
decipher.setAuthTag(Buffer.from(encryptedData.authTag, 'base64'));
|
||||
|
||||
let decrypted = decipher.update(encryptedData.encrypted, 'base64', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### D. Session Security
|
||||
```javascript
|
||||
class SessionManager {
|
||||
generateSessionToken(userId, hardwareId) {
|
||||
const payload = {
|
||||
userId,
|
||||
hardwareId,
|
||||
issuedAt: Date.now(),
|
||||
expiresAt: Date.now() + (24 * 60 * 60 * 1000), // 24 hours
|
||||
nonce: crypto.randomBytes(16).toString('hex')
|
||||
};
|
||||
|
||||
// Sign the token
|
||||
const signature = this.sign(JSON.stringify(payload));
|
||||
|
||||
return Buffer.from(JSON.stringify({ payload, signature }))
|
||||
.toString('base64');
|
||||
}
|
||||
|
||||
validateSessionToken(token, expectedHardwareId) {
|
||||
try {
|
||||
const { payload, signature } = JSON.parse(
|
||||
Buffer.from(token, 'base64').toString()
|
||||
);
|
||||
|
||||
// Check expiry
|
||||
if (Date.now() > payload.expiresAt) {
|
||||
return { valid: false, reason: 'expired' };
|
||||
}
|
||||
|
||||
// Check hardware binding
|
||||
if (payload.hardwareId !== expectedHardwareId) {
|
||||
return { valid: false, reason: 'hardware_mismatch' };
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
if (!this.verify(JSON.stringify(payload), signature)) {
|
||||
return { valid: false, reason: 'invalid_signature' };
|
||||
}
|
||||
|
||||
return { valid: true, payload };
|
||||
} catch (e) {
|
||||
return { valid: false, reason: 'malformed' };
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### E. Audit Logging (LEGAL PROTECTION)
|
||||
```javascript
|
||||
class AuditLogger {
|
||||
constructor(dbPath) {
|
||||
this.db = new Database(dbPath);
|
||||
this.initSchema();
|
||||
}
|
||||
|
||||
log(event) {
|
||||
const entry = {
|
||||
timestamp: new Date().toISOString(),
|
||||
eventType: event.type,
|
||||
userId: event.userId || null,
|
||||
hardwareId: event.hardwareId || null,
|
||||
ipAddress: event.ip || null,
|
||||
action: event.action,
|
||||
details: JSON.stringify(event.details || {}),
|
||||
severity: event.severity || 'INFO'
|
||||
};
|
||||
|
||||
this.db.insert('audit_log', entry);
|
||||
|
||||
// Alert Commander for high severity
|
||||
if (event.severity === 'CRITICAL') {
|
||||
this.alertCommander(entry);
|
||||
}
|
||||
}
|
||||
|
||||
// Required events to log:
|
||||
// - User registration
|
||||
// - Login attempts (success/fail)
|
||||
// - Message sends (metadata only, not content!)
|
||||
// - File transfers (metadata)
|
||||
// - Admin actions
|
||||
// - Kill switch triggers
|
||||
// - Suspicious activity
|
||||
}
|
||||
```
|
||||
|
||||
#### F. Error Handling (DON'T LEAK INFO)
|
||||
```javascript
|
||||
// BAD - leaks internal details
|
||||
app.use((err, req, res, next) => {
|
||||
res.status(500).json({
|
||||
error: err.message,
|
||||
stack: err.stack, // NEVER expose stack trace!
|
||||
query: req.query // NEVER echo back user input!
|
||||
});
|
||||
});
|
||||
|
||||
// GOOD - generic errors
|
||||
app.use((err, req, res, next) => {
|
||||
const errorId = crypto.randomBytes(8).toString('hex');
|
||||
|
||||
// Log full error internally
|
||||
logger.error({
|
||||
errorId,
|
||||
error: err.message,
|
||||
stack: err.stack,
|
||||
request: sanitize(req)
|
||||
});
|
||||
|
||||
// Return generic message to user
|
||||
res.status(500).json({
|
||||
error: 'An error occurred',
|
||||
errorId: errorId, // User can report this ID
|
||||
support: 'Contact support with this error ID'
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### G. Content Security (PREVENT ABUSE)
|
||||
```javascript
|
||||
class ContentFilter {
|
||||
constructor() {
|
||||
// Load filters
|
||||
this.illegalPatterns = [
|
||||
/(?:^|\s)csam(?:\s|$)/i, // Child abuse material
|
||||
/(?:^|\s)bomb(?:\s+making)?(?:\s|$)/i,
|
||||
// etc - comprehensive list
|
||||
];
|
||||
|
||||
this.spamPatterns = [
|
||||
/(.)\1{10,}/, // Repeated characters
|
||||
/(https?:\/\/[^\s]+\s*){5,}/, // Too many URLs
|
||||
];
|
||||
}
|
||||
|
||||
check(content) {
|
||||
// Check for illegal content
|
||||
for (const pattern of this.illegalPatterns) {
|
||||
if (pattern.test(content)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'ILLEGAL_CONTENT',
|
||||
action: 'BLOCK_AND_REPORT'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Check for spam
|
||||
for (const pattern of this.spamPatterns) {
|
||||
if (pattern.test(content)) {
|
||||
return {
|
||||
allowed: false,
|
||||
reason: 'SPAM_DETECTED',
|
||||
action: 'BLOCK'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { allowed: true };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### H. TLS/WSS Configuration (ENCRYPT IN TRANSIT)
|
||||
```javascript
|
||||
const https = require('https');
|
||||
const fs = require('fs');
|
||||
|
||||
// For production - use proper certificates!
|
||||
const server = https.createServer({
|
||||
key: fs.readFileSync('server-key.pem'),
|
||||
cert: fs.readFileSync('server-cert.pem'),
|
||||
|
||||
// Security settings
|
||||
minVersion: 'TLSv1.2', // Minimum TLS version
|
||||
ciphers: [
|
||||
'ECDHE-ECDSA-AES128-GCM-SHA256',
|
||||
'ECDHE-RSA-AES128-GCM-SHA256',
|
||||
'ECDHE-ECDSA-AES256-GCM-SHA384',
|
||||
'ECDHE-RSA-AES256-GCM-SHA384'
|
||||
].join(':'),
|
||||
honorCipherOrder: true
|
||||
});
|
||||
|
||||
// WebSocket over TLS
|
||||
const wss = new WebSocket.Server({ server });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. IMPLEMENTATION PHASES (REVISED)
|
||||
|
||||
### Phase 1: Shared Identity Library (THIS WEEK)
|
||||
**Files to Create**:
|
||||
```
|
||||
/rangerblock/lib/
|
||||
├── identity-service.cjs # Core identity (ported from TS)
|
||||
├── crypto-utils.cjs # RSA, signing, encryption
|
||||
├── storage-utils.cjs # Cross-platform storage
|
||||
├── hardware-id.cjs # Hardware fingerprinting
|
||||
├── sync-manager.cjs # App sync logic
|
||||
└── chain-registration.cjs # On-chain identity
|
||||
```
|
||||
|
||||
### Phase 2: Update ranger-chat-lite
|
||||
- Move identity to `~/.rangerblock/`
|
||||
- Add RangerPlex detection
|
||||
- Add on-chain registration
|
||||
- Enable RSA signing
|
||||
|
||||
### Phase 3: Auth Server + Just-Chat Updates
|
||||
- Build server-only/auth-server.cjs
|
||||
- Update blockchain-chat.cjs
|
||||
- Update voice-chat.cjs
|
||||
|
||||
### Phase 4: Kill Switch Integration
|
||||
- Add Rain Protocol listeners
|
||||
- Add Commander verification
|
||||
- Test shutdown procedures
|
||||
|
||||
### Phase 5: RangerPlex Integration
|
||||
- Detect Chat Lite identity
|
||||
- Migrate settings
|
||||
- Unified dashboard
|
||||
|
||||
---
|
||||
|
||||
## 6. QUICK REFERENCE
|
||||
|
||||
### App Paths
|
||||
| App | Identity Location |
|
||||
|-----|-------------------|
|
||||
| ranger-chat-lite | `~/.rangerblock/` (shared) |
|
||||
| RangerPlex | `~/.rangerblock/` (shared) |
|
||||
| blockchain-chat.cjs | `~/.rangerblock/` (shared) |
|
||||
| voice-chat.cjs | `~/.rangerblock/` (shared) |
|
||||
|
||||
### API Methods
|
||||
| Method | Purpose |
|
||||
|--------|---------|
|
||||
| `getOrCreateIdentity()` | Get or create shared identity |
|
||||
| `registerOnChain()` | Register identity on blockchain |
|
||||
| `syncWithApps()` | Sync settings across apps |
|
||||
| `validateIdentity()` | Verify identity is valid |
|
||||
| `migrateFromApp()` | Import from other app |
|
||||
|
||||
---
|
||||
|
||||
## 7. SUMMARY OF CHANGES (v2.0)
|
||||
|
||||
1. **On-Chain Registration**: Every identity gets registered on the blockchain
|
||||
2. **App Sync**: ranger-chat-lite ↔ RangerPlex automatic sync
|
||||
3. **Settings Migration**: Seamless upgrade from Lite to Full
|
||||
4. **Shared Storage**: `~/.rangerblock/` used by ALL apps
|
||||
5. **Security Additions**:
|
||||
- Input validation
|
||||
- Rate limiting
|
||||
- Encrypted key storage
|
||||
- Session security
|
||||
- Audit logging
|
||||
- Error handling (no info leaks)
|
||||
- Content filtering
|
||||
- TLS/WSS configuration
|
||||
|
||||
---
|
||||
|
||||
**Document Classification**: COMMANDER EYES ONLY
|
||||
**Version**: 2.0
|
||||
**Created**: December 3, 2024
|
||||
**Author**: Ranger (AIR9cd99c4515aeb3f6)
|
||||
**For**: David Keane (IR240474)
|
||||
|
||||
🎖️ Rangers lead the way!
|
||||
Reference in New Issue
Block a user