Initial release: ghost_protocol privacy toolkit

This commit is contained in:
ghost
2026-03-04 09:13:16 -05:00
commit 973cede31f
99 changed files with 12158 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
logs/
__pycache__/
*.pyc
.env
vars.yaml
*.log
*.pem
*.key
id_rsa*
venv/
.venv/
.DS_Store
*.swp
*.swo
*~
*.tar.gz
.claude/
private/
*.retry
inventory*
.vault_pass*
hosts
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 ghost_protocol contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+90
View File
@@ -0,0 +1,90 @@
# ghost_protocol
A privacy toolkit for individuals who take digital autonomy seriously. Three tools, one goal: control your own infrastructure.
## Components
### opsec/
OPSEC hardening suite for Linux systems. MAC randomization, DNS leak prevention, kill switches, Tor integration, hostname randomization, and a desktop status widget. Configurable deployment levels from standard privacy to full paranoid mode.
```bash
cd opsec && sudo ./install.sh
```
### covert_sd/
Covert SD card tool for secure storage operations. Encryption, hidden volumes, and plausible deniability for portable media.
```bash
cd covert_sd && python3 covert_sd_card_tool.py
```
### phantom/
Privacy server deployer. Stand up Matrix, WireGuard, Pi-hole, and more on cloud providers or your own hardware — hardened out of the box.
```bash
cd phantom && python3 phantom.py
```
## Who This Is For
- Journalists protecting sources
- Researchers handling sensitive data
- Privacy advocates practicing what they preach
- Anyone who believes infrastructure shouldn't require trust in third parties
## Quick Start
```bash
git clone <this-repo> ghost_protocol
cd ghost_protocol
# OPSEC hardening
cd opsec && sudo ./install.sh
# Deploy a privacy server
cd ../phantom && python3 phantom.py
# Covert SD operations
cd ../covert_sd && python3 covert_sd_card_tool.py
```
## Requirements
- **opsec**: Linux (Debian/Ubuntu), root access
- **covert_sd**: Python 3.8+, cryptsetup
- **phantom**: Python 3.8+, Ansible 2.12+, SSH, pyyaml
- For AWS deployments: `ansible-galaxy collection install amazon.aws` and `pip install boto3`
## Important: Configure Before Use
**This toolkit ships with intentionally blank or generic defaults.** You must configure it for your environment before relying on it.
### opsec/
After installing, run `sudo opsec-config.sh` to set:
- **Tor exit node exclusions** (`TOR_BLACKLIST`) — empty by default. Set country codes based on your threat model (e.g., `us,gb,ca,au,nz` for Five Eyes exclusion). See `configs/opsec-country-codes.conf` for presets.
- **DNS mode** (`DNS_MODE`) — defaults to `tor`. Choose based on your needs: `tor`, `quad9`, `cloudflare`, `doh`, `dot`, or `custom`.
- **Deployment level** — run `sudo opsec-config.sh --level apply <level>` to select a preset. Review `configs/levels/` to understand what each level enables.
- **Widget theme** (`WIDGET_THEME`) — defaults to `default`. Run `sudo opsec-config.sh --theme list` for options.
- **Hostname pattern** (`HOSTNAME_PATTERN`) — defaults to `desktop`. Set to `random` if you want randomization on boot.
- **MAC randomization**, **kill switch behavior**, and **traffic blending** all need to be reviewed and enabled per your use case.
The generic defaults are safe but minimal. They will **not** protect you against a sophisticated adversary without customization. Review `/etc/opsec/opsec.conf` after install and adjust every section for your threat model.
### phantom/
Each deployment prompts for service-specific configuration (domains, credentials, DNS providers). There are no hardcoded server addresses or API keys. You supply everything at deploy time.
### covert_sd/
The tool prompts for all encryption parameters interactively. No defaults to change, but read the README for security model details.
## Responsible Use
This toolkit is intended for **legitimate privacy protection**: journalists safeguarding sources, researchers handling sensitive data, organizations protecting communications, and individuals exercising their right to privacy.
- Secure deletion features are irreversible. Understand what you are deleting.
- Network anonymization is not foolproof. No tool provides absolute anonymity.
- Comply with applicable laws in your jurisdiction.
- This software is provided as-is with no warranty. You are responsible for how you use it.
## License
MIT — see [LICENSE](LICENSE)
+334
View File
@@ -0,0 +1,334 @@
# Covert SD Card Tool
## Introduction
The **Covert SD Card Tool** is a Python script designed to automate the process of setting up a bootable USB/SD card with either Kali Linux or Tails OS. It includes options to create encrypted persistence partitions, secure document storage, and user-friendly access scripts. This tool simplifies the complex steps involved in preparing a secure, portable operating system on a USB drive or SD card.
## Features
- **Install Kali Linux or Tails OS** on a USB/SD card
- **Create an encrypted persistence partition** for Kali Linux (LUKS encryption)
- **Create a maximum-security encrypted documents partition** with triple-cascade encryption
- **Secure drive wiping** using multi-pass shred for data sanitization
- **User-friendly access scripts** for mounting and locking secure storage
- **OPSEC-focused design** - generated scripts use generic terminology
- **Automated dependency checking and installation**
## Prerequisites
- **Operating System:** Linux (Debian-based distributions recommended)
- **Python Version:** Python 3.x
- **Root Access:** Required for disk operations
- **Dependencies:**
- `parted` - Partition management
- `cryptsetup` - LUKS encryption
- `lsblk` - Block device listing
- `dd` - Disk writing
- `sgdisk` - GPT partition manipulation
- `wipefs` - Filesystem signature removal
- `shred` - Secure data wiping
- `bc` - Calculator for partition math
- `fdisk` - Partition table manipulation
- `veracrypt` - Document partition encryption
- `lsof`, `fuser` - Process detection
- `udevadm` - Device management
**Note:** The script will automatically detect missing dependencies and offer to install them.
## Installation
1. **Clone the Repository or Download the Script:**
```bash
git clone https://github.com/YOUR_USERNAME/ghost_protocol.git
cd ghost_protocol/covert_sd
```
2. **Make the Script Executable:**
```bash
chmod +x covert_sd_card_tool.py
```
## Usage
Run the script with appropriate options:
```bash
sudo ./covert_sd_card_tool.py [options]
```
### Command-Line Options
- `-a`, `--all` : Set up both the OS bootable USB and the documents partition (defaults to Kali)
- `-k`, `--kali` : Create a Kali bootable USB and persistence partition
- `-t`, `--tails` : Create a Tails bootable USB (no persistence, mutually exclusive with `-a`)
- `-c`, `--custom` : Create a custom ISO bootable USB (uses Kali-style partitioning with persistence)
- `-d`, `--docs` : Create an encrypted documents partition
- `-i`, `--iso` : Path to the ISO file (Kali, Tails, or custom)
- `--fast` : Enable fast setup mode (weaker encryption, faster setup - not recommended)
- `--paranoid` : Enable paranoid mode (maximum security: 3-pass wipe, Argon2id, PIM 5000)
- `--debug` : Enable debug mode with verbose logging
**Note:** `--fast` and `--paranoid` are mutually exclusive. Documents partition always uses strong encryption even in fast mode.
### Examples
- **Install Kali with Encrypted Persistence and Encrypted Documents Partition:**
```bash
sudo ./covert_sd_card_tool.py -a -i /path/to/kali.iso
```
- **Install Tails with Encrypted Documents Partition:**
```bash
sudo ./covert_sd_card_tool.py -t -d -i /path/to/tails.iso
```
- **Install Tails Only (No Documents Partition):**
```bash
sudo ./covert_sd_card_tool.py -t -i /path/to/tails.iso
```
- **Create Encrypted Documents Partition Only (No OS):**
```bash
sudo ./covert_sd_card_tool.py -d
```
- **Install Custom ISO (e.g., Parrot OS, BlackArch) with Persistence and Documents:**
```bash
sudo ./covert_sd_card_tool.py -c -d -i /path/to/custom.iso
```
- **Install Custom ISO with Persistence Only (No Documents):**
```bash
sudo ./covert_sd_card_tool.py -c -i /path/to/custom.iso
```
- **Paranoid Mode - Maximum Security (Tails + Docs):**
```bash
sudo ./covert_sd_card_tool.py -t -d -i /path/to/tails.iso --paranoid
```
## Security Features
### Documents Partition Encryption
The documents partition uses **strong security** in all modes:
**Standard Mode (default):**
- **Triple Cascade Encryption:** AES-Twofish-Serpent (3 layers)
- **Hash Algorithm:** SHA-512
- **Key Derivation:** PIM 2000 (strong key stretching)
- **Unlock Time:** ~2-3 seconds
- **Filesystem:** ext4
- **Full Format:** Always overwrites old data
**Paranoid Mode (`--paranoid`):**
- **Triple Cascade Encryption:** AES-Twofish-Serpent (3 layers)
- **Hash Algorithm:** SHA-512
- **Key Derivation:** PIM 5000 (maximum key stretching)
- **Unlock Time:** ~5-7 seconds
- **Security:** Designed to resist brute-force attacks when used with a strong passphrase
**Fast Mode (`--fast`):**
- Documents still use standard mode (PIM 2000) - no compromise on docs security
### Persistence Partition Encryption (Kali/Custom)
**Standard Mode (default):**
- **Algorithm:** AES-XTS-PLAIN64
- **Key Size:** 512-bit
- **Hash:** SHA-512
- **KDF:** LUKS2 PBKDF2
- **Iteration Time:** 5 seconds
**Paranoid Mode (`--paranoid`):**
- **Algorithm:** AES-XTS-PLAIN64
- **Key Size:** 512-bit
- **Hash:** SHA-512
- **KDF:** LUKS2 Argon2id (memory-hard, GPU-resistant)
- **Memory:** 1GB
- **Parallel Threads:** 4
- **Iteration Time:** 10 seconds
**Fast Mode (`--fast`):**
- **Algorithm:** AES-CBC-ESSIV:SHA256
- **Key Size:** 256-bit
- **Hash:** SHA-256
- **KDF:** LUKS1 PBKDF2
- **Iteration Time:** 1 second
### Secure Drive Wiping
**Standard Mode (default):**
- **1 pass** with zeros (`dd if=/dev/zero`)
- Fast and sufficient for most use cases
- Prevents casual data recovery
- Confirmation required (must type 'WIPE')
**Paranoid Mode (`--paranoid`):**
- **3 passes** with random data (`shred`)
- **Final pass** with zeros
- Makes data recovery virtually impossible
- Defense against forensic recovery techniques
- **Much slower** (can take hours on large drives)
- Confirmation required (must type 'WIPE')
### OPSEC (Operational Security)
The generated helper scripts use **generic terminology** to avoid disclosing encryption methods:
- Scripts renamed to `mount_storage.sh` and `lock_storage.sh` (instead of mentioning encryption types)
- README uses terms like "secure storage" instead of specific encryption names
- No algorithm disclosure in user-facing documentation on the device
- Suitable for travel scenarios where device inspection may occur
## Generated Helper Scripts
The tool creates a small unencrypted partition (TOOLS) containing:
### `mount_storage.sh`
- Interactive script to mount the encrypted documents partition
- Shows available devices and validates input
- Mounts to `/mnt/secure_storage`
- Clear error messages and success confirmations
### `lock_storage.sh`
- Safely dismounts and locks the encrypted storage
- Checks for open files before locking
- Shows warnings if applications are still using the storage
- Syncs pending writes before dismount
- Prevents data loss from improper ejection
### `README.txt`
- Simple instructions for non-technical users
- Generic terminology (no encryption disclosure)
- Step-by-step mount/lock procedures
## Security Mode Comparison
| Feature | Fast Mode | Standard Mode (Default) | Paranoid Mode |
|---------|-----------|------------------------|---------------|
| **Drive Wipe** | Partition table clear only | 1-pass zeros | 3-pass shred + zeros |
| **Wipe Time (64GB)** | Instant | ~5-10 min | ~2-3 hours |
| **Persistence KDF** | LUKS1 PBKDF2 | LUKS2 PBKDF2 | LUKS2 Argon2id |
| **Persistence Unlock** | ~1 sec | ~5 sec | ~10 sec |
| **Docs Encryption** | AES-Twofish-Serpent | AES-Twofish-Serpent | AES-Twofish-Serpent |
| **Docs PIM** | 2000 | 2000 | 5000 |
| **Docs Unlock** | ~2-3 sec | ~2-3 sec | ~5-7 sec |
| **Best For** | Testing/dev | Travel, daily use | Maximum security, high-risk scenarios |
**Recommendation:** Use **standard mode** for most cases. Use **paranoid mode** if:
- You're protecting extremely sensitive data
- You face nation-state level threats
- You have time for longer setup and unlock times
- You want defense against forensic analysis
## Important Security Notes
⚠️ **Password Strength:** Use strong passphrases (20+ characters, mixed case, numbers, symbols)
⚠️ **No Password Recovery:** If you forget your password, your data is **permanently inaccessible**
⚠️ **PIM Value:** The tool enforces PIM 2000 for documents partition - this adds 2-3 seconds to unlock time but massively increases security
⚠️ **Always Lock Before Removal:** Use `lock_storage.sh` before removing the device to prevent data corruption
⚠️ **Fast Mode:** While available for persistence partition, documents partition **always uses maximum security**
## Partition Layout Examples
### Kali + Documents (`-a`)
1. **Partition 1:** Kali Live OS (bootable)
2. **Partition 2:** LUKS encrypted persistence (configurable size, e.g., 4GB)
3. **Partition 3:** VeraCrypt encrypted documents (remaining space minus 1GB)
4. **Partition 4:** Unencrypted tools partition (1GB, FAT32, contains scripts)
### Tails Only (`-t`)
- **Entire Drive:** Tails Live OS (bootable, no additional partitions)
### Tails + Documents (`-t -d`)
1. **Partition 1:** Tails Live OS (bootable, 12MB EFI System)
2. **Partition 2:** Tails system partition (~2-3GB depending on Tails version)
3. **Partition 3:** VeraCrypt encrypted documents (remaining space minus 1GB)
4. **Partition 4:** Unencrypted tools partition (1GB, FAT32, contains scripts)
### Documents Only (`-d`)
1. **Partition 1:** VeraCrypt encrypted documents (remaining space minus 1GB)
2. **Partition 2:** Unencrypted tools partition (1GB, FAT32, contains scripts)
### Custom ISO + Documents (`-c -d`)
1. **Partition 1:** Custom Live OS (bootable)
2. **Partition 2:** LUKS encrypted persistence (configurable size, e.g., 4GB)
3. **Partition 3:** VeraCrypt encrypted documents (remaining space minus 1GB)
4. **Partition 4:** Unencrypted tools partition (1GB, FAT32, contains scripts)
**Note:** Custom ISO mode uses Kali-style partitioning. Works well with Debian-based live ISOs like Parrot OS, BlackArch, BackBox, etc.
## Using Custom ISOs
The `-c` (custom) flag allows you to use **any bootable ISO** and set it up with encrypted persistence and documents partitions. This is useful for:
### Compatible ISOs
- **Parrot Security OS** - Privacy-focused security distro
- **BlackArch Linux** - Penetration testing distro
- **BackBox** - Ubuntu-based penetration testing
- **Pentoo** - Gentoo-based security distro
- **Any Debian/Ubuntu-based live ISO**
### How It Works
Custom ISOs are treated like Kali Linux:
1. ISO is flashed to the drive
2. LUKS encrypted persistence partition is created (if you want settings to persist)
3. VeraCrypt encrypted documents partition is added (if `-d` flag is used)
4. Tools partition with mount/lock scripts
### Example: Parrot OS with Docs
```bash
sudo ./covert_sd_card_tool.py -c -d -i ~/Downloads/parrot-security.iso
```
### Compatibility Notes
- **Best for:** Debian/Ubuntu-based live ISOs
- **May not work with:** Arch-based ISOs (different partition structure), Windows ISOs
- **Persistence:** Depends on the ISO supporting LUKS persistence (Debian-based usually do)
- If persistence doesn't work with your ISO, you can still use the documents partition
## Troubleshooting
### Device Busy Errors
- The tool automatically unmounts partitions and kills processes using the drive
- If problems persist, manually unmount: `sudo umount /dev/sdX*`
- Check for processes: `sudo lsof /dev/sdX`
### VeraCrypt Not Found
- On Debian/Ubuntu: `sudo apt install veracrypt`
- Or the script will offer to install it automatically
### Permission Denied
- Always run with `sudo`
- Ensure your user has sudo privileges
### Drive Not Detected
- Check if drive is connected: `lsblk`
- Verify drive path (e.g., `/dev/sdb` not `/dev/sdb1`)
- Try unplugging and reconnecting the device
## License
This tool is provided as-is for educational and legitimate security purposes only. Use responsibly and in compliance with applicable laws.
## Contributing
Contributions, bug reports, and feature requests are welcome! Please open an issue or submit a pull request.
## Disclaimer
This tool performs destructive operations on storage devices. **Always verify you've selected the correct drive** before proceeding. The authors are not responsible for data loss.
+1409
View File
File diff suppressed because it is too large Load Diff
+239
View File
@@ -0,0 +1,239 @@
======================================================================
SECURE STORAGE - MOBILE DEVICE INSTRUCTIONS
======================================================================
IMPORTANT: Mobile access has limitations compared to desktop.
Read carefully to understand what's possible on your device.
======================================================================
ANDROID DEVICES
======================================================================
REQUIREMENTS:
• Android 5.0 or newer
• EDS (Encrypted Data Store) app - REQUIRED
• USB OTG adapter (to connect USB device to phone)
• File manager app (most phones have one built-in)
REQUIRED APP:
• EDS Lite (Free & Open Source)
• Google Play: https://play.google.com/store/apps/details?id=com.sovworks.eds.android
• Direct link if Google Play doesn't work:
https://play.google.com/store/apps/details?id=com.sovworks.projecteds
----------------------------------------------------------------------
SETUP: Using EDS Lite (RECOMMENDED)
----------------------------------------------------------------------
Step 1: Install EDS Lite
• Open Google Play Store
• Search for "EDS Lite" or "EDS (Encrypted Data Store)"
• Install the app (it's FREE)
• Or use the direct link above
Step 2: Connect your USB device
• Connect USB device using OTG adapter
• Android should show "USB device connected" notification
Step 3: Open EDS Lite app
• Tap "Open container"
• Navigate to your USB device
• Look for the partition WITHOUT a filesystem/label
• Select the encrypted partition
Step 4: Enter password
• Select encryption: VeraCrypt
• Enter your password
• For PIM: enter 2000 or 5000 (whatever you set)
• Tap "OK"
Step 5: Access your files
• Files will appear in EDS Lite
• You can view, copy, or share files
• Some apps can open files directly from EDS
IMPORTANT - Safely Close:
• Tap "Close container" when done
• Wait for confirmation before unplugging
• Only then remove USB device
----------------------------------------------------------------------
ALTERNATIVE: VeraCrypt for Android (Advanced Users)
----------------------------------------------------------------------
️ NOTE: EDS Lite (above) is easier to use and recommended.
Only use VeraCrypt if you're familiar with it.
Step 1: Install VeraCrypt for Android
• Download from: https://github.com/veracrypt/veracrypt
• Or install from F-Droid store
• Enable "Unknown sources" if needed
Step 2-5: Follow similar steps as EDS Lite above
• The interface is similar to desktop VeraCrypt
• Process is the same: select partition, enter password, mount
======================================================================
iOS DEVICES (iPhone/iPad)
======================================================================
✅ SOLUTION: Crypto Disks App (Paid)
RECOMMENDED APP:
• Crypto Disks - Store Private Files Securely
• App Store: https://apps.apple.com/us/app/crypto-disks-store-private/id889549308
• Price: ~$4.99 (one-time purchase)
• Supports VeraCrypt containers!
----------------------------------------------------------------------
SETUP: Using Crypto Disks on iOS
----------------------------------------------------------------------
Step 1: Purchase and Install Crypto Disks
• Open App Store on your iPhone/iPad
• Search for "Crypto Disks"
• Or use the direct link above
• Purchase and install (~$4.99)
Step 2: Transfer Encrypted Container
⚠️ LIMITATION: iOS doesn't support direct USB access like Android
You have two options:
OPTION A: Copy via Computer (Easier)
1. Connect USB device to your Mac/PC
2. Mount the encrypted partition with VeraCrypt
3. Create a smaller VeraCrypt container file inside
4. Transfer that container to your iPhone via:
- AirDrop (Mac to iPhone)
- iCloud Drive
- iTunes File Sharing
5. Open in Crypto Disks app
OPTION B: Use Wi-Fi File Transfer
1. Use Crypto Disks' built-in file transfer
2. Transfer container file from computer
3. Open in app
Step 3: Mount in Crypto Disks App
• Open Crypto Disks app
• Select "Add Disk Image"
• Choose your transferred container
• Enter password and PIM
• Access your files
⚠️ IMPORTANT iOS LIMITATIONS:
• Cannot directly mount USB partition (Apple restriction)
• Must use container FILES, not raw partitions
• Requires copying/transferring container to iPhone
• Best for occasional access, not primary use
----------------------------------------------------------------------
iOS WORKAROUND: Emergency Access Only
----------------------------------------------------------------------
If you don't want to pay for Crypto Disks, here are free alternatives:
Option A: Use a Computer as Bridge
1. Mount the encrypted storage on your Mac/PC
2. Copy files you need to iCloud or Files app
3. Access those files from your iPhone
4. Delete copies when done (IMPORTANT for security!)
Option B: Use Remote Access
1. Mount encrypted storage on your home computer
2. Set up secure remote access (VPN + file sharing)
3. Access files remotely from iPhone
4. More complex but maintains security
Option C: Take Photos of Documents
1. If you just need to VIEW travel docs in emergency
2. Take photos/screenshots on computer
3. Store in encrypted note app (Apple Notes with lock)
4. Delete after trip
NOTE: These workarounds compromise security slightly.
Crypto Disks app is the most secure iOS solution.
======================================================================
IMPORTANT SECURITY NOTES FOR MOBILE
======================================================================
⚠ Screen Lock
• Always have a PIN/password on your phone
• If someone steals your phone while storage is mounted,
they can access your files
⚠ App Permissions
• Only grant necessary permissions to encryption apps
• Be cautious with "file access" permissions
⚠ Public WiFi
• Don't use public WiFi when accessing sensitive files
• Or use a VPN if you must
⚠ Battery Concerns
• Don't let your phone die while storage is mounted
• Always properly close/dismount before battery runs out
⚠ Background Apps
• Close the encryption app properly when done
• Don't just switch to another app
• Storage might stay mounted in background
⚠ Phone Updates
• Close encrypted storage before major OS updates
• Updates might interrupt and cause corruption
======================================================================
TROUBLESHOOTING - ANDROID
======================================================================
"Cannot find USB device"
• Try a different OTG adapter
• Check if your phone supports USB OTG
• Some phones need USB settings changed (File Transfer mode)
"Wrong password" (but password is correct)
• Make sure you selected "VeraCrypt" as encryption type
• Verify PIM value (2000 or 5000)
• Some apps don't support all VeraCrypt settings
"App crashes when mounting"
• Try a different encryption app (EDS vs VeraCrypt)
• Check if partition is already mounted
• Restart phone and try again
"Can't edit files, only view"
• This is normal for some apps
• Copy file to phone storage
• Edit it there
• Copy back when done
"Storage disconnected unexpectedly"
• Check USB connection
• Some phones cut power to USB to save battery
• Disable battery optimization for the encryption app
======================================================================
LIMITATIONS OF MOBILE ACCESS
======================================================================
Things that DON'T work well on mobile:
✗ Editing large files directly on encrypted storage
✗ Running programs/apps from encrypted storage
✗ Fast file operations (mobile is slower than desktop)
✗ iOS access (not supported at all)
Things that WORK WELL on mobile:
✓ Viewing documents (PDFs, photos, etc.)
✓ Copying individual files to phone
✓ Sharing files to other apps
✓ Emergency access when no computer available
✓ Quick lookup of information
RECOMMENDATION:
Use mobile access for emergencies and viewing only.
Use desktop (Linux/Windows) for actual file management.
======================================================================
+194
View File
@@ -0,0 +1,194 @@
======================================================================
SECURE STORAGE - QUICK START GUIDE
======================================================================
WHAT'S ON THIS DEVICE:
• Encrypted storage partition for sensitive documents
• Portable VeraCrypt (works on ANY computer - no installation!)
• Helper scripts (Linux/Mac)
• Instructions for Windows and mobile devices
======================================================================
ACCESSING YOUR FILES - QUICK GUIDE
======================================================================
LINUX / MAC:
1. Open terminal in this folder
2. Run: sudo ./mount_storage.sh
3. Enter your password
4. Files appear in: ~/SecureStorage
WINDOWS:
1. Open "WINDOWS_INSTRUCTIONS.txt" on this device
2. Follow the step-by-step guide
3. Use portable VeraCrypt from "VeraCrypt" folder (no install needed!)
ANDROID:
1. Install "EDS Lite" from Google Play Store (FREE)
https://play.google.com/store/apps/details?id=com.sovworks.projecteds
2. Connect USB with OTG adapter
3. Open "MOBILE_INSTRUCTIONS.txt" for step-by-step guide
iOS (iPhone/iPad):
1. Install "Crypto Disks" from App Store (~$4.99)
https://apps.apple.com/us/app/crypto-disks-store-private/id889549308
2. See "MOBILE_INSTRUCTIONS.txt" for setup
⚠ iOS has limitations - requires copying files, can't mount USB directly
======================================================================
IMPORTANT: HOW TO SAFELY LOCK YOUR STORAGE
======================================================================
LINUX / MAC:
1. Close ALL programs using the secure storage
2. Run: sudo ./lock_storage.sh
3. Wait for "SUCCESS" message
4. Safe to remove device
WINDOWS:
1. Close all files
2. In VeraCrypt, select the drive
3. Click "Dismount"
4. Use "Safely Remove Hardware"
⚠ CRITICAL: ALWAYS lock/dismount before removing!
Otherwise you risk losing your data.
======================================================================
PORTABLE VERACRYPT - NO INSTALLATION REQUIRED!
======================================================================
Inside the "VeraCrypt" folder on this device:
• VeraCrypt-Portable-Windows.exe
→ Double-click to run on ANY Windows PC
→ Works without admin rights on most systems
• VeraCrypt-Portable-Linux.AppImage
→ Run on ANY Linux system (no install!)
→ Command: ./VeraCrypt-Portable-Linux.AppImage
→ AppImage = portable, works on all Linux distributions
• veracrypt-Ubuntu-22.04-amd64.deb
→ System-wide installer for Ubuntu/Debian
→ Install with: sudo dpkg -i veracrypt-Ubuntu-22.04-amd64.deb
→ Use this if you prefer installed version over AppImage
• VeraCrypt-MacOS.dmg
→ Install on Mac (one-time)
→ Works on any Mac after that
This means you can access your encrypted files on ANY computer
even if VeraCrypt is not installed!
======================================================================
YOUR PASSWORD
======================================================================
⚠ There is NO password recovery! If you forget it, data is GONE.
Password details:
• Case-sensitive (Abc123 ≠ abc123)
• Minimum 20 characters recommended
• Mix of letters, numbers, symbols
• Write it down and keep it SAFE (not on this device!)
If you set a PIM value:
• Standard setup: PIM = 2000
• Paranoid setup: PIM = 5000
• VeraCrypt will ask for this when mounting
======================================================================
TROUBLESHOOTING
======================================================================
PROBLEM: "Cannot find TOOLS partition"
FIX: Ensure device is fully connected; try different USB port
PROBLEM: "Wrong password" (but you know it's correct)
FIX: Check if CAPS LOCK is on; verify PIM value
PROBLEM: "Volume already mounted"
FIX: Close any VeraCrypt windows and run:
• Linux/Mac: sudo veracrypt -d
• Windows: Dismount all in VeraCrypt
PROBLEM: "Permission denied" when accessing files
FIX:
• Linux/Mac: Re-run mount script (it sets permissions)
• Windows: Check if you ran VeraCrypt as administrator
PROBLEM: "Files appear but I can't edit them"
FIX:
• Linux/Mac: Run: sudo chown -R $USER ~/SecureStorage
• Windows: Right-click → Properties → Security → Edit
PROBLEM: Device not detected on Android
FIX:
• Check USB OTG adapter is working
• Enable "File Transfer" mode in USB settings
• Try different OTG adapter
======================================================================
FILE LOCATIONS AFTER MOUNTING
======================================================================
Linux/Mac: ~/SecureStorage
(In your home folder)
Windows: Z:\ (or whatever drive letter you selected)
Shows in "This PC"
Android: Accessible within EDS Lite or VeraCrypt app
======================================================================
SECURITY NOTES
======================================================================
✓ Your files use strong multi-layer encryption
✓ AES-Twofish-Serpent triple cascade (strongest available)
✓ Designed to resist brute-force attacks with a strong passphrase
⚠ The password is the ONLY key
⚠ No backdoors, no recovery, no "forgot password" option
⚠ Keep password safe but separate from this device
Best practices:
• Always dismount before removing device
• Don't leave mounted when unattended
• Use strong unique password
• Don't store password on this device
• Lock your computer when storage is mounted
======================================================================
ADVANCED: MANUAL MOUNTING (if scripts fail)
======================================================================
LINUX/MAC:
1. Find encrypted partition:
lsblk -o NAME,SIZE,LABEL,FSTYPE
(Look for partition WITHOUT filesystem label)
2. Mount with VeraCrypt:
sudo veracrypt /dev/sdX2 ~/SecureStorage
(Replace sdX2 with your partition)
3. Enter password and PIM when prompted
WINDOWS:
1. Open portable VeraCrypt
2. Select any drive letter
3. Click "Select Device"
4. Choose partition WITHOUT label (not the TOOLS one)
5. Click "Mount"
6. Enter password
======================================================================
NEED MORE HELP?
======================================================================
• Windows users: See WINDOWS_INSTRUCTIONS.txt
• Mobile users: See MOBILE_INSTRUCTIONS.txt
• VeraCrypt documentation: https://www.veracrypt.fr/en/Documentation.html
======================================================================
+93
View File
@@ -0,0 +1,93 @@
======================================================================
SECURE STORAGE - WINDOWS INSTRUCTIONS
======================================================================
REQUIREMENTS:
• This secure storage device connected
• NO SOFTWARE INSTALLATION NEEDED!
======================================================================
HOW TO ACCESS YOUR SECURE FILES (WINDOWS)
======================================================================
OPTION 1: USE PORTABLE VERACRYPT (RECOMMENDED - NO INSTALL!)
Step 1: Open the VeraCrypt folder on this USB drive
Step 2: Double-click "VeraCrypt-Portable-Windows.exe"
• It runs directly - no installation!
• Works on most Windows PCs without admin rights
• If blocked, right-click → "Run anyway"
Step 3: The VeraCrypt window opens
----------------------------------------------------------------------
OPTION 2: USE INSTALLED VERACRYPT (if you prefer)
Step 1: Install VeraCrypt (if not already installed)
• Go to https://www.veracrypt.fr/en/Downloads.html
• Download VeraCrypt for Windows
• Run the installer
Step 2: Open VeraCrypt from Start Menu
• Search for "VeraCrypt" in Start Menu
• Click "VeraCrypt" to launch
----------------------------------------------------------------------
CONTINUING (after opening VeraCrypt either way):
Step 3: Select a drive letter
• In VeraCrypt window, click any drive letter (e.g., Z:)
Step 4: Select Volume
• Click "Select Device..." button
• Find your USB device (look for the partition WITHOUT a filesystem)
• Usually it's the second-to-last partition
• Click OK
Step 5: Click "Mount" button
• Enter your password when prompted
• For PIM: leave blank or enter the PIM you set (2000 or 5000)
• Click OK
Step 6: Access your files
• Open File Explorer
• Look for new drive (e.g., Z:)
• Your files are there!
======================================================================
HOW TO SAFELY LOCK YOUR STORAGE (WINDOWS)
======================================================================
Step 1: Close ALL programs using the secure storage
• Save documents
• Close File Explorer windows
• Close any apps with open files
Step 2: In VeraCrypt window
• Select the mounted drive (e.g., Z:)
• Click "Dismount" button
Step 3: Safe to remove
• Click "Safely Remove Hardware" in system tray
• Select your USB device
• Remove when Windows says it's safe
======================================================================
IMPORTANT NOTES
======================================================================
⚠ ALWAYS dismount before removing the device!
Otherwise you risk data corruption.
⚠ If you forget your password, your data is GONE.
There is NO password recovery.
⚠ Partition to select:
• Look in VeraCrypt's "Select Device" window
• Choose the partition WITHOUT a label/filesystem
• It's usually NOT the one labeled "TOOLS"
======================================================================
+88
View File
@@ -0,0 +1,88 @@
#!/bin/bash
# Script to safely lock secure storage
echo "======================================================================"
echo " SECURE STORAGE LOCK TOOL"
echo "======================================================================"
echo ""
# Mount point location
MOUNT_POINT="$HOME/SecureStorage"
# Check if storage is mounted
if mountpoint -q "$MOUNT_POINT" 2>/dev/null || [ -d "$MOUNT_POINT" ] && sudo veracrypt --text --list | grep -q "$MOUNT_POINT"; then
echo "Step 1: Checking for open files..."
# Check if any processes are using the mount
OPEN_FILES=$(sudo lsof "$MOUNT_POINT" 2>/dev/null | tail -n +2)
if [ -n "$OPEN_FILES" ]; then
echo ""
echo "⚠ WARNING: Files or programs are still using the secure storage!"
echo ""
echo "Open files/processes:"
echo "$OPEN_FILES"
echo ""
echo "You should close these programs first to avoid data loss."
echo ""
read -p "Force close and lock anyway? (y/N): " FORCE
if [ "$FORCE" != "y" ] && [ "$FORCE" != "Y" ]; then
echo ""
echo "Lock cancelled. Please:"
echo " 1. Close all programs using the secure storage"
echo " 2. Save any open documents"
echo " 3. Run this script again"
exit 1
fi
echo ""
echo "Forcing lock (files will be closed)..."
else
echo "✓ No open files detected"
fi
echo ""
echo "Step 2: Syncing pending writes to disk..."
sync
echo "✓ All data written to disk"
echo ""
echo "Step 3: Dismounting encrypted volume..."
# Dismount the encrypted volume
if sudo veracrypt --text --dismount "$MOUNT_POINT" 2>/dev/null; then
rmdir "$MOUNT_POINT" 2>/dev/null
echo "✓ Volume dismounted"
echo ""
echo "======================================================================"
echo " SUCCESS!"
echo "======================================================================"
echo ""
echo "Secure storage is now locked and encrypted."
echo "Your data is safe to remove the device."
echo ""
echo "======================================================================"
else
echo ""
echo "======================================================================"
echo " DISMOUNT FAILED"
echo "======================================================================"
echo ""
echo "Possible reasons:"
echo " • Files still in use (close all programs)"
echo " • Permission denied"
echo " • Volume not mounted with this script"
echo ""
echo "Troubleshooting:"
echo " 1. Check what's using it: sudo lsof $MOUNT_POINT"
echo " 2. Force dismount all: sudo veracrypt -d"
echo " 3. Kill processes: sudo fuser -km $MOUNT_POINT"
echo ""
exit 1
fi
else
echo " Secure storage is not currently mounted."
echo ""
echo "Nothing to lock - your data is already secured."
echo ""
echo "If you want to access it, run:"
echo " sudo ./mount_storage.sh"
fi
+137
View File
@@ -0,0 +1,137 @@
#!/bin/bash
# Script to mount secure storage partition
echo "======================================================================"
echo " SECURE STORAGE MOUNT TOOL"
echo "======================================================================"
echo ""
# Function to find the documents partition (second-to-last partition on drive)
find_docs_partition() {
# Look for drives with TOOLS partition (our indicator)
local tools_drive=$(lsblk -ln -o NAME,LABEL | grep "TOOLS" | awk '{print $1}' | head -1)
if [ -z "$tools_drive" ]; then
echo "ERROR: Could not find TOOLS partition."
echo "Please ensure the secure storage device is connected."
return 1
fi
# Get the base drive name (remove partition number)
local base_drive=$(echo "$tools_drive" | sed 's/[0-9]*$//' | sed 's/p$//')
# Get all partitions on this drive
local partitions=$(lsblk -ln -o NAME "/dev/$base_drive" | grep "^$base_drive" | tail -n +2)
local part_count=$(echo "$partitions" | wc -l)
if [ "$part_count" -lt 2 ]; then
echo "ERROR: Not enough partitions found."
return 1
fi
# Get second-to-last partition (documents partition)
local docs_part=$(echo "$partitions" | tail -n 2 | head -n 1)
echo "/dev/$docs_part"
return 0
}
echo "Step 1: Detecting secure storage device..."
DOCS_PARTITION=$(find_docs_partition)
if [ $? -ne 0 ]; then
echo ""
echo "======================================================================"
echo "AUTO-DETECTION FAILED - MANUAL MODE"
echo "======================================================================"
echo ""
echo "Available partitions:"
lsblk -o NAME,SIZE,TYPE,LABEL,FSTYPE | grep -v "loop"
echo ""
echo "TIP: Look for a partition WITHOUT a filesystem (blank FSTYPE)"
echo " This is usually your encrypted documents partition."
echo ""
read -p "Enter the partition path (e.g., /dev/sdb1): " DOCS_PARTITION
if [ -z "$DOCS_PARTITION" ]; then
echo "ERROR: No partition specified."
exit 1
fi
if [ ! -b "$DOCS_PARTITION" ]; then
echo "ERROR: $DOCS_PARTITION is not a valid block device."
exit 1
fi
fi
echo "✓ Found encrypted partition: $DOCS_PARTITION"
echo ""
echo "Step 2: Creating mount point..."
# Mount in user's home directory for easy access
MOUNT_POINT="$HOME/SecureStorage"
mkdir -p "$MOUNT_POINT"
echo "✓ Mount point ready at: $MOUNT_POINT"
echo ""
echo "Step 3: Mounting encrypted volume..."
echo "You will now be prompted for your encryption password."
echo ""
# Mount the encrypted volume
if sudo veracrypt --text --mount "$DOCS_PARTITION" "$MOUNT_POINT"; then
echo ""
echo "Setting correct permissions..."
# Change ownership of the mount point to current user
# This allows the user to read/write files
sudo chown $USER:$USER "$MOUNT_POINT"
# If there are already files, change their ownership too
if [ "$(ls -A $MOUNT_POINT 2>/dev/null)" ]; then
sudo chown -R $USER:$USER "$MOUNT_POINT"/*
fi
echo "✓ Permissions set for user access"
echo ""
echo "======================================================================"
echo " SUCCESS!"
echo "======================================================================"
echo ""
echo "Your secure storage is now accessible at:"
echo " $MOUNT_POINT"
echo ""
echo "Easy access:"
echo " • Open file manager and go to Home folder"
echo " • Look for 'SecureStorage' folder"
echo " • Or in terminal: cd ~/SecureStorage"
echo ""
echo "You can now:"
echo " • Drag and drop files"
echo " • Edit documents"
echo " • Create new folders"
echo ""
echo "IMPORTANT: When finished, run:"
echo " sudo ./lock_storage.sh"
echo ""
echo "======================================================================"
else
echo ""
echo "======================================================================"
echo " MOUNT FAILED"
echo "======================================================================"
echo ""
echo "Possible reasons:"
echo " • Wrong password"
echo " • Wrong partition selected"
echo " • Volume already mounted"
echo " • veracrypt not installed"
echo ""
echo "Troubleshooting:"
echo " 1. Check if already mounted: mount | grep secure_storage"
echo " 2. Try unmounting first: sudo veracrypt -d"
echo " 3. Verify veracrypt: which veracrypt"
echo ""
rmdir "$MOUNT_POINT" 2>/dev/null
exit 1
fi
@@ -0,0 +1,93 @@
# Portable VeraCrypt Setup Instructions
Before running the main tool, you need to download portable VeraCrypt binaries.
These will be copied to the TOOLS partition so the device works on any computer.
## Required Downloads
Download the following files and place them in this directory (`tools/veracrypt/`):
### 1. Windows Portable (REQUIRED)
- **File**: `VeraCrypt Portable.exe`
- **URL**: https://www.veracrypt.fr/en/Downloads.html
- **Section**: "Portable" under Windows
- **Size**: ~30 MB
- **Rename to**: `VeraCrypt-Portable-Windows.exe`
### 2. Linux Portable (REQUIRED)
- **Option A - GUI version**:
- Download from: https://www.veracrypt.fr/en/Downloads.html
- Generic Installer: `veracrypt-*-setup.tar.bz2`
- Extract and place: `veracrypt-*-setup-gui-x64`
- **Rename to**: `VeraCrypt-Portable-Linux`
- **Option B - Console only**:
- Download: `veracrypt-*-setup-console-x64`
- **Rename to**: `VeraCrypt-Portable-Linux-Console`
### 3. macOS Portable (OPTIONAL)
- **File**: `VeraCrypt_*.dmg`
- **URL**: https://www.veracrypt.fr/en/Downloads.html
- **Note**: Not truly "portable" but can be included for reference
- **Rename to**: `VeraCrypt-MacOS.dmg`
### 4. Android APK (OPTIONAL but recommended)
- **Source**: https://github.com/veracrypt/VeraCrypt/releases
- **Alternative**: EDS Lite from Play Store (can't include, user must download)
- **Note**: Android apps can't be "portable" but having APK helps
## Quick Download Script
Run this to download automatically (Linux):
```bash
cd tools/veracrypt/
# Windows Portable
wget https://launchpad.net/veracrypt/trunk/1.26.7/+download/VeraCrypt_Portable_1.26.7.exe \
-O VeraCrypt-Portable-Windows.exe
# Linux Console
wget https://launchpad.net/veracrypt/trunk/1.26.7/+download/veracrypt-console-1.26.7-Ubuntu-22.04-amd64.deb \
-O veracrypt-linux.deb
ar x veracrypt-linux.deb
tar xf data.tar.xz
cp usr/bin/veracrypt VeraCrypt-Portable-Linux
chmod +x VeraCrypt-Portable-Linux
rm -rf usr/ *.tar.* *.deb
# macOS
wget https://launchpad.net/veracrypt/trunk/1.26.7/+download/VeraCrypt_1.26.7.dmg \
-O VeraCrypt-MacOS.dmg
echo "✓ Downloads complete!"
```
## Verification
After downloading, your `tools/veracrypt/` directory should contain:
```
tools/veracrypt/
├── DOWNLOAD_INSTRUCTIONS.md (this file)
├── VeraCrypt-Portable-Windows.exe (~30 MB)
├── VeraCrypt-Portable-Linux (~3-5 MB)
└── VeraCrypt-MacOS.dmg (~40 MB) [optional]
```
## Notes
- **Total size**: ~70-80 MB for all platforms
- **License**: VeraCrypt is open source (Apache 2.0 / TrueCrypt License 3.0)
- **Updates**: Check veracrypt.fr periodically for newer versions
- These portable versions allow accessing your encrypted partition on ANY computer without installing software
## After Downloading
Once you have the files in place, run the main setup script:
```bash
sudo python3 covert_sd_card_tool.py -d /dev/sdX -a -t /path/to/tails.iso
```
The script will automatically copy these portable versions to the TOOLS partition.
+97
View File
@@ -0,0 +1,97 @@
# opsec — Privacy Hardening Toolkit
A comprehensive OPSEC hardening suite for Linux systems. Designed for anyone who needs strong privacy defaults without constant manual configuration.
## Features
- **Kill Switch**: iptables rules that block all non-Tor/VPN traffic when enabled
- **MAC Randomization**: Automatic MAC address spoofing on boot
- **Hostname Randomization**: Random hostname generation to prevent tracking
- **DNS Leak Prevention**: Locks DNS to privacy resolvers with immutable resolv.conf
- **Tor Integration**: Configurable Tor routing with circuit management, exit node filtering, and bridge support
- **Traffic Blending**: Decoy browsing traffic to mask real activity patterns
- **Desktop Widget**: Conky-based status HUD with theme support (7 themes included)
- **Deployment Levels**: Preset configurations from standard privacy to full paranoid mode
- **Profile System**: Save, load, and switch between configuration profiles
- **SSH Honeypot Detection**: Check SSH servers against known honeypot signatures
- **WiFi Security Auditing**: Check wireless configuration for common leaks
## Installation
```bash
sudo ./install.sh
```
This copies scripts to `/usr/local/bin/`, configs to `/etc/opsec/`, and sets up systemd services.
## Usage
```bash
# Interactive configuration
sudo opsec-config.sh
# Toggle ghost mode (advanced privacy)
sudo opsec-mode.sh on
sudo opsec-mode.sh off
# Check OPSEC status
opsec-check.sh
# Pre-flight readiness check
opsec-preflight.sh
# Kill switch control
sudo opsec-killswitch.sh on|off|status
# Apply a deployment level
sudo opsec-config.sh --level apply bare-metal-standard
```
## Deployment Levels
| Level | Description |
|---|---|
| `bare-metal-standard` | Physical machine, ghost mode toggle available |
| `bare-metal-paranoid` | Physical machine, ghost mode always on |
| `cloud-normal` | Cloud VPS, standard privacy (no MAC/hostname) |
| `cloud-paranoid` | Cloud VPS, maximum security always on |
## Configuration
All settings live in `/etc/opsec/opsec.conf`. Edit via `opsec-config.sh` (interactive TUI) or manually.
Key settings:
- `TOR_BLACKLIST` — Comma-separated country codes to exclude from Tor exit nodes
- `DNS_MODE` — DNS resolution mode: `tor`, `quad9`, `cloudflare`, `doh`, `dot`, `custom`
- `HOSTNAME_PATTERN` — Hostname strategy: `desktop`, `random`, `custom`
- `LEVEL_TYPE``standard` (toggle) or `paranoid` (always on)
## Widget Themes
Seven color themes for the desktop status widget:
`default` `aurora` `ember` `slate` `cyberpunk` `frost` `terminal`
```bash
sudo opsec-config.sh --theme apply cyberpunk
```
## Important: Review Your Configuration
**This toolkit ships with intentionally generic defaults.** After installing, you **must** run `sudo opsec-config.sh` and review every setting.
Key items that require your input:
- **`TOR_BLACKLIST`** is empty by default. You need to set exit node exclusions based on your threat model.
- **`HOSTNAME_PATTERN`** defaults to `desktop` with no prefix. Set to `random` if you want randomization.
- **Kill switch and traffic blending** are off by default. Enable them if your threat model requires it.
- **Deployment level** should be selected to match your environment (bare metal vs cloud, standard vs paranoid).
The generic defaults are **safe but minimal**. They prevent accidental misconfiguration but do not represent a hardened posture. Customize for your needs.
## Responsible Use
This toolkit is designed for legitimate privacy protection. Secure deletion features are irreversible. Network anonymization tools have limitations and are not a guarantee of anonymity. Comply with applicable laws in your jurisdiction. This software is provided as-is.
## License
MIT
+3
View File
@@ -0,0 +1,3 @@
# /etc/cron.d/opsec-banner-cache — Update WAN IP cache for terminal banner
# Runs every 5 minutes to keep the cached external IP current
*/5 * * * * root /usr/local/bin/opsec-banner-cache.sh >/dev/null 2>&1
+6
View File
@@ -0,0 +1,6 @@
# /etc/cron.d/opsec-log-rotate — Secure log rotation
# Interval synced from LOG_ROTATION_HOURS in /etc/opsec/opsec.conf (default: 4h)
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
0 */4 * * * root /usr/local/bin/opsec-log-rotate.sh >/dev/null 2>&1
@@ -0,0 +1,9 @@
[Desktop Entry]
Type=Application
Name=OPSEC Mode Toggle
Comment=Toggle between OPSEC Advanced and Standard modes
Exec=/usr/local/bin/opsec-toggle.sh
Icon=security-high
Terminal=false
Categories=System;Security;
Keywords=opsec;security;toggle;mode;
@@ -0,0 +1,85 @@
# /etc/opsec/levels/bare-metal-paranoid.conf — Physical Machine (Paranoid)
# Maximum security always on. Ghost mode is the default — cannot be disabled.
# Apply via: sudo opsec-config.sh --level apply bare-metal-paranoid
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
# paranoid = ghost mode always on, cannot disable
LEVEL_TYPE="paranoid"
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
PROFILE_NAME="default"
# ─── BASE STATE ───────────────────────────────────────────────────────────────
BASE_DNS="quad9"
BASE_MAC_RANDOMIZE="1"
BASE_IPV6_DISABLE="1"
# ─── TOR SETTINGS ─────────────────────────────────────────────────────────────
TOR_CIRCUIT_ROTATION="15"
TOR_BLACKLIST=""
TOR_STRICT_NODES="1"
TOR_ISOLATION="1"
TOR_PADDING="1"
TOR_SOCKS_PORT="9050"
TOR_DNS_PORT="5353"
TOR_NUM_GUARDS="3"
TOR_SAFE_LOGGING="1"
# ─── DNS SETTINGS ─────────────────────────────────────────────────────────────
DNS_MODE="tor"
DNS_CUSTOM_SERVERS=""
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
KILLSWITCH_ALLOW_DHCP="1"
KILLSWITCH_ALLOW_OPENVPN="1"
KILLSWITCH_ALLOW_WIREGUARD="1"
KILLSWITCH_EXTRA_PORTS=""
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
MAC_INTERFACES="auto"
MAC_VENDOR_SPOOF=""
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
HOSTNAME_PATTERN="random"
HOSTNAME_CUSTOM_PREFIX=""
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
HARDEN_IPV6="1"
HARDEN_SWAP="1"
HARDEN_CORE_DUMPS="1"
HARDEN_CLIPBOARD_CLEAR="1"
HARDEN_SCREEN_LOCK="1"
HARDEN_SCREEN_LOCK_TIMEOUT="300"
HARDEN_TIMEZONE_SPOOF="1"
HARDEN_TIMEZONE_VALUE="UTC"
HARDEN_LOCALE_SPOOF="1"
HARDEN_LOCALE_VALUE="en_US.UTF-8"
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
LEAK_WEBRTC_BLOCK="1"
LEAK_USB_BLOCK="1"
# ─── MONITORING ────────────────────────────────────────────────────────────────
MONITOR_PROCESSES="1"
MONITOR_LOG_ROTATION="1"
LOG_ROTATION_HOURS="1"
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
TRAFFIC_JITTER_ENABLED="1"
TRAFFIC_JITTER_MS="50"
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
TOR_BRIDGE_MODE="obfs4"
TOR_BRIDGE_RELAY=""
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
WIPE_METHOD="auto"
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
DEPLOYMENT_LEVEL="bare-metal-paranoid"
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
OPSEC_BANNER="compact"
# ─── WIDGET THEME ─────────────────────────────────────────────────────────────
WIDGET_THEME="default"
@@ -0,0 +1,86 @@
# /etc/opsec/levels/bare-metal-standard.conf — Physical Machine (Standard)
# Privacy-focused daily driver. Ghost mode available via: opsec-mode on
# Apply via: sudo opsec-config.sh --level apply bare-metal-standard
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
# standard = privacy base, ghost mode toggle available
# paranoid = ghost mode always on, cannot disable
LEVEL_TYPE="standard"
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
PROFILE_NAME="default"
# ─── BASE STATE (always active in standard mode) ─────────────────────────────
BASE_DNS="quad9"
BASE_MAC_RANDOMIZE="1"
BASE_IPV6_DISABLE="1"
# ─── TOR SETTINGS (used when ghost mode is toggled ON) ───────────────────────
TOR_CIRCUIT_ROTATION="30"
TOR_BLACKLIST=""
TOR_STRICT_NODES="1"
TOR_ISOLATION="1"
TOR_PADDING="1"
TOR_SOCKS_PORT="9050"
TOR_DNS_PORT="5353"
TOR_NUM_GUARDS="3"
TOR_SAFE_LOGGING="1"
# ─── DNS SETTINGS (ghost mode DNS — base uses BASE_DNS above) ────────────────
DNS_MODE="tor"
DNS_CUSTOM_SERVERS=""
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
KILLSWITCH_ALLOW_DHCP="1"
KILLSWITCH_ALLOW_OPENVPN="1"
KILLSWITCH_ALLOW_WIREGUARD="1"
KILLSWITCH_EXTRA_PORTS=""
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
MAC_INTERFACES="auto"
MAC_VENDOR_SPOOF=""
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
HOSTNAME_PATTERN="random"
HOSTNAME_CUSTOM_PREFIX=""
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
HARDEN_IPV6="1"
HARDEN_SWAP="1"
HARDEN_CORE_DUMPS="1"
HARDEN_CLIPBOARD_CLEAR="1"
HARDEN_SCREEN_LOCK="1"
HARDEN_SCREEN_LOCK_TIMEOUT="300"
HARDEN_TIMEZONE_SPOOF="0"
HARDEN_TIMEZONE_VALUE="UTC"
HARDEN_LOCALE_SPOOF="0"
HARDEN_LOCALE_VALUE="en_US.UTF-8"
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
LEAK_WEBRTC_BLOCK="1"
LEAK_USB_BLOCK="1"
# ─── MONITORING ────────────────────────────────────────────────────────────────
MONITOR_PROCESSES="1"
MONITOR_LOG_ROTATION="1"
LOG_ROTATION_HOURS="4"
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
TRAFFIC_JITTER_ENABLED="0"
TRAFFIC_JITTER_MS="50"
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
TOR_BRIDGE_MODE="off"
TOR_BRIDGE_RELAY=""
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
WIPE_METHOD="auto"
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
DEPLOYMENT_LEVEL="bare-metal-standard"
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
OPSEC_BANNER="compact"
# ─── WIDGET THEME ─────────────────────────────────────────────────────────────
WIDGET_THEME="default"
+85
View File
@@ -0,0 +1,85 @@
# /etc/opsec/levels/cloud-normal.conf — Cloud VPS (Standard)
# Privacy-focused cloud deployment. Ghost mode available via: opsec-mode on
# No MAC/hostname/screen/USB (hardware-irrelevant on cloud).
# Apply via: sudo opsec-config.sh --level apply cloud-normal
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
LEVEL_TYPE="standard"
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
PROFILE_NAME="default"
# ─── BASE STATE (always active in standard mode) ─────────────────────────────
BASE_DNS="quad9"
BASE_MAC_RANDOMIZE="0"
BASE_IPV6_DISABLE="1"
# ─── TOR SETTINGS (used when ghost mode is toggled ON) ───────────────────────
TOR_CIRCUIT_ROTATION="30"
TOR_BLACKLIST=""
TOR_STRICT_NODES="1"
TOR_ISOLATION="1"
TOR_PADDING="0"
TOR_SOCKS_PORT="9050"
TOR_DNS_PORT="5353"
TOR_NUM_GUARDS="3"
TOR_SAFE_LOGGING="1"
# ─── DNS SETTINGS ─────────────────────────────────────────────────────────────
DNS_MODE="tor"
DNS_CUSTOM_SERVERS=""
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
KILLSWITCH_ALLOW_DHCP="1"
KILLSWITCH_ALLOW_OPENVPN="1"
KILLSWITCH_ALLOW_WIREGUARD="1"
KILLSWITCH_EXTRA_PORTS=""
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
MAC_INTERFACES="auto"
MAC_VENDOR_SPOOF=""
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
HOSTNAME_PATTERN="desktop"
HOSTNAME_CUSTOM_PREFIX=""
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
HARDEN_IPV6="1"
HARDEN_SWAP="1"
HARDEN_CORE_DUMPS="1"
HARDEN_CLIPBOARD_CLEAR="0"
HARDEN_SCREEN_LOCK="0"
HARDEN_SCREEN_LOCK_TIMEOUT="300"
HARDEN_TIMEZONE_SPOOF="0"
HARDEN_TIMEZONE_VALUE="UTC"
HARDEN_LOCALE_SPOOF="0"
HARDEN_LOCALE_VALUE="en_US.UTF-8"
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
LEAK_WEBRTC_BLOCK="1"
LEAK_USB_BLOCK="0"
# ─── MONITORING ────────────────────────────────────────────────────────────────
MONITOR_PROCESSES="1"
MONITOR_LOG_ROTATION="1"
LOG_ROTATION_HOURS="1"
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
TRAFFIC_JITTER_ENABLED="0"
TRAFFIC_JITTER_MS="50"
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
TOR_BRIDGE_MODE="off"
TOR_BRIDGE_RELAY=""
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
WIPE_METHOD="auto"
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
DEPLOYMENT_LEVEL="cloud-normal"
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
OPSEC_BANNER="compact"
# ─── WIDGET THEME ─────────────────────────────────────────────────────────────
WIDGET_THEME="default"
+84
View File
@@ -0,0 +1,84 @@
# /etc/opsec/levels/cloud-paranoid.conf — Cloud VPS (Paranoid)
# Maximum security always on. Ghost mode is the default — cannot be disabled.
# Apply via: sudo opsec-config.sh --level apply cloud-paranoid
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
LEVEL_TYPE="paranoid"
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
PROFILE_NAME="default"
# ─── BASE STATE ───────────────────────────────────────────────────────────────
BASE_DNS="quad9"
BASE_MAC_RANDOMIZE="0"
BASE_IPV6_DISABLE="1"
# ─── TOR SETTINGS ─────────────────────────────────────────────────────────────
TOR_CIRCUIT_ROTATION="15"
TOR_BLACKLIST=""
TOR_STRICT_NODES="1"
TOR_ISOLATION="1"
TOR_PADDING="1"
TOR_SOCKS_PORT="9050"
TOR_DNS_PORT="5353"
TOR_NUM_GUARDS="3"
TOR_SAFE_LOGGING="1"
# ─── DNS SETTINGS ─────────────────────────────────────────────────────────────
DNS_MODE="tor"
DNS_CUSTOM_SERVERS=""
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
KILLSWITCH_ALLOW_DHCP="1"
KILLSWITCH_ALLOW_OPENVPN="1"
KILLSWITCH_ALLOW_WIREGUARD="1"
KILLSWITCH_EXTRA_PORTS=""
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
MAC_INTERFACES="auto"
MAC_VENDOR_SPOOF=""
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
HOSTNAME_PATTERN="desktop"
HOSTNAME_CUSTOM_PREFIX=""
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
HARDEN_IPV6="1"
HARDEN_SWAP="1"
HARDEN_CORE_DUMPS="1"
HARDEN_CLIPBOARD_CLEAR="1"
HARDEN_SCREEN_LOCK="0"
HARDEN_SCREEN_LOCK_TIMEOUT="300"
HARDEN_TIMEZONE_SPOOF="1"
HARDEN_TIMEZONE_VALUE="UTC"
HARDEN_LOCALE_SPOOF="1"
HARDEN_LOCALE_VALUE="en_US.UTF-8"
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
LEAK_WEBRTC_BLOCK="1"
LEAK_USB_BLOCK="0"
# ─── MONITORING ────────────────────────────────────────────────────────────────
MONITOR_PROCESSES="1"
MONITOR_LOG_ROTATION="1"
LOG_ROTATION_HOURS="1"
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
TRAFFIC_JITTER_ENABLED="1"
TRAFFIC_JITTER_MS="50"
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
TOR_BRIDGE_MODE="obfs4"
TOR_BRIDGE_RELAY=""
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
WIPE_METHOD="auto"
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
DEPLOYMENT_LEVEL="cloud-paranoid"
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
OPSEC_BANNER="compact"
# ─── WIDGET THEME ─────────────────────────────────────────────────────────────
WIDGET_THEME="default"
+100
View File
@@ -0,0 +1,100 @@
# ─── OPSEC ALIASES ───────────────────────────────────────────────────────────
# Network checks
alias myip='curl -s ifconfig.me'
alias checkip='curl -s https://ipinfo.io/ip'
alias checkdns='cat /etc/resolv.conf'
alias ports='netstat -tulanp'
alias listen='lsof -i -P | grep LISTEN'
alias estab='lsof -i -P | grep ESTABLISHED'
# Process and connection monitoring
alias checkcon='ss -tupan | grep ESTABLISHED'
alias checklis='ss -tupan | grep LISTEN'
alias checkproc='ps auxf | grep -v grep | grep'
alias psg='ps aux | grep -v grep | grep -i'
# Emergency and cleanup
alias killcon='killall -9 openvpn ssh 2>/dev/null'
alias randmac='randomize-mac.sh'
alias opsec='opsec-check.sh'
# Cleanup operations
alias clear-logs='sudo find /var/log -name '\''opsec*'\'' -type f -exec truncate -s 0 {} \; && sudo truncate -s 0 /var/log/syslog /var/log/auth.log 2>/dev/null'
alias clear-history='history -c && > ~/.bash_history && > ~/.zsh_history'
alias shred-file='shred -vfz -n 3'
# Anonymity
alias anon-on='sudo systemctl start tor && . torsocks on'
alias anon-off='. torsocks off && sudo systemctl stop tor'
alias check-tor='curl -s https://check.torproject.org/api/ip'
# Safe OPSEC status check
alias opsec-status='echo "OPSEC Status:"; echo "VPN: $(pgrep openvpn > /dev/null && echo "Connected" || echo "Disconnected")"; echo "Tor: $(systemctl is-active tor 2>/dev/null | grep -q active && echo "Active" || echo "Inactive")"; echo "IP: $(curl -s --max-time 2 ifconfig.me || echo "Check failed")"'
# Quick OPSEC check
alias quick-opsec='opsec-status && echo "" && echo "=== Connections ===" && ss -tupln | grep -E ":443|:9050|:1080" | head -5'
# ─── ADVANCED OPSEC MODE ──────────────────────────────────────────────────────
# Master toggle
alias opsec-on='sudo /usr/local/bin/opsec-mode.sh on'
alias opsec-off='sudo /usr/local/bin/opsec-mode.sh off'
alias opsec-advanced='sudo /usr/local/bin/opsec-mode.sh status'
# Kill switch controls
alias killswitch-on='sudo /usr/local/bin/opsec-killswitch.sh on'
alias killswitch-off='sudo /usr/local/bin/opsec-killswitch.sh off'
alias killswitch-status='sudo /usr/local/bin/opsec-killswitch.sh status'
# Hostname randomization
alias randhostname='sudo /usr/local/bin/opsec-hostname-randomize.sh'
# Tor circuit management
alias check-circuit='curl -s --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip'
alias new-circuit='sudo killall -HUP tor && echo "New Tor circuit requested"'
# DNS leak check
alias check-dns-leak='echo "=== DNS Leak Check ===" && echo "resolv.conf:" && grep nameserver /etc/resolv.conf && echo "" && echo "Tor DNS test:" && dig +short @127.0.0.1 -p 5353 check.torproject.org 2>/dev/null || echo "Tor DNS not available"'
# OPSEC Config Manager
alias opsec-config='sudo /usr/local/bin/opsec-config.sh'
alias opsec-show='sudo /usr/local/bin/opsec-mode.sh status'
# Profile management
alias opsec-profile='sudo /usr/local/bin/opsec-config.sh --profile'
# Boot mode
alias opsec-boot-on='sudo /usr/local/bin/opsec-config.sh --boot on'
alias opsec-boot-off='sudo /usr/local/bin/opsec-config.sh --boot off'
# SSH honeypot check
alias ssh-safe='/usr/local/bin/opsec-ssh-check.sh'
# Connection monitor
alias opsec-monitor-start='sudo /usr/local/bin/opsec-monitor.sh start'
alias opsec-monitor-stop='sudo /usr/local/bin/opsec-monitor.sh stop'
alias opsec-monitor-status='sudo /usr/local/bin/opsec-monitor.sh status'
# WiFi security
alias opsec-wifi='sudo /usr/local/bin/opsec-wifi-check.sh'
# Conky widget controls — position: tl tc tr bl bc br (default: bl)
alias opsec-widget-kill='killall conky 2>/dev/null && echo "Widget stopped" || echo "Widget not running"'
opsec-widget() { ~/.config/conky/opsec-widget-launch.sh "${1:-bl}"; }
opsec-widget-restart() { ~/.config/conky/opsec-widget-launch.sh "${1:-bl}"; }
# Deployment level and banner control
alias opsec-level='sudo /usr/local/bin/opsec-config.sh --level apply'
alias opsec-level-list='sudo /usr/local/bin/opsec-config.sh --level list'
alias opsec-banner-set='sudo /usr/local/bin/opsec-config.sh --banner'
alias opsec-toggle='/usr/local/bin/opsec-toggle.sh'
# Preflight and break-glass
alias opsec-preflight='/usr/local/bin/opsec-preflight.sh'
alias opsec-score='/usr/local/bin/opsec-preflight.sh --score'
alias opsec-breakglass='sudo /usr/local/bin/opsec-mode.sh breakglass'
alias opsec-breakglass-off='sudo /usr/local/bin/opsec-mode.sh breakglass-off'
# ─── OPSEC TERMINAL BANNER ──────────────────────────────────────────────────
[ -x /usr/local/bin/opsec-banner.sh ] && /usr/local/bin/opsec-banner.sh
+28
View File
@@ -0,0 +1,28 @@
# /etc/opsec/country-codes.conf — Country Code Presets for Tor Blacklisting
# Used by opsec-config.sh and opsec-lib.sh for quick preset loading
# Format: PRESET_NAME="cc1,cc2,cc3,..."
# ─── INTELLIGENCE ALLIANCES ────────────────────────────────────────────────────
# Five Eyes — core anglosphere intelligence sharing
FIVE_EYES="us,gb,ca,au,nz"
# Nine Eyes — Five Eyes + Denmark, France, Netherlands, Norway
NINE_EYES="us,gb,ca,au,nz,dk,fr,nl,no"
# Fourteen Eyes — Nine Eyes + Germany, Belgium, Italy, Sweden, Spain
FOURTEEN_EYES="us,gb,ca,au,nz,dk,fr,nl,no,de,be,it,se,es"
# ─── HIGH-SURVEILLANCE STATES ─────────────────────────────────────────────────
# Countries with known mass surveillance / hostile SIGINT
SURVEILLANCE_STATES="cn,ru,ir,kp,il,sg,ae,sa,eg,tr,pk,th,vn,by,kz"
# ─── COMBINED PRESETS ──────────────────────────────────────────────────────────
# Maximum exclusion: Fourteen Eyes + surveillance states
MAX_EXCLUSION="us,gb,ca,au,nz,dk,fr,nl,no,de,be,it,se,es,cn,ru,ir,kp,il,sg,ae,sa,eg,tr,pk,th,vn,by,kz"
# ─── VALID COUNTRY CODES (for input validation) ───────────────────────────────
# ISO 3166-1 alpha-2 codes commonly used as Tor country codes
VALID_CODES="ad,ae,af,ag,ai,al,am,ao,ar,as,at,au,az,ba,bb,bd,be,bf,bg,bh,bi,bj,bm,bn,bo,br,bs,bt,bw,by,bz,ca,cd,cf,cg,ch,ci,cl,cm,cn,co,cr,cu,cv,cy,cz,de,dj,dk,dm,do,dz,ec,ee,eg,er,es,et,fi,fj,fm,fr,ga,gb,gd,ge,gh,gm,gn,gp,gq,gr,gt,gw,gy,hk,hn,hr,ht,hu,id,ie,il,in,iq,ir,is,it,jm,jo,jp,ke,kg,kh,ki,km,kn,kp,kr,kw,kz,la,lb,lc,li,lk,lr,ls,lt,lu,lv,ly,ma,mc,md,me,mg,mh,mk,ml,mm,mn,mo,mr,mt,mu,mv,mw,mx,my,mz,na,ne,ng,ni,nl,no,np,nr,nz,om,pa,pe,pg,ph,pk,pl,pt,pw,py,qa,ro,rs,ru,rw,sa,sb,sc,sd,se,sg,si,sk,sl,sm,sn,so,sr,ss,st,sv,sy,sz,td,tg,th,tj,tl,tm,tn,to,tr,tt,tv,tw,tz,ua,ug,us,uy,uz,va,vc,ve,vn,vu,ws,ye,za,zm,zw"
+96
View File
@@ -0,0 +1,96 @@
# /etc/opsec/opsec.conf — Central OPSEC Configuration
# Shell-sourceable KEY="value" format. All scripts source this file.
# Edit via: sudo opsec-config.sh (interactive TUI)
# Manual edits: source /usr/local/lib/opsec/opsec-lib.sh && opsec_set_value KEY "value"
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
PROFILE_NAME="default"
# ─── TOR SETTINGS ──────────────────────────────────────────────────────────────
TOR_CIRCUIT_ROTATION="30"
TOR_BLACKLIST=""
TOR_STRICT_NODES="1"
TOR_ISOLATION="1"
TOR_PADDING="1"
TOR_SOCKS_PORT="9050"
TOR_DNS_PORT="5353"
TOR_NUM_GUARDS="3"
TOR_SAFE_LOGGING="1"
# ─── DNS SETTINGS ──────────────────────────────────────────────────────────────
# Mode: tor | quad9 | cloudflare | doh | dot | custom
DNS_MODE="tor"
DNS_CUSTOM_SERVERS=""
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
KILLSWITCH_ALLOW_DHCP="1"
KILLSWITCH_ALLOW_OPENVPN="1"
KILLSWITCH_ALLOW_WIREGUARD="1"
KILLSWITCH_EXTRA_PORTS=""
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
# Interfaces: auto (all non-lo) or comma-separated list (e.g. "eth0,wlan0")
MAC_INTERFACES="auto"
# Vendor spoof: empty for random, or OUI prefix (e.g. "00:1A:2B" for specific vendor)
MAC_VENDOR_SPOOF=""
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
# Pattern: desktop | random | custom
HOSTNAME_PATTERN="desktop"
HOSTNAME_CUSTOM_PREFIX=""
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
HARDEN_IPV6="1"
HARDEN_SWAP="1"
HARDEN_CORE_DUMPS="1"
HARDEN_CLIPBOARD_CLEAR="0"
HARDEN_SCREEN_LOCK="1"
HARDEN_SCREEN_LOCK_TIMEOUT="300"
HARDEN_TIMEZONE_SPOOF="0"
HARDEN_TIMEZONE_VALUE="UTC"
HARDEN_LOCALE_SPOOF="0"
HARDEN_LOCALE_VALUE="en_US.UTF-8"
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
LEAK_WEBRTC_BLOCK="1"
LEAK_USB_BLOCK="0"
# ─── MONITORING ────────────────────────────────────────────────────────────────
MONITOR_PROCESSES="0"
MONITOR_LOG_ROTATION="1"
LOG_ROTATION_HOURS="4"
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
TRAFFIC_JITTER_ENABLED="0"
TRAFFIC_JITTER_MS="50"
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
# standard = privacy base, ghost mode toggle available
# paranoid = ghost mode always on, cannot disable
LEVEL_TYPE="standard"
# ─── BASE STATE (always active in standard mode) ─────────────────────────────
BASE_DNS="quad9"
BASE_MAC_RANDOMIZE="1"
BASE_IPV6_DISABLE="1"
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
# Mode: off | obfs4 | meek-azure | snowflake
TOR_BRIDGE_MODE="off"
TOR_BRIDGE_RELAY=""
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
# Method: auto | shred | fstrim | luks
WIPE_METHOD="auto"
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
# Level: bare-metal-standard | bare-metal-paranoid | cloud-normal | cloud-paranoid
DEPLOYMENT_LEVEL="bare-metal-standard"
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
# Mode: compact | full | auto | off
OPSEC_BANNER="compact"
# ─── WIDGET THEME ────────────────────────────────────────────────────────────
# Theme: default | aurora | ember | slate | cyberpunk | frost | terminal
WIDGET_THEME="default"
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1/policyconfig.dtd">
<policyconfig>
<action id="com.opsec.mode.toggle">
<description>Toggle OPSEC Advanced Mode</description>
<message>Authentication is required to toggle OPSEC mode</message>
<defaults>
<allow_any>auth_admin</allow_any>
<allow_inactive>auth_admin</allow_inactive>
<allow_active>yes</allow_active>
</defaults>
<annotate key="org.freedesktop.policykit.exec.path">/usr/local/bin/opsec-mode.sh</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
</action>
</policyconfig>
+14
View File
@@ -0,0 +1,14 @@
# OPSEC DNS Servers
# Quad9 (Security focused)
nameserver 9.9.9.9
nameserver 149.112.112.112
# Cloudflare (Privacy focused)
nameserver 1.1.1.1
nameserver 1.0.0.1
# DNS Options
options edns0 trust-ad
options timeout:1
options attempts:1
options rotate
+3
View File
@@ -0,0 +1,3 @@
# OPSEC Mode DNS — all resolution through Tor DNSPort
# Deployed by opsec-mode on, locked with chattr +i
nameserver 127.0.0.1
@@ -0,0 +1,18 @@
[Unit]
Description=OPSEC Boot-into-Advanced Mode Initializer
Documentation=man:opsec-mode(8)
DefaultDependencies=no
Before=network-pre.target NetworkManager.service networking.service
Before=opsec-mac-randomize.service opsec-hostname-randomize.service opsec-killswitch.service
After=local-fs.target sysinit.target
ConditionPathExists=/etc/opsec/boot-advanced.enabled
[Service]
Type=oneshot
ExecStart=/usr/local/bin/opsec-boot-init.sh
RemainAfterExit=yes
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,14 @@
[Unit]
Description=OPSEC Hostname Randomization
Before=NetworkManager.service
Before=networking.service
After=local-fs.target
ConditionPathExists=/var/run/opsec-advanced.enabled
[Service]
Type=oneshot
ExecStart=/usr/local/bin/opsec-hostname-randomize.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,15 @@
[Unit]
Description=OPSEC Kill Switch (block non-Tor/VPN traffic)
Before=NetworkManager.service
Before=networking.service
After=local-fs.target
ConditionPathExists=/var/run/opsec-advanced.enabled
[Service]
Type=oneshot
ExecStart=/usr/local/bin/opsec-killswitch.sh on
ExecStop=/usr/local/bin/opsec-killswitch.sh off
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
@@ -0,0 +1,14 @@
[Unit]
Description=OPSEC MAC Address Randomization
Before=NetworkManager.service
Before=networking.service
After=local-fs.target
ConditionPathExists=/var/run/opsec-advanced.enabled
[Service]
Type=oneshot
ExecStart=/usr/local/bin/randomize-mac.sh
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
+14
View File
@@ -0,0 +1,14 @@
# Aurora theme — purple and cyan tones
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="Aurora"
CONKY_BG="000000"
CONKY_COLOR0="B55AFC"
CONKY_COLOR1="FF63BE"
CONKY_COLOR2="85E7FF"
CONKY_COLOR3="000000"
CONKY_COLOR4="268BD2"
CONKY_COLOR5="07CAF9"
CONKY_COLOR6="85E7FF"
CONKY_COLOR7="ECDEF7"
CONKY_COLOR8="B55AFC"
CONKY_COLOR9="4A6A7A"
+14
View File
@@ -0,0 +1,14 @@
# CyberPunk theme — neon cyan/pink/green on black
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="CyberPunk"
CONKY_BG="000000"
CONKY_COLOR0="FF3B3B"
CONKY_COLOR1="00F5A0"
CONKY_COLOR2="08F7FE"
CONKY_COLOR3="000000"
CONKY_COLOR4="08F7FE"
CONKY_COLOR5="FF2E88"
CONKY_COLOR6="00D1FF"
CONKY_COLOR7="EAEAFF"
CONKY_COLOR8="08F7FE"
CONKY_COLOR9="2A2A2A"
+14
View File
@@ -0,0 +1,14 @@
# Default theme — balanced green-on-black aesthetic
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="Default"
CONKY_BG="000000"
CONKY_COLOR0="FF4C4C"
CONKY_COLOR1="00FF9C"
CONKY_COLOR2="49D6B6"
CONKY_COLOR3="000000"
CONKY_COLOR4="49D6B6"
CONKY_COLOR5="2AFFC6"
CONKY_COLOR6="2AFFC6"
CONKY_COLOR7="CFFFE6"
CONKY_COLOR8="49D6B6"
CONKY_COLOR9="1A1A1A"
+14
View File
@@ -0,0 +1,14 @@
# Ember theme — warm red and white tones
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="Ember"
CONKY_BG="000000"
CONKY_COLOR0="FF2B2B"
CONKY_COLOR1="A0A0A0"
CONKY_COLOR2="B0B0B0"
CONKY_COLOR3="000000"
CONKY_COLOR4="FF2B2B"
CONKY_COLOR5="FF7A7A"
CONKY_COLOR6="CFCFCF"
CONKY_COLOR7="EDEDED"
CONKY_COLOR8="FF2B2B"
CONKY_COLOR9="A0A0A0"
+14
View File
@@ -0,0 +1,14 @@
# Frost theme — cool blues and teals
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="Frost"
CONKY_BG="000000"
CONKY_COLOR0="DC2626"
CONKY_COLOR1="5EEAD4"
CONKY_COLOR2="00E5FF"
CONKY_COLOR3="000000"
CONKY_COLOR4="22D3EE"
CONKY_COLOR5="00E5FF"
CONKY_COLOR6="22D3EE"
CONKY_COLOR7="DDE6F3"
CONKY_COLOR8="00E5FF"
CONKY_COLOR9="1A1A1A"
+14
View File
@@ -0,0 +1,14 @@
# Slate theme — muted neutral tones with gold accent
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="Slate"
CONKY_BG="000000"
CONKY_COLOR0="C63636"
CONKY_COLOR1="9A9A9A"
CONKY_COLOR2="B0B0B0"
CONKY_COLOR3="000000"
CONKY_COLOR4="7A7A7A"
CONKY_COLOR5="D1A954"
CONKY_COLOR6="AFAFAF"
CONKY_COLOR7="E6E6E6"
CONKY_COLOR8="B0B0B0"
CONKY_COLOR9="7A7A7A"
+14
View File
@@ -0,0 +1,14 @@
# Terminal theme — classic monochrome green CRT aesthetic
# Deployed to /etc/opsec/themes/ — sourced by opsec_generate_conky()
THEME_LABEL="Terminal"
CONKY_BG="0A0A0A"
CONKY_COLOR0="FF3333"
CONKY_COLOR1="33FF33"
CONKY_COLOR2="22BB22"
CONKY_COLOR3="0A0A0A"
CONKY_COLOR4="118811"
CONKY_COLOR5="44FF44"
CONKY_COLOR6="33DD33"
CONKY_COLOR7="AAFFAA"
CONKY_COLOR8="22CC22"
CONKY_COLOR9="1A3A1A"
+5
View File
@@ -0,0 +1,5 @@
# Minimal stock Tor configuration
# Restored by opsec-mode off
SocksPort 9050
Log notice file /var/log/tor/notices.log
+32
View File
@@ -0,0 +1,32 @@
# Hardened Tor Configuration for OPSEC Mode
# Deployed by opsec-mode on — restored to torrc-default on off
# ─── SOCKS & DNS ────────────────────────────────────────────────────────────
SocksPort 9050 IsolateDestAddr IsolateDestPort
DNSPort 5353
# ─── FIVE EYES EXCLUSION ────────────────────────────────────────────────────
# Never use exit nodes in Five Eyes countries
ExcludeExitNodes {us},{gb},{ca},{au},{nz}
StrictNodes 1
# ─── CIRCUIT ROTATION ───────────────────────────────────────────────────────
# Rotate circuits every 30 seconds for maximum anonymity
MaxCircuitDirtiness 30
# ─── STREAM ISOLATION ───────────────────────────────────────────────────────
# Each destination gets its own circuit
IsolateSOCKSAuth 1
# ─── TRAFFIC PADDING ────────────────────────────────────────────────────────
# Pad cells to resist traffic analysis
ConnectionPadding 1
# ─── SAFE LOGGING ────────────────────────────────────────────────────────────
# Scrub sensitive info from logs
SafeLogging 1
Log notice file /var/log/tor/notices.log
# ─── PERFORMANCE ─────────────────────────────────────────────────────────────
NumEntryGuards 3
UseEntryGuards 1
+15
View File
@@ -0,0 +1,15 @@
# /etc/udev/rules.d/99-opsec-usb.rules — OPSEC USB Device Monitoring
# Logs all USB insertions and sends desktop notification
# Optionally blocks new devices when LEAK_USB_BLOCK=1 in /etc/opsec/opsec.conf
# Log all USB device insertions
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
RUN+="/bin/bash -c 'echo \"[$(date +%%Y-%%m-%%d\\ %%H:%%M:%%S)] USB INSERT: vendor=$attr{idVendor} product=$attr{idProduct} serial=$attr{serial}\" >> /var/log/opsec-usb.log'"
# Desktop notification on USB insertion
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
RUN+="/bin/bash -c 'REAL_USER=$(who | head -1 | awk \"{print \\$1}\"); [ -n \"$REAL_USER\" ] && su - $REAL_USER -c \"DISPLAY=:0 notify-send -u critical OPSEC\\ USB \\\"USB device inserted: $attr{idVendor}:$attr{idProduct}\\\"\" 2>/dev/null || true'"
# Log USB mass storage specifically (higher risk)
ACTION=="add", SUBSYSTEM=="block", ENV{ID_USB_DRIVER}=="usb-storage", \
RUN+="/bin/bash -c 'echo \"[$(date +%%Y-%%m-%%d\\ %%H:%%M:%%S)] USB STORAGE: $env{ID_VENDOR} $env{ID_MODEL} $env{ID_SERIAL}\" >> /var/log/opsec-usb.log'"
@@ -0,0 +1,69 @@
# Double-Hop OpenVPN Configuration Template
# Chain two VPN servers for additional anonymity layer
# Replace %%VARIABLES%% with actual values before use
#
# Usage: Copy and fill in variables, then:
# sudo openvpn --config double-hop.ovpn
# ─── First Hop (Entry Server) ─────────────────────────────────────────────────
# This is the server your ISP sees you connecting to
client
dev tun
proto %%PROTO_1%%
remote %%SERVER_1%% %%PORT_1%%
resolv-retry infinite
nobind
persist-key
persist-tun
# Authentication
auth-user-pass %%AUTH_FILE_1%%
ca %%CA_FILE_1%%
# Security
cipher AES-256-GCM
auth SHA512
tls-cipher TLS-ECDHE-RSA-WITH-AES-256-GCM-SHA384
tls-version-min 1.2
remote-cert-tls server
# Routing — send all traffic through VPN
redirect-gateway def1
dhcp-option DNS 10.8.0.1
# Keepalive
keepalive 10 60
ping-timer-rem
# Logging
verb 3
mute 10
# ─── Second Hop Configuration ─────────────────────────────────────────────────
# After first VPN is established, launch second hop:
#
# sudo openvpn --config hop2.ovpn --route-nopull \
# --route %%SERVER_2%% 255.255.255.255 net_gateway
#
# Where hop2.ovpn contains:
# client
# dev tun1
# proto %%PROTO_2%%
# remote %%SERVER_2%% %%PORT_2%%
# auth-user-pass %%AUTH_FILE_2%%
# ca %%CA_FILE_2%%
# cipher AES-256-GCM
# redirect-gateway def1
# ─── Variables Reference ───────────────────────────────────────────────────────
# %%SERVER_1%% — First hop IP/hostname
# %%PORT_1%% — First hop port (e.g. 1194, 443)
# %%PROTO_1%% — First hop protocol (udp/tcp)
# %%AUTH_FILE_1%% — First hop credentials file
# %%CA_FILE_1%% — First hop CA certificate
# %%SERVER_2%% — Second hop IP/hostname
# %%PORT_2%% — Second hop port
# %%PROTO_2%% — Second hop protocol
# %%AUTH_FILE_2%% — Second hop credentials file
# %%CA_FILE_2%% — Second hop CA certificate
@@ -0,0 +1,72 @@
# WireGuard Multi-Hop Configuration Template
# Chain WireGuard tunnels for additional anonymity
# Replace %%VARIABLES%% with actual values
#
# Architecture:
# You → wg0 (Hop 1) → wg1 (Hop 2) → Internet
#
# Usage:
# sudo cp this-file /etc/wireguard/wg0.conf (fill in variables)
# sudo wg-quick up wg0
# sudo wg-quick up wg1
# ═══════════════════════════════════════════════════════════════════════════════
# HOP 1: /etc/wireguard/wg0.conf — Entry tunnel (your ISP sees this)
# ═══════════════════════════════════════════════════════════════════════════════
[Interface]
PrivateKey = %%PRIVATE_KEY_1%%
Address = %%TUNNEL_IP_1%%/32
DNS = %%DNS_1%%
# MTU adjustment for encapsulation overhead
MTU = 1380
# Post-routing to allow hop 2 through hop 1
PostUp = ip rule add from %%TUNNEL_IP_2%% table 200; ip route add default via %%GATEWAY_1%% table 200
PostDown = ip rule del from %%TUNNEL_IP_2%% table 200; ip route del default via %%GATEWAY_1%% table 200
[Peer]
PublicKey = %%PUBLIC_KEY_1%%
PresharedKey = %%PSK_1%%
Endpoint = %%ENDPOINT_1%%:%%PORT_1%%
AllowedIPs = 0.0.0.0/0, ::/0
PersistentKeepalive = 25
# ═══════════════════════════════════════════════════════════════════════════════
# HOP 2: /etc/wireguard/wg1.conf — Exit tunnel (internet sees this)
# ═══════════════════════════════════════════════════════════════════════════════
#
# [Interface]
# PrivateKey = %%PRIVATE_KEY_2%%
# Address = %%TUNNEL_IP_2%%/32
# DNS = %%DNS_2%%
# MTU = 1340
# # Route this tunnel's traffic through wg0
# Table = off
# PostUp = ip rule add from %%TUNNEL_IP_2%% table 200
# PostDown = ip rule del from %%TUNNEL_IP_2%% table 200
#
# [Peer]
# PublicKey = %%PUBLIC_KEY_2%%
# PresharedKey = %%PSK_2%%
# Endpoint = %%ENDPOINT_2%%:%%PORT_2%%
# AllowedIPs = 0.0.0.0/0, ::/0
# PersistentKeepalive = 25
# ─── Variables Reference ───────────────────────────────────────────────────────
# %%PRIVATE_KEY_1%% — Your private key for hop 1 (wg genkey)
# %%PUBLIC_KEY_1%% — Hop 1 server's public key
# %%PSK_1%% — Preshared key for hop 1 (wg genpsk)
# %%ENDPOINT_1%% — Hop 1 server IP/hostname
# %%PORT_1%% — Hop 1 server port (default: 51820)
# %%TUNNEL_IP_1%% — Your assigned tunnel IP on hop 1
# %%GATEWAY_1%% — Hop 1 internal gateway IP
# %%DNS_1%% — DNS server for hop 1
#
# %%PRIVATE_KEY_2%% — Your private key for hop 2
# %%PUBLIC_KEY_2%% — Hop 2 server's public key
# %%PSK_2%% — Preshared key for hop 2
# %%ENDPOINT_2%% — Hop 2 server IP/hostname (reachable via hop 1)
# %%PORT_2%% — Hop 2 server port
# %%TUNNEL_IP_2%% — Your assigned tunnel IP on hop 2
# %%DNS_2%% — DNS server for hop 2
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
# conky-opsec-cache — Background IP/geo cache updater
# Runs in a loop, writes results to cache file that the status script reads.
# This keeps all slow network calls OUT of the conky render path.
CACHE_DIR="/tmp/.opsec-cache"
CACHE_FILE="${CACHE_DIR}/netinfo"
LOCK_FILE="${CACHE_DIR}/.updating"
INTERVAL=30 # seconds between updates
mkdir -p "$CACHE_DIR"
chmod 700 "$CACHE_DIR"
update_cache() {
# Prevent concurrent updates
[ -f "$LOCK_FILE" ] && return
touch "$LOCK_FILE"
local advanced=false
[ -f /var/run/opsec-advanced.enabled ] && advanced=true
local pub_ip="" pub_geo="" routed_tor=false exit_country=""
# In Advanced mode with SOCKS available: verify Tor via HTTPS, then get geo
if $advanced && ss -tln 2>/dev/null | grep -q ':9050 '; then
# Step 1: Verify Tor routing via HTTPS (encrypted, trusted endpoint)
local tor_json
tor_json=$(curl -4 -s --max-time 10 --socks5-hostname 127.0.0.1:9050 "https://check.torproject.org/api/ip" 2>/dev/null)
if echo "$tor_json" | grep -q '"IsTor":true'; then
pub_ip=$(echo "$tor_json" | grep -o '"IP":"[^"]*"' | cut -d'"' -f4)
if [ -n "$pub_ip" ] && echo "$pub_ip" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
routed_tor=true
else
pub_ip=""
fi
fi
# Step 2: Get geo data via ip-api.com (HTTP only — but over Tor SOCKS so exit encrypts)
# ip-api.com pro HTTPS requires a paid key; free tier is HTTP-only
# This is acceptable: traffic goes through Tor SOCKS tunnel, so local network can't see it
if [ -n "$pub_ip" ]; then
local geo_json
geo_json=$(curl -4 -s --max-time 10 --socks5-hostname 127.0.0.1:9050 "http://ip-api.com/json/${pub_ip}?fields=countryCode,city" 2>/dev/null)
local geo_country geo_city
geo_country=$(echo "$geo_json" | grep -o '"countryCode":"[^"]*"' | cut -d'"' -f4)
geo_city=$(echo "$geo_json" | grep -o '"city":"[^"]*"' | cut -d'"' -f4)
exit_country="$geo_country"
[ -n "$geo_country" ] && pub_geo="${geo_city:+${geo_city}, }${geo_country}"
fi
fi
# Fallback to direct — only if NOT in advanced mode (kill switch would DROP it)
if [ -z "$pub_ip" ] && ! $advanced; then
local api_json
api_json=$(curl -4 -s --max-time 4 "https://check.torproject.org/api/ip" 2>/dev/null)
if [ -n "$api_json" ]; then
pub_ip=$(echo "$api_json" | grep -o '"IP":"[^"]*"' | cut -d'"' -f4)
fi
# Validate IP format
if [ -n "$pub_ip" ] && ! echo "$pub_ip" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
pub_ip=""
fi
# Get geo via HTTPS (direct mode — no Tor)
if [ -n "$pub_ip" ]; then
local geo_json
geo_json=$(curl -4 -s --max-time 4 "http://ip-api.com/json/${pub_ip}?fields=countryCode,city" 2>/dev/null)
local geo_country geo_city
geo_country=$(echo "$geo_json" | grep -o '"countryCode":"[^"]*"' | cut -d'"' -f4)
geo_city=$(echo "$geo_json" | grep -o '"city":"[^"]*"' | cut -d'"' -f4)
[ -n "$geo_country" ] && pub_geo="${geo_city:+${geo_city}, }${geo_country}"
fi
fi
# Sanitize exit country — should be 2-3 letter country code
[ -n "$exit_country" ] && [ ${#exit_country} -gt 3 ] && exit_country=""
# Track when exit IP last changed (for circuit age display)
local exit_change_time=""
local prev_ip="" prev_change_time=""
if [ -f "$CACHE_FILE" ]; then
prev_ip=$(grep '^PUB_IP=' "$CACHE_FILE" 2>/dev/null | cut -d'"' -f2)
prev_change_time=$(grep '^EXIT_CHANGE_TIME=' "$CACHE_FILE" 2>/dev/null | cut -d'"' -f2)
fi
if [ -n "$pub_ip" ] && [ "$pub_ip" != "$prev_ip" ]; then
exit_change_time="$(date +%s)"
elif [ -n "$prev_change_time" ]; then
exit_change_time="$prev_change_time"
else
exit_change_time="$(date +%s)"
fi
# Tor bootstrap progress — only from current Tor session
local tor_bootstrap="" tor_phase=""
if $advanced && systemctl is-active tor >/dev/null 2>&1; then
# Get Tor start time, only read log lines after it
local tor_start
tor_start=$(systemctl show tor@default --property=ActiveEnterTimestamp 2>/dev/null | cut -d= -f2)
if [ -n "$tor_start" ]; then
local start_ts
start_ts=$(date -d "$tor_start" +%s 2>/dev/null || echo 0)
local boot_line=""
# Read bootstrap lines — -h suppresses filename prefix when checking both paths
while IFS= read -r line; do
local log_date
log_date=$(echo "$line" | grep -oP '^\w+ \d+ [\d:.]+')
if [ -n "$log_date" ]; then
local log_ts
log_ts=$(date -d "$log_date" +%s 2>/dev/null || echo 0)
[ "$log_ts" -ge "$start_ts" ] && boot_line="$line"
fi
done < <(grep -h "Bootstrapped" /run/tor/notices.log /var/log/tor/notices.log 2>/dev/null)
if [ -n "$boot_line" ]; then
tor_bootstrap=$(echo "$boot_line" | grep -oP '\d+(?=%)')
tor_phase=$(echo "$boot_line" | grep -oP '\(\K[^)]+' | head -1)
fi
fi
fi
# Write atomically (write to tmp then move) — quote values for safe sourcing
local tmp="${CACHE_FILE}.tmp"
cat > "$tmp" <<EOF
PUB_IP="${pub_ip}"
PUB_GEO="${pub_geo}"
ROUTED_TOR="${routed_tor}"
EXIT_COUNTRY="${exit_country}"
EXIT_CHANGE_TIME="${exit_change_time}"
TOR_BOOTSTRAP="${tor_bootstrap}"
TOR_PHASE="${tor_phase}"
CACHE_TIME="$(date +%s)"
EOF
chmod 600 "$tmp"
mv -f "$tmp" "$CACHE_FILE"
rm -f "$LOCK_FILE"
}
# Initial update immediately
update_cache
# Loop forever — poll faster when Tor is bootstrapping
while true; do
if [ -f /var/run/opsec-advanced.enabled ] && systemctl is-active tor >/dev/null 2>&1 && ! ss -tln 2>/dev/null | grep -q ':9050 '; then
sleep 5 # Tor bootstrapping — fast poll
else
sleep "$INTERVAL"
fi
update_cache
done
+225
View File
@@ -0,0 +1,225 @@
#!/bin/bash
# Conky OPSEC Status — OPSEC Status Widget
# Called by conky via execpi — outputs Conky color markup
# Network data is read from cache (updated by conky-opsec-cache.sh in background)
#
# Color map (managed by theme system):
# 0 = ALERT/BAD 1 = SECURE/GOOD 2 = INFO/NEUTRAL
# 3 = VOID/BG 4 = STRUCTURAL 5 = TITLE/ACCENT
# 6 = LABELS 7 = VALUES 8 = SECTION HDR
# 9 = METADATA
ADVANCED=false
[ -f /var/run/opsec-advanced.enabled ] && ADVANCED=true
# ─── READ CACHE (instant — no network calls) ──────────────────────────────
# Safe parsing: extract values via grep/cut instead of sourcing as shell code
CACHE_FILE="/tmp/.opsec-cache/netinfo"
PUB_IP="" PUB_GEO="" ROUTED_TOR=false EXIT_COUNTRY="" EXIT_CHANGE_TIME="" TOR_BOOTSTRAP="" TOR_PHASE="" CACHE_TIME=0
if [ -f "$CACHE_FILE" ]; then
_safe_read() { grep "^${1}=" "$CACHE_FILE" 2>/dev/null | head -1 | cut -d'"' -f2; }
PUB_IP=$(_safe_read PUB_IP)
PUB_GEO=$(_safe_read PUB_GEO)
ROUTED_TOR=$(_safe_read ROUTED_TOR)
EXIT_COUNTRY=$(_safe_read EXIT_COUNTRY)
EXIT_CHANGE_TIME=$(_safe_read EXIT_CHANGE_TIME)
TOR_BOOTSTRAP=$(_safe_read TOR_BOOTSTRAP)
TOR_PHASE=$(_safe_read TOR_PHASE)
CACHE_TIME=$(_safe_read CACHE_TIME)
# Sanitize: strip anything that isn't alphanumeric, dots, commas, spaces, or dashes
PUB_IP=$(echo "$PUB_IP" | tr -cd '0-9.')
EXIT_COUNTRY=$(echo "$EXIT_COUNTRY" | tr -cd 'A-Za-z')
EXIT_CHANGE_TIME=$(echo "$EXIT_CHANGE_TIME" | tr -cd '0-9')
TOR_BOOTSTRAP=$(echo "$TOR_BOOTSTRAP" | tr -cd '0-9')
fi
# Kick off cache daemon if not running
if ! pgrep -f 'conky-opsec-cache\.sh' >/dev/null 2>&1; then
nohup ~/.config/conky/conky-opsec-cache.sh >/dev/null 2>&1 &
fi
# ─── HEADER ──────────────────────────────────────────────────────────────────
echo "\${color4}\${hr 1}"
if $ADVANCED; then
echo "\${color5}\${font JetBrains Mono:bold:size=11}\${alignc}OPSEC STATUS\${font}"
echo "\${color1}\${font JetBrains Mono:bold:size=8}\${alignc}▲ ADVANCED ▲\${font}"
else
echo "\${color5}\${font JetBrains Mono:bold:size=11}\${alignc}OPSEC STATUS\${font}"
echo "\${color2}\${font JetBrains Mono:size=8}\${alignc}── STANDARD ──\${font}"
fi
echo "\${color4}\${hr 1}"
# ─── SYSTEM ──────────────────────────────────────────────────────────────────
echo "\${color8}\${font JetBrains Mono:bold:size=9} ▌SYSTEM\${font}"
echo "\${color6} HOST \${color7}\${nodename}\${alignr}\${color6}UP \${color7}\${uptime_short}"
echo "\${color6} CPU \${color7}\${cpu}%\${alignr}\${color6}RAM \${color7}\${memperc}% \${color9}(\${mem}/\${memmax})"
# ─── NETWORK ─────────────────────────────────────────────────────────────────
echo "\${color4}\${hr 1}"
echo "\${color8}\${font JetBrains Mono:bold:size=9} ▌NETWORK\${font}"
LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
[ -z "$LOCAL_IP" ] && LOCAL_IP="No route"
echo "\${color6} LOCAL IP \${color7}${LOCAL_IP}"
# External IP from cache
_display_ip="${PUB_IP}"
[ -z "$_display_ip" ] && _display_ip="\${color0}UNAVAILABLE"
if [ -n "$PUB_GEO" ]; then
echo "\${color6} EXT IP \${color7}${_display_ip} \${color9}(${PUB_GEO})"
else
echo "\${color6} EXT IP \${color7}${_display_ip}"
fi
VPN_IF=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^(tun|wg)' | head -1)
if [ -n "$VPN_IF" ]; then
VPN_IP=$(ip -4 addr show "$VPN_IF" 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1)
echo "\${color6} VPN \${color1}ACTIVE \${color7}${VPN_IP} \${color9}(${VPN_IF})"
fi
# TOR/DNS/KSWCH status moved to TOR STATUS section below
PRIMARY_IF=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [ -n "$PRIMARY_IF" ]; then
CUR_MAC=$(ip link show "$PRIMARY_IF" 2>/dev/null | awk '/ether/ {print $2}')
FIRST_OCTET=$(echo "$CUR_MAC" | cut -d: -f1)
FIRST_DEC=$((16#${FIRST_OCTET}))
if (( FIRST_DEC & 2 )); then
echo "\${color6} MAC \${color1}RANDOM \${color9}(${CUR_MAC})"
else
echo "\${color6} MAC \${color0}HWADDR \${color9}(${CUR_MAC})"
fi
else
echo "\${color6} MAC \${color9}NO IFACE"
fi
# ─── ADVANCED MODE EXTRAS ────────────────────────────────────────────────────
if $ADVANCED; then
echo "\${color4}\${hr 1}"
echo "\${color8}\${font JetBrains Mono:bold:size=9} ▌HARDENING\${font}"
IPV6_ALL=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0")
[ "$IPV6_ALL" = "1" ] && echo "\${color6} IPv6 \${color1}BLOCKED" || echo "\${color6} IPv6 \${color0}LEAKING"
if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then
echo "\${color6} DNSLK \${color1}LOCKED \${color9}(immutable)"
else
echo "\${color6} DNSLK \${color0}UNLOCKED"
fi
ISO=$(grep "IsolateDestAddr" /etc/tor/torrc 2>/dev/null | head -1)
[ -n "$ISO" ] && echo "\${color6} ISOL \${color1}ACTIVE \${color9}(stream isolation)"
PAD=$(grep "ConnectionPadding" /etc/tor/torrc 2>/dev/null | awk '{print $2}')
[ "$PAD" = "1" ] && echo "\${color6} TPAD \${color1}ACTIVE \${color9}(traffic padding)"
BLACKLIST=$(grep "ExcludeExitNodes" /etc/tor/torrc 2>/dev/null | sed 's/ExcludeExitNodes //' | tr -d '{}' | tr ',' ' ')
[ -n "$BLACKLIST" ] && echo "\${color6} BLOCK \${color0}$(echo "$BLACKLIST" | tr ' ' ',' | sed 's/,$//')"
CORE_PAT=$(sysctl -n kernel.core_pattern 2>/dev/null)
[[ "$CORE_PAT" == *"/bin/false"* ]] && echo "\${color6} CORE \${color1}BLOCKED" || echo "\${color6} CORE \${color0}ENABLED"
SWAP_ACTIVE=$(swapon --show=SIZE --noheadings 2>/dev/null | head -1)
[ -z "$SWAP_ACTIVE" ] && echo "\${color6} SWAP \${color1}OFF" || echo "\${color6} SWAP \${color0}ACTIVE \${color9}(${SWAP_ACTIVE})"
BOOT_COUNT=0
for svc in opsec-boot-advanced opsec-mac-randomize opsec-hostname-randomize opsec-killswitch; do
systemctl is-enabled "$svc" 2>/dev/null | grep -q enabled && BOOT_COUNT=$((BOOT_COUNT + 1))
done
if [ "$BOOT_COUNT" -eq 4 ]; then
echo "\${color6} BOOT \${color1}PERSIST \${color9}(${BOOT_COUNT}/4)"
elif [ "$BOOT_COUNT" -gt 0 ]; then
echo "\${color6} BOOT \${color2}PARTIAL \${color9}(${BOOT_COUNT}/4)"
else
echo "\${color6} BOOT \${color0}NONE \${color9}(${BOOT_COUNT}/4)"
fi
[ -f /etc/opsec/opsec.conf ] && {
PROFILE=$(grep "^PROFILE_NAME=" /etc/opsec/opsec.conf 2>/dev/null | cut -d'"' -f2)
[ -n "$PROFILE" ] && echo "\${color6} PROF \${color2}${PROFILE}"
}
fi
# ─── TOR STATUS (replaces Route Chain) ──────────────────────────────────────
if $ADVANCED; then
echo "\${color4}\${hr 1}"
echo "\${color8}\${font JetBrains Mono:bold:size=9} ▌TOR STATUS\${font}"
# Determine protection state
_tor_svc=false; _tor_socks=false; _ks_armed=false; _tor_routed=false
systemctl is-active tor >/dev/null 2>&1 && _tor_svc=true
ss -tln 2>/dev/null | grep -q ':9050 ' && _tor_socks=true
# iptables -L requires root; use state file instead (kill switch is always armed when advanced is on)
$ADVANCED && _ks_armed=true
[ "$ROUTED_TOR" = "true" ] && _tor_routed=true
if $_tor_svc && $_tor_socks && $_ks_armed && $_tor_routed; then
echo "\${color6} STATE \${color1}PROTECTED"
elif $_tor_svc && ! $_tor_socks; then
# Bootstrapping
echo "\${color6} STATE \${color2}BOOTSTRAP \${color9}(${TOR_BOOTSTRAP:-0}% ${TOR_PHASE:-connecting})"
elif ! $_tor_svc; then
echo "\${color6} STATE \${color0}EXPOSED \${color9}(tor down)"
elif ! $_ks_armed; then
echo "\${color6} STATE \${color0}EXPOSED \${color9}(kill switch off)"
else
echo "\${color6} STATE \${color0}EXPOSED \${color9}(not routed)"
fi
# Exit country code only — no IP
if [ -n "$EXIT_COUNTRY" ] && [ ${#EXIT_COUNTRY} -le 3 ]; then
echo "\${color6} EXIT \${color2}${EXIT_COUNTRY}"
elif $_tor_socks; then
echo "\${color6} EXIT \${color9}resolving..."
else
echo "\${color6} EXIT \${color9}—"
fi
# Circuit age — time since exit IP last changed
if [ -n "$EXIT_CHANGE_TIME" ] && [ "$EXIT_CHANGE_TIME" -gt 0 ] 2>/dev/null; then
_now=$(date +%s)
_age=$(( _now - EXIT_CHANGE_TIME ))
if [ "$_age" -lt 60 ]; then
echo "\${color6} CIRCUIT \${color2}${_age}s ago"
elif [ "$_age" -lt 3600 ]; then
echo "\${color6} CIRCUIT \${color2}$(( _age / 60 ))m ago"
else
echo "\${color6} CIRCUIT \${color2}$(( _age / 3600 ))h $(( (_age % 3600) / 60 ))m ago"
fi
else
echo "\${color6} CIRCUIT \${color9}—"
fi
# DNS leak check — local checks only
_dns_server=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf 2>/dev/null)
_dns_immutable=false
lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i' && _dns_immutable=true
_nm_locked=false
[ -f /etc/NetworkManager/conf.d/opsec-dns-lock.conf ] && _nm_locked=true
if [ "$_dns_server" = "127.0.0.1" ] && $_dns_immutable && $_nm_locked; then
echo "\${color6} DNS \${color1}SECURE \${color9}(tor + locked)"
elif [ "$_dns_server" = "127.0.0.1" ] && $_dns_immutable; then
echo "\${color6} DNS \${color2}SECURE \${color9}(tor, NM unlocked)"
elif [ "$_dns_server" = "127.0.0.1" ]; then
echo "\${color6} DNS \${color2}PARTIAL \${color9}(tor, not locked)"
elif echo "$_dns_server" | grep -qE '^(9\.9\.9\.9|149\.112\.112\.112|1\.1\.1\.1|1\.0\.0\.1)$'; then
echo "\${color6} DNS \${color2}PRIVACY \${color9}(${_dns_server})"
else
echo "\${color6} DNS \${color0}LEAKED \${color9}(${_dns_server})"
fi
fi
# ─── MODE / LEVEL ───────────────────────────────────────────────────────────
echo "\${color4}\${hr 1}"
LEVEL=""
[ -f /etc/opsec/opsec.conf ] && LEVEL=$(grep "^DEPLOYMENT_LEVEL=" /etc/opsec/opsec.conf 2>/dev/null | cut -d'"' -f2)
if $ADVANCED; then
echo "\${color6} MODE \${color1}ADVANCED"
else
echo "\${color6} MODE \${color0}STANDARD"
fi
[ -n "$LEVEL" ] && echo "\${color6} LEVEL \${color7}${LEVEL}"
echo "\${color4}\${hr 1}"
echo "\${color9}\${font JetBrains Mono:size=7}\${alignc}// updated \${time %H:%M:%S} //\${font}"
+71
View File
@@ -0,0 +1,71 @@
-- OPSEC Status Widget
-- Colors managed by theme system via opsec-config.sh (--theme apply NAME)
-- Uses execpi to parse Conky color markup from helper script
conky.config = {
-- Window settings
-- Default: upper-right on primary display
alignment = 'top_right',
gap_x = 15,
gap_y = 60,
minimum_width = 400,
minimum_height = 200,
maximum_width = 420,
-- Multi-monitor: pin to primary display (head 0)
-- Set xinerama_head to a different number to move to another monitor
xinerama_head = 0,
-- Window type
own_window = true,
own_window_type = 'desktop',
own_window_transparent = false,
own_window_argb_visual = true,
own_window_argb_value = 210,
own_window_colour = '0d1117',
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
-- Drawing
double_buffer = true,
draw_shades = true,
default_shade_color = '000000',
draw_outline = false,
draw_borders = true,
border_inner_margin = 12,
border_outer_margin = 4,
border_width = 1,
border_colour = '1b3a5c',
stippled_borders = 0,
-- Font
use_xft = true,
font = 'JetBrains Mono:size=10',
override_utf8_locale = true,
-- Colors — OPSEC Status Widget
default_color = 'b0b0b0',
color0 = 'df2020', -- ares red (NOT SECURE)
color1 = '33ff33', -- terminal green (SECURE)
color2 = '3a8fd6', -- steel blue (info/neutral)
color3 = '0d1117', -- void (dividers)
color4 = '1f6feb', -- blue (structural lines)
color5 = '58a6ff', -- bright blue (title/accent)
color6 = '79c0ff', -- light blue (labels)
color7 = 'c9d1d9', -- silver (values)
color8 = '1f6feb', -- blue (section headers)
color9 = '484f58', -- dark grey (metadata)
-- Update interval
update_interval = 3,
total_run_times = 0,
-- Misc
cpu_avg_samples = 2,
no_buffers = true,
text_buffer_size = 8192,
short_units = true,
};
conky.text = [[
${execpi 5 ~/.config/conky/conky-opsec-status.sh}
]];
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# OPSEC Widget Launcher — positions and launches the Conky widget
# Usage: opsec-widget-launch.sh [position]
#
# Positions:
# tl = top-left tc = top-center tr = top-right
# bl = bottom-left bc = bottom-center br = bottom-right
#
# Default: tr (top-right)
# Uses the theme system — regenerates config from /etc/opsec/themes/
CONKY_DIR="$HOME/.config/conky"
CONF="$CONKY_DIR/conky-opsec-widget.conf"
POS="${1:-tr}"
# Map shorthand to conky alignment + gaps
case "$POS" in
tl) ALIGN="top_left"; GAP_X=15; GAP_Y=15 ;;
tc) ALIGN="top_middle"; GAP_X=0; GAP_Y=15 ;;
tr) ALIGN="top_right"; GAP_X=15; GAP_Y=60 ;;
bl) ALIGN="bottom_left"; GAP_X=15; GAP_Y=60 ;;
bc) ALIGN="bottom_middle"; GAP_X=0; GAP_Y=60 ;;
br) ALIGN="bottom_right"; GAP_X=15; GAP_Y=60 ;;
*)
echo "Usage: $(basename "$0") [tl|tc|tr|bl|bc|br]"
echo ""
echo " tl = top-left tc = top-center tr = top-right"
echo " bl = bottom-left bc = bottom-center br = bottom-right"
exit 1
;;
esac
# Kill existing widget and stale cache daemon
killall conky 2>/dev/null
pkill -f 'conky-opsec-cache\.sh' 2>/dev/null
rm -f /tmp/.opsec-cache/netinfo 2>/dev/null
sleep 0.3
# Load theme from opsec config
OPSEC_CONF="/etc/opsec/opsec.conf"
THEME="default"
[ -f "$OPSEC_CONF" ] && THEME=$(grep "^WIDGET_THEME=" "$OPSEC_CONF" 2>/dev/null | cut -d'"' -f2)
[ -z "$THEME" ] && THEME="default"
THEME_FILE="/etc/opsec/themes/${THEME}.theme"
if [ -f "$THEME_FILE" ]; then
. "$THEME_FILE"
else
echo "Theme '${THEME}' not found, using defaults"
THEME_LABEL="Default"
CONKY_BG="0d1117"
CONKY_COLOR0="df2020"; CONKY_COLOR1="33ff33"; CONKY_COLOR2="3a8fd6"
CONKY_COLOR3="0d1117"; CONKY_COLOR4="1f6feb"; CONKY_COLOR5="58a6ff"
CONKY_COLOR6="79c0ff"; CONKY_COLOR7="c9d1d9"; CONKY_COLOR8="1f6feb"
CONKY_COLOR9="484f58"
fi
# Generate config with theme colors and chosen position
mkdir -p "$CONKY_DIR"
cat > "$CONF" << EOF
-- OPSEC Status Widget — OPSEC Status Widget
-- Theme: ${THEME} (${THEME_LABEL:-Custom})
-- Position: ${ALIGN}
conky.config = {
alignment = '${ALIGN}',
gap_x = ${GAP_X},
gap_y = ${GAP_Y},
minimum_width = 400,
minimum_height = 200,
maximum_width = 420,
own_window = true,
own_window_type = 'normal',
own_window_transparent = false,
own_window_argb_visual = true,
own_window_argb_value = 210,
own_window_colour = '${CONKY_BG:-0d1117}',
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
xinerama_head = 0,
double_buffer = true,
draw_shades = true,
default_shade_color = '000000',
draw_outline = false,
draw_borders = true,
border_inner_margin = 12,
border_outer_margin = 4,
border_width = 1,
border_colour = '${CONKY_COLOR4:-1f6feb}',
stippled_borders = 0,
use_xft = true,
font = 'JetBrains Mono:size=10',
override_utf8_locale = true,
default_color = 'b0b0b0',
color0 = '${CONKY_COLOR0:-df2020}',
color1 = '${CONKY_COLOR1:-33ff33}',
color2 = '${CONKY_COLOR2:-3a8fd6}',
color3 = '${CONKY_COLOR3:-0d1117}',
color4 = '${CONKY_COLOR4:-1f6feb}',
color5 = '${CONKY_COLOR5:-58a6ff}',
color6 = '${CONKY_COLOR6:-79c0ff}',
color7 = '${CONKY_COLOR7:-c9d1d9}',
color8 = '${CONKY_COLOR8:-1f6feb}',
color9 = '${CONKY_COLOR9:-484f58}',
update_interval = 3,
total_run_times = 0,
cpu_avg_samples = 2,
no_buffers = true,
text_buffer_size = 8192,
short_units = true,
};
conky.text = [[
\${execpi 5 ~/.config/conky/conky-opsec-status.sh}
]];
EOF
# Launch
conky -c "$CONF" &
disown
echo "OPSEC widget launched: $ALIGN (theme: $THEME)"
+39
View File
@@ -0,0 +1,39 @@
# OPSEC Checklist
## Pre-Session Setup
- [ ] Fresh VM snapshot taken
- [ ] MAC address randomized: `randomize-mac`
- [ ] System timezone set appropriately
- [ ] DNS configured for privacy
- [ ] Verify no personal accounts logged in
- [ ] Disable WiFi/Bluetooth if not needed
- [ ] Configure VPN/proxy settings
- [ ] Test kill switches
## During Session
- [ ] Use workspace isolation (separate VM/container)
- [ ] All traffic through VPN/Tor
- [ ] Monitor connections: `opsec-monitor`
- [ ] Use encrypted communications
- [ ] No personal browsing/email
- [ ] Regular history clearing
- [ ] Avoid saving sensitive data locally
- [ ] Use in-memory tools when possible
## Post-Session
- [ ] Export necessary data
- [ ] Clear all logs: `clear-logs`
- [ ] Clear shell history: `clear-history`
- [ ] Wipe free space
- [ ] Secure delete files: `shred-file <files>`
- [ ] Revert to clean VM snapshot
## Emergency Procedures
- [ ] Kill network connections
- [ ] Emergency wipe if necessary
## Communication OPSEC
- [ ] Use encrypted channels
- [ ] Avoid real names/identifiers
- [ ] Use dedicated accounts
- [ ] Regular key rotation
+289
View File
@@ -0,0 +1,289 @@
#!/bin/bash
# opsec-toolkit installer — standalone, no Ansible required
# Usage: sudo ./install.sh [--uninstall]
#
# Installs the OPSEC privacy/security toolkit on Debian/Kali/Ubuntu systems.
# Requires: tor, macchanger, iptables, curl, jq (auto-installed if missing)
set -euo pipefail
# ─── COLORS ──────────────────────────────────────────────────────────────────
RED=$'\e[38;5;196m'
GRN=$'\e[38;5;49m'
YEL=$'\e[38;5;214m'
CYN=$'\e[38;5;45m'
RST=$'\e[0m'
ok() { echo "${GRN}[+]${RST} $*"; }
warn() { echo "${YEL}[*]${RST} $*"; }
err() { echo "${RED}[-]${RST} $*"; }
info() { echo "${CYN}[~]${RST} $*"; }
# ─── PREFLIGHT ───────────────────────────────────────────────────────────────
if [ "$EUID" -ne 0 ]; then
err "Please run as root: sudo ./install.sh"
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REAL_USER="${SUDO_USER:-$USER}"
REAL_HOME=$(eval echo "~${REAL_USER}")
# ─── UNINSTALL ───────────────────────────────────────────────────────────────
if [ "${1:-}" = "--uninstall" ]; then
echo ""
warn "Uninstalling OPSEC toolkit..."
echo ""
# Stop services
for svc in opsec-boot-advanced opsec-killswitch opsec-hostname-randomize opsec-mac-randomize; do
systemctl stop "$svc" 2>/dev/null || true
systemctl disable "$svc" 2>/dev/null || true
done
# Remove scripts
rm -f /usr/local/bin/opsec-*.sh
# Remove library
rm -rf /usr/local/lib/opsec
# Remove configs (preserve opsec.conf as backup)
if [ -d /etc/opsec ]; then
cp /etc/opsec/opsec.conf "/tmp/opsec.conf.backup.$(date +%s)" 2>/dev/null || true
rm -rf /etc/opsec
fi
# Remove systemd services
rm -f /etc/systemd/system/opsec-*.service
systemctl daemon-reload
# Remove cron jobs
rm -f /etc/cron.d/opsec-*
# Remove polkit, desktop, udev
rm -f /usr/share/polkit-1/actions/com.opsec.mode.policy
rm -f /usr/share/applications/opsec-toggle.desktop
rm -f /etc/udev/rules.d/99-opsec-usb.rules
ok "OPSEC toolkit uninstalled"
info "Config backup saved to /tmp/opsec.conf.backup.*"
exit 0
fi
# ─── BANNER ──────────────────────────────────────────────────────────────────
echo ""
echo "${CYN}╔══════════════════════════════════════════╗${RST}"
echo "${CYN}${RST} ${RED}OPSEC TOOLKIT${RST} — Installer v1.0 ${CYN}${RST}"
echo "${CYN}${RST} Privacy & Security Hardening ${CYN}${RST}"
echo "${CYN}╚══════════════════════════════════════════╝${RST}"
echo ""
# ─── DEPENDENCY CHECK ────────────────────────────────────────────────────────
info "Checking dependencies..."
DEPS=(tor macchanger iptables ip6tables curl jq iproute2 net-tools procps)
MISSING=()
for dep in "${DEPS[@]}"; do
case "$dep" in
tor) command -v tor >/dev/null 2>&1 || MISSING+=("tor") ;;
macchanger) command -v macchanger >/dev/null 2>&1 || MISSING+=("macchanger") ;;
iptables) command -v iptables >/dev/null 2>&1 || MISSING+=("iptables") ;;
ip6tables) command -v ip6tables >/dev/null 2>&1 || MISSING+=("iptables") ;;
curl) command -v curl >/dev/null 2>&1 || MISSING+=("curl") ;;
jq) command -v jq >/dev/null 2>&1 || MISSING+=("jq") ;;
iproute2) command -v ip >/dev/null 2>&1 || MISSING+=("iproute2") ;;
net-tools) command -v netstat >/dev/null 2>&1 || MISSING+=("net-tools") ;;
procps) command -v ps >/dev/null 2>&1 || MISSING+=("procps") ;;
esac
done
# Deduplicate
MISSING=($(printf '%s\n' "${MISSING[@]}" | sort -u))
if [ ${#MISSING[@]} -gt 0 ]; then
warn "Installing missing packages: ${MISSING[*]}"
apt-get update -qq
apt-get install -y -qq "${MISSING[@]}"
ok "Dependencies installed"
else
ok "All dependencies present"
fi
# Optional: conky for desktop widget
if ! command -v conky >/dev/null 2>&1; then
warn "Conky not installed — desktop widget will not be available"
warn "Install later with: apt install conky-all"
fi
# ─── INSTALL LIBRARY ─────────────────────────────────────────────────────────
info "Installing OPSEC library..."
mkdir -p /usr/local/lib/opsec
cp "$SCRIPT_DIR/lib/opsec-lib.sh" /usr/local/lib/opsec/
chmod 644 /usr/local/lib/opsec/opsec-lib.sh
ok "Library installed → /usr/local/lib/opsec/"
# ─── INSTALL SCRIPTS ─────────────────────────────────────────────────────────
info "Installing OPSEC scripts..."
for script in "$SCRIPT_DIR"/scripts/opsec-*.sh; do
[ -f "$script" ] || continue
cp "$script" /usr/local/bin/
chmod 755 "/usr/local/bin/$(basename "$script")"
done
ok "Scripts installed → /usr/local/bin/"
# ─── INSTALL CONFIGS ─────────────────────────────────────────────────────────
info "Installing configuration..."
# Main config directory
mkdir -p /etc/opsec/{themes,levels,vpn-templates,.harden-backup}
# Main config (don't overwrite existing)
if [ -f /etc/opsec/opsec.conf ]; then
warn "Existing opsec.conf found — preserving (new config saved as opsec.conf.new)"
cp "$SCRIPT_DIR/configs/opsec.conf" /etc/opsec/opsec.conf.new
else
cp "$SCRIPT_DIR/configs/opsec.conf" /etc/opsec/opsec.conf
fi
# Country codes
cp "$SCRIPT_DIR/configs/opsec-country-codes.conf" /etc/opsec/country-codes.conf
# Themes
cp "$SCRIPT_DIR/configs/themes/"*.theme /etc/opsec/themes/
# Levels
for f in "$SCRIPT_DIR/configs/levels/"*.conf; do
[ -f "$f" ] || continue
# Map filenames: bare-metal-standard.conf → bare-metal.conf etc.
cp "$f" /etc/opsec/levels/
done
# VPN templates
cp "$SCRIPT_DIR/configs/vpn-templates/"*.template /etc/opsec/vpn-templates/
# Torrc templates
mkdir -p /etc/opsec/torrc
cp "$SCRIPT_DIR/configs/torrc/torrc-default" /etc/opsec/torrc/
cp "$SCRIPT_DIR/configs/torrc/torrc-opsec" /etc/opsec/torrc/
# DNS configs
[ -f "$SCRIPT_DIR/configs/resolv.conf.opsec" ] && cp "$SCRIPT_DIR/configs/resolv.conf.opsec" /etc/opsec/
[ -f "$SCRIPT_DIR/configs/resolv.conf.head" ] && cp "$SCRIPT_DIR/configs/resolv.conf.head" /etc/opsec/
ok "Configuration installed → /etc/opsec/"
# ─── INSTALL SYSTEMD SERVICES ────────────────────────────────────────────────
info "Installing systemd services..."
for svc in "$SCRIPT_DIR/configs/systemd/"*.service; do
[ -f "$svc" ] || continue
cp "$svc" /etc/systemd/system/
done
systemctl daemon-reload
# Enable MAC randomize and hostname randomize at boot
systemctl enable opsec-mac-randomize.service 2>/dev/null || true
systemctl enable opsec-hostname-randomize.service 2>/dev/null || true
ok "Systemd services installed and enabled"
# ─── INSTALL CRON JOBS ──────────────────────────────────────────────────────
info "Installing cron jobs..."
cp "$SCRIPT_DIR/configs/cron/opsec-banner-cache" /etc/cron.d/
cp "$SCRIPT_DIR/configs/cron/opsec-log-rotate" /etc/cron.d/
chmod 644 /etc/cron.d/opsec-*
ok "Cron jobs installed → /etc/cron.d/"
# ─── INSTALL POLKIT POLICY ──────────────────────────────────────────────────
info "Installing polkit policy..."
mkdir -p /usr/share/polkit-1/actions
cp "$SCRIPT_DIR/configs/polkit/com.opsec.mode.policy" /usr/share/polkit-1/actions/
ok "Polkit policy installed"
# ─── INSTALL DESKTOP ENTRY ──────────────────────────────────────────────────
info "Installing desktop entry..."
cp "$SCRIPT_DIR/configs/desktop/opsec-toggle.desktop" /usr/share/applications/
ok "Desktop entry installed"
# ─── INSTALL UDEV RULES ─────────────────────────────────────────────────────
info "Installing udev rules..."
cp "$SCRIPT_DIR/configs/udev/99-opsec-usb.rules" /etc/udev/rules.d/
udevadm control --reload-rules 2>/dev/null || true
ok "Udev rules installed"
# ─── INSTALL CONKY WIDGET (user-space) ──────────────────────────────────────
info "Installing Conky widget files..."
CONKY_DIR="${REAL_HOME}/.config/conky"
mkdir -p "$CONKY_DIR"
for f in "$SCRIPT_DIR"/conky/*; do
[ -f "$f" ] || continue
cp "$f" "$CONKY_DIR/"
chown "${REAL_USER}:${REAL_USER}" "$CONKY_DIR/$(basename "$f")"
done
chmod +x "$CONKY_DIR"/*.sh 2>/dev/null || true
ok "Conky widget installed → ${CONKY_DIR}/"
# ─── INSTALL SHELL ALIASES ──────────────────────────────────────────────────
info "Installing shell aliases..."
ALIAS_FILE="${REAL_HOME}/.opsec-aliases"
cp "$SCRIPT_DIR/configs/opsec-aliases" "$ALIAS_FILE"
chown "${REAL_USER}:${REAL_USER}" "$ALIAS_FILE"
# Add source line to .bashrc and .zshrc if not already present
for rc in "${REAL_HOME}/.bashrc" "${REAL_HOME}/.zshrc"; do
if [ -f "$rc" ]; then
if ! grep -q '.opsec-aliases' "$rc" 2>/dev/null; then
echo "" >> "$rc"
echo "# OPSEC toolkit aliases" >> "$rc"
echo "[ -f ~/.opsec-aliases ] && . ~/.opsec-aliases" >> "$rc"
chown "${REAL_USER}:${REAL_USER}" "$rc"
fi
fi
done
ok "Aliases installed → ${ALIAS_FILE}"
# ─── TOR CONFIGURATION ──────────────────────────────────────────────────────
info "Configuring Tor..."
# Ensure tor log directory exists
mkdir -p /var/log/tor /run/tor
chown debian-tor:debian-tor /var/log/tor /run/tor 2>/dev/null || true
# Stop tor if running (we'll configure, user starts via opsec-on)
systemctl stop tor 2>/dev/null || true
ok "Tor configured (start with: opsec-on)"
# ─── TMPFS FOR LOGS ─────────────────────────────────────────────────────────
info "Setting up tmpfs for OPSEC logs..."
if ! grep -q 'opsec-logs' /etc/fstab 2>/dev/null; then
echo "" >> /etc/fstab
echo "# OPSEC: volatile log storage (RAM-only, cleared on reboot)" >> /etc/fstab
echo "tmpfs /var/log/opsec tmpfs nosuid,nodev,noexec,mode=0700,size=50M 0 0 # opsec-logs" >> /etc/fstab
fi
mkdir -p /var/log/opsec
mount /var/log/opsec 2>/dev/null || mount -t tmpfs -o nosuid,nodev,noexec,mode=0700,size=50M tmpfs /var/log/opsec 2>/dev/null || true
ok "OPSEC logs on tmpfs (RAM-only)"
# ─── CACHE DIRECTORY ─────────────────────────────────────────────────────────
mkdir -p /tmp/.opsec-cache
chown "${REAL_USER}:${REAL_USER}" /tmp/.opsec-cache 2>/dev/null || true
# ─── POST-INSTALL ────────────────────────────────────────────────────────────
echo ""
echo "${CYN}══════════════════════════════════════════${RST}"
ok "OPSEC toolkit installed successfully!"
echo "${CYN}══════════════════════════════════════════${RST}"
echo ""
info "Quick start:"
echo " ${GRN}opsec-on${RST} — Activate ghost mode (Tor + kill switch + hardening)"
echo " ${GRN}opsec-off${RST} — Deactivate ghost mode"
echo " ${GRN}opsec-config${RST} — Interactive configuration TUI"
echo " ${GRN}opsec-show${RST} — Show current status"
echo " ${GRN}killswitch-on${RST} — Activate kill switch only"
echo " ${GRN}opsec-preflight${RST} — Pre-session readiness check"
echo ""
info "Reload your shell to activate aliases:"
echo " ${GRN}source ~/.bashrc${RST} or ${GRN}source ~/.zshrc${RST}"
echo ""
info "Uninstall with: ${YEL}sudo ./install.sh --uninstall${RST}"
echo ""
+717
View File
@@ -0,0 +1,717 @@
#!/bin/bash
# /usr/local/lib/opsec/opsec-lib.sh — Shared OPSEC function library
# Sourced by all OPSEC scripts for config management and common operations
OPSEC_CONF="/etc/opsec/opsec.conf"
OPSEC_COUNTRY_CODES="/etc/opsec/country-codes.conf"
OPSEC_PROFILES_DIR="/etc/opsec/profiles"
OPSEC_STATE_FILE="/var/run/opsec-advanced.enabled"
OPSEC_BOOT_MARKER="/etc/opsec/boot-advanced.enabled"
# ─── COLOR OUTPUT ──────────────────────────────────────────────────────────────
opsec_green() { echo -e "\033[38;5;49m[+]\033[0m \033[38;5;49m$*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-]\033[0m \033[38;5;196m$*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*]\033[0m \033[38;5;214m$*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~]\033[0m \033[38;5;75m$*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>]\033[0m \033[38;5;51m$*\033[0m"; }
opsec_mag() { echo -e "\033[38;5;201m[*]\033[0m \033[38;5;201m$*\033[0m"; }
opsec_dim() { echo -e "\033[38;5;244m $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
# ─── CONFIG MANAGEMENT ─────────────────────────────────────────────────────────
opsec_load_config() {
if [ -f "$OPSEC_CONF" ]; then
# shellcheck disable=SC1090
. "$OPSEC_CONF"
return 0
fi
return 1
}
opsec_save_config() {
# Re-serialize all known keys back to config file
# Preserves comments and structure
local tmp
tmp=$(mktemp)
cat > "$tmp" << 'HEADER'
# /etc/opsec/opsec.conf — Central OPSEC Configuration
# Shell-sourceable KEY="value" format. All scripts source this file.
# Edit via: sudo opsec-config.sh (interactive TUI)
HEADER
cat >> "$tmp" << EOF
# ─── ACTIVE PROFILE ────────────────────────────────────────────────────────────
PROFILE_NAME="${PROFILE_NAME:-default}"
# ─── TOR SETTINGS ──────────────────────────────────────────────────────────────
TOR_CIRCUIT_ROTATION="${TOR_CIRCUIT_ROTATION:-30}"
TOR_BLACKLIST="${TOR_BLACKLIST:-}"
TOR_STRICT_NODES="${TOR_STRICT_NODES:-1}"
TOR_ISOLATION="${TOR_ISOLATION:-1}"
TOR_PADDING="${TOR_PADDING:-1}"
TOR_SOCKS_PORT="${TOR_SOCKS_PORT:-9050}"
TOR_DNS_PORT="${TOR_DNS_PORT:-5353}"
TOR_TRANS_PORT="${TOR_TRANS_PORT:-9040}"
TOR_NUM_GUARDS="${TOR_NUM_GUARDS:-3}"
TOR_SAFE_LOGGING="${TOR_SAFE_LOGGING:-1}"
# ─── DNS SETTINGS ──────────────────────────────────────────────────────────────
DNS_MODE="${DNS_MODE:-tor}"
DNS_CUSTOM_SERVERS="${DNS_CUSTOM_SERVERS:-}"
# ─── KILL SWITCH ───────────────────────────────────────────────────────────────
KILLSWITCH_ALLOW_DHCP="${KILLSWITCH_ALLOW_DHCP:-1}"
KILLSWITCH_ALLOW_OPENVPN="${KILLSWITCH_ALLOW_OPENVPN:-1}"
KILLSWITCH_ALLOW_WIREGUARD="${KILLSWITCH_ALLOW_WIREGUARD:-1}"
KILLSWITCH_EXTRA_PORTS="${KILLSWITCH_EXTRA_PORTS:-}"
# ─── MAC ADDRESS ───────────────────────────────────────────────────────────────
MAC_INTERFACES="${MAC_INTERFACES:-auto}"
MAC_VENDOR_SPOOF="${MAC_VENDOR_SPOOF:-}"
# ─── HOSTNAME ──────────────────────────────────────────────────────────────────
HOSTNAME_PATTERN="${HOSTNAME_PATTERN:-desktop}"
HOSTNAME_CUSTOM_PREFIX="${HOSTNAME_CUSTOM_PREFIX:-}"
# ─── SYSTEM HARDENING ──────────────────────────────────────────────────────────
HARDEN_IPV6="${HARDEN_IPV6:-1}"
HARDEN_SWAP="${HARDEN_SWAP:-1}"
HARDEN_CORE_DUMPS="${HARDEN_CORE_DUMPS:-1}"
HARDEN_CLIPBOARD_CLEAR="${HARDEN_CLIPBOARD_CLEAR:-0}"
HARDEN_SCREEN_LOCK="${HARDEN_SCREEN_LOCK:-1}"
HARDEN_SCREEN_LOCK_TIMEOUT="${HARDEN_SCREEN_LOCK_TIMEOUT:-300}"
HARDEN_TIMEZONE_SPOOF="${HARDEN_TIMEZONE_SPOOF:-0}"
HARDEN_TIMEZONE_VALUE="${HARDEN_TIMEZONE_VALUE:-UTC}"
HARDEN_LOCALE_SPOOF="${HARDEN_LOCALE_SPOOF:-0}"
HARDEN_LOCALE_VALUE="${HARDEN_LOCALE_VALUE:-en_US.UTF-8}"
# ─── LEAK PREVENTION ──────────────────────────────────────────────────────────
LEAK_WEBRTC_BLOCK="${LEAK_WEBRTC_BLOCK:-1}"
LEAK_USB_BLOCK="${LEAK_USB_BLOCK:-0}"
# ─── MONITORING ────────────────────────────────────────────────────────────────
MONITOR_PROCESSES="${MONITOR_PROCESSES:-0}"
MONITOR_LOG_ROTATION="${MONITOR_LOG_ROTATION:-1}"
LOG_ROTATION_HOURS="${LOG_ROTATION_HOURS:-4}"
# ─── TRAFFIC SHAPING ──────────────────────────────────────────────────────────
TRAFFIC_JITTER_ENABLED="${TRAFFIC_JITTER_ENABLED:-0}"
TRAFFIC_JITTER_MS="${TRAFFIC_JITTER_MS:-50}"
# ─── LEVEL TYPE ───────────────────────────────────────────────────────────────
LEVEL_TYPE="${LEVEL_TYPE:-standard}"
# ─── BASE STATE ───────────────────────────────────────────────────────────────
BASE_DNS="${BASE_DNS:-quad9}"
BASE_MAC_RANDOMIZE="${BASE_MAC_RANDOMIZE:-1}"
BASE_IPV6_DISABLE="${BASE_IPV6_DISABLE:-1}"
# ─── TOR BRIDGES ──────────────────────────────────────────────────────────────
TOR_BRIDGE_MODE="${TOR_BRIDGE_MODE:-off}"
TOR_BRIDGE_RELAY="${TOR_BRIDGE_RELAY:-}"
# ─── SECURE DELETION ──────────────────────────────────────────────────────────
WIPE_METHOD="${WIPE_METHOD:-auto}"
# ─── DEPLOYMENT LEVEL ─────────────────────────────────────────────────────────
DEPLOYMENT_LEVEL="${DEPLOYMENT_LEVEL:-bare-metal-standard}"
# ─── TERMINAL BANNER ──────────────────────────────────────────────────────────
OPSEC_BANNER="${OPSEC_BANNER:-compact}"
# ─── WIDGET THEME ────────────────────────────────────────────────────────────
WIDGET_THEME="${WIDGET_THEME:-default}"
EOF
mv "$tmp" "$OPSEC_CONF"
chmod 600 "$OPSEC_CONF"
}
opsec_set_value() {
local key="$1" val="$2"
if [ -z "$key" ]; then return 1; fi
# Validate key: must be a valid shell variable name (letters, digits, underscore, starts with letter/underscore)
if ! echo "$key" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
echo "[!] opsec_set_value: invalid key name '${key}'" >&2
return 1
fi
# Sanitize value: strip characters that could break shell quoting
val=$(printf '%s' "$val" | tr -d '`$\\\"'"'" | tr -cd '[:print:]')
opsec_load_config
eval "${key}=\"${val}\""
opsec_save_config
}
opsec_get_value() {
local key="$1"
# Validate key: must be a valid shell variable name
if ! echo "$key" | grep -qE '^[A-Za-z_][A-Za-z0-9_]*$'; then
echo "[!] opsec_get_value: invalid key name '${key}'" >&2
return 1
fi
opsec_load_config
eval "echo \"\${${key}:-}\""
}
# ─── TORRC GENERATION ──────────────────────────────────────────────────────────
opsec_generate_torrc() {
local _dbg="/run/opsec/debug.log"
echo "[$(date -Is)] [lib] opsec_generate_torrc called" >> "$_dbg" 2>/dev/null || true
opsec_load_config || return 1
local torrc="/etc/tor/torrc"
local socks_port="${TOR_SOCKS_PORT:-9050}"
local trans_port="${TOR_TRANS_PORT:-9040}"
local dns_port="${TOR_DNS_PORT:-5353}"
local rotation="${TOR_CIRCUIT_ROTATION:-30}"
local blacklist="${TOR_BLACKLIST:-}"
local strict="${TOR_STRICT_NODES:-1}"
local isolation="${TOR_ISOLATION:-1}"
local padding="${TOR_PADDING:-1}"
local guards="${TOR_NUM_GUARDS:-3}"
local safe_log="${TOR_SAFE_LOGGING:-1}"
echo "[$(date -Is)] [lib] socks=${socks_port} trans=${trans_port} dns=${dns_port} blacklist=${blacklist}" >> "$_dbg" 2>/dev/null || true
# Build SocksPort line with isolation flags
local socks_line="SocksPort ${socks_port}"
if [ "$isolation" = "1" ]; then
socks_line="${socks_line} IsolateDestAddr IsolateDestPort"
fi
# Build ExcludeExitNodes from blacklist
local exclude_line=""
if [ -n "$blacklist" ]; then
local formatted
formatted=$(echo "$blacklist" | sed 's/\([a-z][a-z]\)/{\1}/g; s/,/,/g')
exclude_line="ExcludeExitNodes ${formatted}"
fi
local bridge_mode="${TOR_BRIDGE_MODE:-off}"
local bridge_relay="${TOR_BRIDGE_RELAY:-}"
cat > "$torrc" << EOF
# Autogenerated — do not edit manually
# Regenerate via: opsec-config.sh --apply
# ─── SOCKS, TRANSPARENT PROXY & DNS ─────────────────────────────────────────
${socks_line}
TransPort ${trans_port}
DNSPort ${dns_port}
# ─── EXIT NODE EXCLUSION ────────────────────────────────────────────────────
${exclude_line}
StrictNodes ${strict}
# ─── CIRCUIT ROTATION ───────────────────────────────────────────────────────
MaxCircuitDirtiness ${rotation}
# ─── TRAFFIC PADDING ────────────────────────────────────────────────────────
ConnectionPadding ${padding}
# ─── SAFE LOGGING ────────────────────────────────────────────────────────────
SafeLogging ${safe_log}
Log notice file /run/tor/notices.log
# ─── ENTRY GUARDS ───────────────────────────────────────────────────────────
NumEntryGuards ${guards}
UseEntryGuards 1
EOF
# ─── PLUGGABLE TRANSPORTS (bridges) ──────────────────────────────────────
if [ "$bridge_mode" != "off" ] && [ "$bridge_mode" != "" ]; then
cat >> "$torrc" << 'BRIDGE_HEADER'
# ─── BRIDGE CONFIGURATION ────────────────────────────────────────────────────
UseBridges 1
BRIDGE_HEADER
case "$bridge_mode" in
obfs4)
echo "ClientTransportPlugin obfs4 exec /usr/bin/obfs4proxy" >> "$torrc"
;;
meek-azure)
echo "ClientTransportPlugin meek_lite exec /usr/bin/obfs4proxy" >> "$torrc"
;;
snowflake)
# snowflake-client location varies by distro
local sf_bin
sf_bin=$(command -v snowflake-client 2>/dev/null || echo "/usr/bin/snowflake-client")
echo "ClientTransportPlugin snowflake exec ${sf_bin}" >> "$torrc"
;;
esac
# Add user-specified bridge relay if provided
if [ -n "$bridge_relay" ]; then
echo "Bridge ${bridge_relay}" >> "$torrc"
else
# Default bridges for each transport type
case "$bridge_mode" in
meek-azure)
echo "Bridge meek_lite 192.0.2.18:80 BE776A53492E1E044A26F17306E1BC46A55A1625 url=https://meek.azureedge.net/ front=ajax.aspnetcdn.com" >> "$torrc"
;;
snowflake)
echo "Bridge snowflake 192.0.2.3:80 2B280B23E1107BB62ABFC40DDCC8824814F80A72 fingerprint=2B280B23E1107BB62ABFC40DDCC8824814F80A72 url=https://snowflake-broker.torproject.net.global.prod.fastly.net/ front=foursquare.com ice=stun:stun.l.google.com:19302,stun:stun.antisip.com:3478,stun:stun.bluesip.net:3478,stun:stun.dus.net:3478,stun:stun.epygi.com:3478,stun:stun.sonetel.com:3478,stun:stun.uls.co.za:3478,stun:stun.voipgate.com:3478,stun:stun.voys.nl:3478 utls-imitate=hellorandomizedalpn" >> "$torrc"
;;
esac
fi
fi
chmod 644 "$torrc"
echo "[$(date -Is)] [lib] torrc written ($(wc -l < "$torrc") lines)" >> "$_dbg" 2>/dev/null || true
echo "[$(date -Is)] [lib] torrc contents:" >> "$_dbg" 2>/dev/null || true
cat "$torrc" >> "$_dbg" 2>/dev/null || true
echo "[$(date -Is)] [lib] --- end torrc ---" >> "$_dbg" 2>/dev/null || true
}
# ─── RESOLV.CONF GENERATION ───────────────────────────────────────────────────
opsec_generate_resolv() {
local _dbg="/run/opsec/debug.log"
echo "[$(date -Is)] [lib] opsec_generate_resolv called" >> "$_dbg" 2>/dev/null || true
opsec_load_config || return 1
local mode="${DNS_MODE:-tor}"
local resolv="/etc/resolv.conf"
echo "[$(date -Is)] [lib] DNS_MODE=${mode}" >> "$_dbg" 2>/dev/null || true
# Unlock if immutable
chattr -i "$resolv" 2>/dev/null || true
case "$mode" in
tor)
echo "nameserver 127.0.0.1" > "$resolv"
;;
quad9)
cat > "$resolv" << 'EOF'
nameserver 9.9.9.9
nameserver 149.112.112.112
EOF
;;
cloudflare)
cat > "$resolv" << 'EOF'
nameserver 1.1.1.1
nameserver 1.0.0.1
EOF
;;
doh)
# DNS-over-HTTPS via dnscrypt-proxy
# Ensure dnscrypt-proxy is running
if command -v dnscrypt-proxy >/dev/null 2>&1; then
# Deploy config if not present
if [ ! -f /etc/dnscrypt-proxy/dnscrypt-proxy.toml ] && [ -f /etc/opsec/dnscrypt-proxy.toml ]; then
mkdir -p /etc/dnscrypt-proxy
cp /etc/opsec/dnscrypt-proxy.toml /etc/dnscrypt-proxy/dnscrypt-proxy.toml
fi
mkdir -p /var/log/dnscrypt-proxy /var/cache/dnscrypt-proxy
# Stop systemd-resolved if it conflicts on :53
systemctl stop systemd-resolved 2>/dev/null || true
systemctl start dnscrypt-proxy 2>/dev/null || dnscrypt-proxy -config /etc/dnscrypt-proxy/dnscrypt-proxy.toml &
fi
cat > "$resolv" << 'EOF'
nameserver 127.0.0.53
EOF
;;
dot)
# DNS-over-TLS via systemd-resolved
cat > "$resolv" << 'EOF'
nameserver 127.0.0.53
options edns0 trust-ad
EOF
;;
custom)
if [ -n "$DNS_CUSTOM_SERVERS" ]; then
: > "$resolv"
local IFS=','
for server in $DNS_CUSTOM_SERVERS; do
echo "nameserver ${server}" >> "$resolv"
done
else
echo "nameserver 9.9.9.9" > "$resolv"
fi
;;
*)
echo "nameserver 9.9.9.9" > "$resolv"
;;
esac
# Lock if in advanced mode
if opsec_is_advanced; then
chattr +i "$resolv"
echo "[$(date -Is)] [lib] resolv.conf locked (chattr +i)" >> "$_dbg" 2>/dev/null || true
fi
echo "[$(date -Is)] [lib] resolv.conf contents: $(cat "$resolv" 2>/dev/null | tr '\n' ' ')" >> "$_dbg" 2>/dev/null || true
}
# ─── STATE CHECKS ─────────────────────────────────────────────────────────────
opsec_is_advanced() {
[ -f "$OPSEC_STATE_FILE" ]
}
opsec_is_boot_enabled() {
[ -f "$OPSEC_BOOT_MARKER" ]
}
# ─── PROFILE MANAGEMENT ───────────────────────────────────────────────────────
opsec_profile_save() {
local name="$1"
if [ -z "$name" ]; then return 1; fi
mkdir -p "$OPSEC_PROFILES_DIR"
cp "$OPSEC_CONF" "${OPSEC_PROFILES_DIR}/${name}.conf"
# Tag profile name inside the saved copy
sed -i "s/^PROFILE_NAME=.*/PROFILE_NAME=\"${name}\"/" "${OPSEC_PROFILES_DIR}/${name}.conf"
}
opsec_profile_load() {
local name="$1"
local profile="${OPSEC_PROFILES_DIR}/${name}.conf"
if [ ! -f "$profile" ]; then return 1; fi
cp "$profile" "$OPSEC_CONF"
# Update active profile name
sed -i "s/^PROFILE_NAME=.*/PROFILE_NAME=\"${name}\"/" "$OPSEC_CONF"
}
opsec_profile_list() {
if [ -d "$OPSEC_PROFILES_DIR" ]; then
find "$OPSEC_PROFILES_DIR" -name '*.conf' -printf '%f\n' | sed 's/\.conf$//'
fi
}
opsec_profile_delete() {
local name="$1"
rm -f "${OPSEC_PROFILES_DIR}/${name}.conf"
}
opsec_profile_export() {
local name="$1" dest="$2"
local profile="${OPSEC_PROFILES_DIR}/${name}.conf"
if [ ! -f "$profile" ]; then return 1; fi
cp "$profile" "$dest"
}
opsec_profile_import() {
local src="$1" name="$2"
if [ ! -f "$src" ]; then return 1; fi
mkdir -p "$OPSEC_PROFILES_DIR"
cp "$src" "${OPSEC_PROFILES_DIR}/${name}.conf"
}
# ─── COUNTRY CODE HELPERS ─────────────────────────────────────────────────────
opsec_load_country_presets() {
if [ -f "$OPSEC_COUNTRY_CODES" ]; then
# shellcheck disable=SC1090
. "$OPSEC_COUNTRY_CODES"
fi
}
opsec_get_preset() {
local preset="$1"
opsec_load_country_presets
case "$preset" in
5eyes|fiveeyes) echo "$FIVE_EYES" ;;
9eyes|nineeyes) echo "$NINE_EYES" ;;
14eyes|fourteeneyes) echo "$FOURTEEN_EYES" ;;
surveillance) echo "$SURVEILLANCE_STATES" ;;
max) echo "$MAX_EXCLUSION" ;;
*) echo "" ;;
esac
}
# ─── DEPLOYMENT LEVEL MANAGEMENT ─────────────────────────────────────────────
OPSEC_LEVELS_DIR="/etc/opsec/levels"
opsec_level_list() {
if [ -d "$OPSEC_LEVELS_DIR" ]; then
find "$OPSEC_LEVELS_DIR" -name '*.conf' -printf '%f\n' | sed 's/\.conf$//' | sort
fi
}
opsec_level_apply() {
local level="$1"
local level_file="${OPSEC_LEVELS_DIR}/${level}.conf"
if [ ! -f "$level_file" ]; then
opsec_red "Level '${level}' not found in ${OPSEC_LEVELS_DIR}"
return 1
fi
# Preserve current PROFILE_NAME before overwriting
opsec_load_config 2>/dev/null || true
local saved_profile="${PROFILE_NAME:-default}"
# Copy level preset over active config
cp "$level_file" "$OPSEC_CONF"
chmod 600 "$OPSEC_CONF"
# Restore profile name and ensure deployment level is tagged
opsec_load_config
PROFILE_NAME="$saved_profile"
DEPLOYMENT_LEVEL="$level"
opsec_save_config
}
# ─── SECURE DELETION ─────────────────────────────────────────────────────────
opsec_detect_storage_type() {
# Detect storage type for a given path
# Returns: hdd | ssd | luks | unknown
local target_path="${1:-/}"
local device
# Find the block device for the path
device=$(df -P "$target_path" 2>/dev/null | tail -1 | awk '{print $1}')
[ -z "$device" ] && echo "unknown" && return
# Check for LUKS
if command -v cryptsetup >/dev/null 2>&1; then
# Check if device is on a dm-crypt layer
local dm_name
dm_name=$(basename "$device" 2>/dev/null)
if [ -e "/sys/block/${dm_name}/dm/uuid" ] 2>/dev/null; then
local dm_uuid
dm_uuid=$(cat "/sys/block/${dm_name}/dm/uuid" 2>/dev/null)
if echo "$dm_uuid" | grep -qi "CRYPT-LUKS"; then
echo "luks"
return
fi
fi
# Also check via dmsetup
if dmsetup info "$device" 2>/dev/null | grep -qi "CRYPT-LUKS"; then
echo "luks"
return
fi
fi
# Resolve to physical disk (strip partition number, handle /dev/mapper)
local phys_disk
phys_disk=$(lsblk -ndo PKNAME "$device" 2>/dev/null | head -1)
[ -z "$phys_disk" ] && phys_disk=$(echo "$device" | sed 's/[0-9]*$//' | sed 's|^/dev/||')
# Check rotational flag (0 = SSD, 1 = HDD)
local rotational_file="/sys/block/${phys_disk}/queue/rotational"
if [ -f "$rotational_file" ]; then
local rotational
rotational=$(cat "$rotational_file" 2>/dev/null)
if [ "$rotational" = "0" ]; then
echo "ssd"
else
echo "hdd"
fi
return
fi
echo "unknown"
}
opsec_secure_delete() {
# SSD-aware secure file deletion
# Usage: opsec_secure_delete <file_or_dir> [method]
# method: auto (default) | shred | fstrim | luks
local target="$1"
local method="${2:-${WIPE_METHOD:-auto}}"
[ -z "$target" ] && return 1
if [ "$method" = "auto" ]; then
method=$(opsec_detect_storage_type "$target")
fi
case "$method" in
hdd|shred)
# Traditional shred for spinning disks
if [ -d "$target" ]; then
find "$target" -type f -exec shred -fuz -n 1 {} \; 2>/dev/null
rm -rf "$target" 2>/dev/null
elif [ -f "$target" ]; then
shred -fuz -n 1 "$target" 2>/dev/null
fi
;;
ssd|fstrim)
# For SSDs: overwrite with zeros, delete, then fstrim
# shred is ineffective on SSDs due to wear leveling
if [ -d "$target" ]; then
find "$target" -type f -exec dd if=/dev/zero of={} bs=4k count=1 conv=notrunc 2>/dev/null \;
find "$target" -type f -delete 2>/dev/null
rm -rf "$target" 2>/dev/null
elif [ -f "$target" ]; then
dd if=/dev/zero of="$target" bs=4k count=1 conv=notrunc 2>/dev/null
rm -f "$target" 2>/dev/null
fi
# Request TRIM/discard on the filesystem
local mount_point
mount_point=$(df -P "${target%/*}" 2>/dev/null | tail -1 | awk '{print $6}')
if [ -n "$mount_point" ]; then
fstrim "$mount_point" 2>/dev/null || true
fi
;;
luks)
# For LUKS: zero file + rely on LUKS key destroy for full wipe
if [ -d "$target" ]; then
find "$target" -type f -exec dd if=/dev/zero of={} bs=4k count=1 conv=notrunc 2>/dev/null \;
find "$target" -type f -delete 2>/dev/null
rm -rf "$target" 2>/dev/null
elif [ -f "$target" ]; then
dd if=/dev/zero of="$target" bs=4k count=1 conv=notrunc 2>/dev/null
rm -f "$target" 2>/dev/null
fi
# Note: full LUKS key destroy handled by emergency-wipe
;;
*)
# Fallback: basic shred
if [ -d "$target" ]; then
find "$target" -type f -exec shred -fuz -n 1 {} \; 2>/dev/null
rm -rf "$target" 2>/dev/null
elif [ -f "$target" ]; then
shred -fuz -n 1 "$target" 2>/dev/null
fi
;;
esac
}
# ─── WIDGET THEME MANAGEMENT ─────────────────────────────────────────────────
OPSEC_THEMES_DIR="/etc/opsec/themes"
opsec_theme_list() {
if [ -d "$OPSEC_THEMES_DIR" ]; then
find "$OPSEC_THEMES_DIR" -name '*.theme' -printf '%f\n' | sed 's/\.theme$//' | sort
fi
}
opsec_generate_conky() {
opsec_load_config || return 1
local theme="${WIDGET_THEME:-default}"
local theme_file="${OPSEC_THEMES_DIR}/${theme}.theme"
if [ ! -f "$theme_file" ]; then
opsec_red "Theme '${theme}' not found at ${theme_file}"
return 1
fi
# Source theme to get CONKY_COLOR* and CONKY_BG values
# shellcheck disable=SC1090
. "$theme_file"
local conky_conf_name="conky-opsec-widget.conf"
# Find and regenerate conky config for each user with the widget installed
for home_dir in /home/*; do
local conky_conf="${home_dir}/.config/conky/${conky_conf_name}"
[ -f "$conky_conf" ] || continue
local owner
owner=$(stat -c '%U' "$conky_conf" 2>/dev/null) || continue
cat > "$conky_conf" << CONKYEOF
-- OPSEC Status Widget
-- Colors managed by theme system via opsec-config.sh
-- Theme: ${theme} (${THEME_LABEL:-Custom})
conky.config = {
-- Window settings
alignment = 'top_right',
gap_x = 15,
gap_y = 60,
minimum_width = 400,
minimum_height = 200,
maximum_width = 420,
-- Multi-monitor: run xrandr --listmonitors to find head number
xinerama_head = 0,
-- Window type
own_window = true,
own_window_type = 'desktop',
own_window_transparent = false,
own_window_argb_visual = true,
own_window_argb_value = 210,
own_window_colour = '${CONKY_BG:-0d1117}',
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
-- Drawing
double_buffer = true,
draw_shades = true,
default_shade_color = '000000',
draw_outline = false,
draw_borders = true,
border_inner_margin = 12,
border_outer_margin = 4,
border_width = 1,
border_colour = '1b3a5c',
stippled_borders = 0,
-- Font
use_xft = true,
font = 'JetBrains Mono:size=10',
override_utf8_locale = true,
-- Colors — Theme: ${theme}
default_color = 'b0b0b0',
color0 = '${CONKY_COLOR0:-df2020}',
color1 = '${CONKY_COLOR1:-33ff33}',
color2 = '${CONKY_COLOR2:-3a8fd6}',
color3 = '${CONKY_COLOR3:-0d1117}',
color4 = '${CONKY_COLOR4:-1f6feb}',
color5 = '${CONKY_COLOR5:-58a6ff}',
color6 = '${CONKY_COLOR6:-79c0ff}',
color7 = '${CONKY_COLOR7:-c9d1d9}',
color8 = '${CONKY_COLOR8:-1f6feb}',
color9 = '${CONKY_COLOR9:-484f58}',
-- Update interval
update_interval = 3,
total_run_times = 0,
-- Misc
cpu_avg_samples = 2,
no_buffers = true,
text_buffer_size = 8192,
short_units = true,
};
conky.text = [[
\${execpi 5 ~/.config/conky/conky-opsec-status.sh}
]];
CONKYEOF
chown "$owner":"$owner" "$conky_conf"
chmod 644 "$conky_conf"
done
# Restart conky for all users running the opsec widget
pkill -f 'conky.*opsec' 2>/dev/null || true
sleep 1
for home_dir in /home/*; do
local conky_conf="${home_dir}/.config/conky/${conky_conf_name}"
[ -f "$conky_conf" ] || continue
local owner
owner=$(stat -c '%U' "$conky_conf" 2>/dev/null) || continue
local uid
uid=$(id -u "$owner" 2>/dev/null) || continue
# Relaunch conky as the file owner
su - "$owner" -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/${uid}/bus nohup conky -c '${conky_conf}' >/dev/null 2>&1 &" 2>/dev/null || true
done
}
# ─── INIT CHECK ────────────────────────────────────────────────────────────────
# Ensure /etc/opsec exists
_opsec_init_dirs() {
[ -d /etc/opsec ] || mkdir -p /etc/opsec
[ -d "$OPSEC_PROFILES_DIR" ] || mkdir -p "$OPSEC_PROFILES_DIR"
}
# Auto-init on source if running as root
if [ "$EUID" = "0" ] 2>/dev/null || [ "$(id -u)" = "0" ] 2>/dev/null; then
_opsec_init_dirs
fi
+35
View File
@@ -0,0 +1,35 @@
#!/bin/bash
# /usr/local/bin/opsec-banner-cache.sh — Background WAN IP Cache Updater
# Updates /var/cache/opsec/wan_ip every 5 minutes (via cron)
# Never blocks shell startup — the banner reads from cache only
CACHE_DIR="/var/cache/opsec"
CACHE_FILE="${CACHE_DIR}/wan_ip"
ENDPOINTS="https://icanhazip.com https://ifconfig.me https://api.ipify.org"
mkdir -p "$CACHE_DIR"
chmod 755 "$CACHE_DIR"
PUB_IP=""
# Try via Tor first if running
if pgrep -x tor >/dev/null 2>&1 && ss -tln 2>/dev/null | grep -q ':9050 '; then
for ep in $ENDPOINTS; do
PUB_IP=$(curl -s --max-time 10 --socks5-hostname 127.0.0.1:9050 "$ep" 2>/dev/null | tr -d '[:space:]')
[ -n "$PUB_IP" ] && break
done
fi
# Fallback to direct if Tor failed or not running
if [ -z "$PUB_IP" ]; then
for ep in $ENDPOINTS; do
PUB_IP=$(curl -s --max-time 5 "$ep" 2>/dev/null | tr -d '[:space:]')
[ -n "$PUB_IP" ] && break
done
fi
# Only write if we got a valid-looking IP
if echo "$PUB_IP" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
echo "$PUB_IP" > "$CACHE_FILE"
chmod 644 "$CACHE_FILE"
fi
+320
View File
@@ -0,0 +1,320 @@
#!/bin/bash
# /usr/local/bin/opsec-banner.sh — Terminal OPSEC Status Banner
# Mirrors the Conky desktop widget layout and Widget theme colors
# Modes: compact | full | off (configured via OPSEC_BANNER in opsec.conf)
OPSEC_CONF="/etc/opsec/opsec.conf"
[ -f "$OPSEC_CONF" ] && . "$OPSEC_CONF"
BANNER_MODE="${OPSEC_BANNER:-compact}"
[ -n "$1" ] && BANNER_MODE="$1"
[ "$BANNER_MODE" = "off" ] && exit 0
# ─── Widget theme colors (matching Conky widget) ────────────────────────────
R=$'\e[0m'
BLD=$'\e[1m'
C0=$'\e[38;5;135m' # ALERT/BAD — #B55AFC purple
C1=$'\e[38;5;204m' # SECURE/GOOD — #FF63BE hot pink
C2=$'\e[38;5;117m' # INFO/NEUTRAL — #85E7FF cyan
C4=$'\e[38;5;32m' # STRUCTURAL — #268BD2 blue
C5=$'\e[38;5;45m' # TITLE/ACCENT — #07CAF9 bright cyan
C6=$'\e[38;5;117m' # LABELS — #85E7FF cyan
C7=$'\e[38;5;189m' # VALUES — #ECDEF7 lavender
C8=$'\e[38;5;135m' # SECTION HDR — #B55AFC purple
C9=$'\e[38;5;66m' # METADATA — #4A6A7A dark grey
# ─── DETECT STATE ────────────────────────────────────────────────────────────
ADVANCED=false
[ -f /var/run/opsec-advanced.enabled ] && ADVANCED=true
BREAKGLASS=false
if [ -f /var/run/opsec-breakglass.active ]; then
BREAKGLASS=true
_bge=$(cat /var/run/opsec-breakglass.active 2>/dev/null)
_bgn=$(date +%s)
BG_REM=""
[ -n "$_bge" ] && [ "$_bge" -gt "$_bgn" ] 2>/dev/null && BG_REM="$(( (_bge - _bgn) / 60 ))m"
fi
# ─── HELPERS ─────────────────────────────────────────────────────────────────
HR=" ${C4}$(printf '─%.0s' {1..48})${R}"
# ─── READ CACHE ──────────────────────────────────────────────────────────────
CACHE_FILE="/tmp/.opsec-cache/netinfo"
PUB_IP="" PUB_GEO="" ROUTED_TOR=false EXIT_COUNTRY="" EXIT_CHANGE_TIME="" TOR_BOOTSTRAP="" TOR_PHASE=""
if [ -f "$CACHE_FILE" ]; then
_cr() { grep "^${1}=" "$CACHE_FILE" 2>/dev/null | head -1 | cut -d'"' -f2; }
PUB_IP=$(_cr PUB_IP)
PUB_GEO=$(_cr PUB_GEO)
ROUTED_TOR=$(_cr ROUTED_TOR)
EXIT_COUNTRY=$(_cr EXIT_COUNTRY)
EXIT_CHANGE_TIME=$(_cr EXIT_CHANGE_TIME)
TOR_BOOTSTRAP=$(_cr TOR_BOOTSTRAP)
TOR_PHASE=$(_cr TOR_PHASE)
PUB_IP=$(echo "$PUB_IP" | tr -cd '0-9.')
EXIT_COUNTRY=$(echo "$EXIT_COUNTRY" | tr -cd 'A-Za-z')
EXIT_CHANGE_TIME=$(echo "$EXIT_CHANGE_TIME" | tr -cd '0-9')
TOR_BOOTSTRAP=$(echo "$TOR_BOOTSTRAP" | tr -cd '0-9')
fi
# ─── GATHER LIVE STATUS ─────────────────────────────────────────────────────
LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src") print $(i+1)}' | head -1)
[ -z "$LOCAL_IP" ] && LOCAL_IP="No route"
VPN_IF=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^(tun|wg)' | head -1)
VPN_OK=false; VPN_IP=""
if [ -n "$VPN_IF" ]; then
VPN_OK=true
VPN_IP=$(ip -4 addr show "$VPN_IF" 2>/dev/null | awk '/inet /{print $2}' | cut -d/ -f1)
fi
PRIMARY_IF=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
MAC_OK=false; CUR_MAC=""
if [ -n "$PRIMARY_IF" ]; then
CUR_MAC=$(ip link show "$PRIMARY_IF" 2>/dev/null | awk '/ether/{print $2}')
_oct=$((16#$(echo "$CUR_MAC" | cut -d: -f1))) 2>/dev/null
(( _oct & 2 )) 2>/dev/null && MAC_OK=true
fi
# ═════════════════════════════════════════════════════════════════════════════
# COMPACT BANNER
# ═════════════════════════════════════════════════════════════════════════════
render_compact() {
_si() {
local ok=$1 lbl="$2"
if $ok; then
echo -n "${C6}${lbl}${C1}${BLD}+${R}"
else
echo -n "${C6}${lbl}${C0}${BLD}x${R}"
fi
}
local _tor_ok=false; ss -tln 2>/dev/null | grep -q ':9050 ' && _tor_ok=true
local _ks_ok=false
[ -f /var/run/opsec-killswitch.enabled ] && _ks_ok=true
iptables -L GP_FW >/dev/null 2>&1 && _ks_ok=true
local _dns_ok=false
local _ds=$(awk '/^nameserver/{print $2;exit}' /etc/resolv.conf 2>/dev/null)
case "$_ds" in 127.0.0.1|9.9.9.9|149.112.*|1.1.1.1|1.0.0.1) _dns_ok=true ;; esac
local _v6_ok=false
[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" = "1" ] && _v6_ok=true
echo ""
if $BREAKGLASS; then
echo " ${C0}${BLD}⚠ BREAKGLASS${R} ${C0}KS bypassed${BG_REM:+ (${BG_REM})}${R}"
fi
if $ADVANCED; then
local sigil="${C1}${BLD}▲ ADVANCED${R}"
local ind="$(_si $_tor_ok TOR) $(_si $_ks_ok KS) $(_si $_dns_ok DNS) $(_si $MAC_OK MAC) $(_si $_v6_ok v6)"
$VPN_OK && ind="$ind $(_si $VPN_OK VPN)"
echo " ${sigil} ${ind} ${C9}${LOCAL_IP}${R}"
else
local sigil="${C2}── STANDARD${R}"
local ind="$(_si $_dns_ok DNS) $(_si $MAC_OK MAC)"
$VPN_OK && ind="$ind $(_si $VPN_OK VPN)"
echo " ${sigil} ${ind} ${C9}${LOCAL_IP}${R}"
fi
echo ""
}
# ═════════════════════════════════════════════════════════════════════════════
# FULL BANNER — mirrors Conky widget
# ═════════════════════════════════════════════════════════════════════════════
render_full() {
echo ""
# ── HEADER ──
echo "$HR"
if $ADVANCED; then
echo " ${C5}${BLD} OPSEC STATUS${R}"
echo " ${C1}${BLD} ▲ ADVANCED ▲${R}"
else
echo " ${C5}${BLD} OPSEC STATUS${R}"
echo " ${C2} ── STANDARD ──${R}"
fi
echo "$HR"
# Breakglass warning
if $BREAKGLASS; then
echo " ${C0}${BLD} ⚠ BREAKGLASS — Kill switch bypassed ${BG_REM:+(${BG_REM})}${R}"
echo "$HR"
fi
# ── SYSTEM ──
echo " ${C8}${BLD} ▌SYSTEM${R}"
echo " ${C6} HOST ${C7}$(hostname)${R}$(printf '%*s' 1 '')${C6}UP ${C7}$(uptime -p 2>/dev/null | sed 's/^up //' | sed 's/ hours\?/h/;s/ minutes\?/m/;s/ days\?/d/;s/, */ /g' || uptime | awk -F'up ' '{print $2}' | awk -F, '{print $1}')${R}"
local _cpu _mem _memtot _memp
_cpu=$(awk '/^cpu /{u=$2+$4; t=$2+$4+$5; if(t>0) printf "%.0f", u*100/t}' /proc/stat 2>/dev/null)
_mem=$(free -h 2>/dev/null | awk '/^Mem:/{print $3}')
_memtot=$(free -h 2>/dev/null | awk '/^Mem:/{print $2}')
_memp=$(free 2>/dev/null | awk '/^Mem:/{if($2>0) printf "%.0f", $3*100/$2}')
echo " ${C6} CPU ${C7}${_cpu:-0}%${R}$(printf '%*s' 1 '')${C6}RAM ${C7}${_memp:-0}% ${C9}(${_mem:-?}/${_memtot:-?})${R}"
# ── NETWORK ──
echo "$HR"
echo " ${C8}${BLD} ▌NETWORK${R}"
echo " ${C6} LOCAL IP ${C7}${LOCAL_IP}${R}"
# External IP
if [ -n "$PUB_IP" ]; then
if [ -n "$PUB_GEO" ]; then
echo " ${C6} EXT IP ${C7}${PUB_IP} ${C9}(${PUB_GEO})${R}"
else
echo " ${C6} EXT IP ${C7}${PUB_IP}${R}"
fi
else
echo " ${C6} EXT IP ${C0}UNAVAILABLE${R}"
fi
# VPN
if $VPN_OK; then
echo " ${C6} VPN ${C1}ACTIVE ${C7}${VPN_IP} ${C9}(${VPN_IF})${R}"
fi
# MAC
if [ -n "$CUR_MAC" ]; then
if $MAC_OK; then
echo " ${C6} MAC ${C1}RANDOM ${C9}(${CUR_MAC})${R}"
else
echo " ${C6} MAC ${C0}HWADDR ${C9}(${CUR_MAC})${R}"
fi
else
echo " ${C6} MAC ${C9}NO IFACE${R}"
fi
# ── HARDENING (advanced only) ──
if $ADVANCED; then
echo "$HR"
echo " ${C8}${BLD} ▌HARDENING${R}"
local _v6=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0")
[ "$_v6" = "1" ] && echo " ${C6} IPv6 ${C1}BLOCKED${R}" || echo " ${C6} IPv6 ${C0}LEAKING${R}"
if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then
echo " ${C6} DNSLK ${C1}LOCKED ${C9}(immutable)${R}"
else
echo " ${C6} DNSLK ${C0}UNLOCKED${R}"
fi
local _iso=$(grep "IsolateDestAddr" /etc/tor/torrc 2>/dev/null | head -1)
[ -n "$_iso" ] && echo " ${C6} ISOL ${C1}ACTIVE ${C9}(stream isolation)${R}"
local _pad=$(grep "ConnectionPadding" /etc/tor/torrc 2>/dev/null | awk '{print $2}')
[ "$_pad" = "1" ] && echo " ${C6} TPAD ${C1}ACTIVE ${C9}(traffic padding)${R}"
local _bl=$(grep "ExcludeExitNodes" /etc/tor/torrc 2>/dev/null | sed 's/ExcludeExitNodes //' | tr -d '{}' | tr ',' ' ')
[ -n "$_bl" ] && echo " ${C6} BLOCK ${C0}$(echo "$_bl" | tr ' ' ',' | sed 's/,$//')${R}"
local _core=$(sysctl -n kernel.core_pattern 2>/dev/null)
[[ "$_core" == *"/bin/false"* ]] && echo " ${C6} CORE ${C1}BLOCKED${R}" || echo " ${C6} CORE ${C0}ENABLED${R}"
local _swap=$(swapon --show=SIZE --noheadings 2>/dev/null | head -1)
[ -z "$_swap" ] && echo " ${C6} SWAP ${C1}OFF${R}" || echo " ${C6} SWAP ${C0}ACTIVE ${C9}(${_swap})${R}"
local _bootc=0
for _svc in opsec-boot-advanced opsec-mac-randomize opsec-hostname-randomize opsec-killswitch; do
systemctl is-enabled "$_svc" 2>/dev/null | grep -q enabled && _bootc=$((_bootc + 1))
done
if [ "$_bootc" -eq 4 ]; then
echo " ${C6} BOOT ${C1}PERSIST ${C9}(${_bootc}/4)${R}"
elif [ "$_bootc" -gt 0 ]; then
echo " ${C6} BOOT ${C2}PARTIAL ${C9}(${_bootc}/4)${R}"
else
echo " ${C6} BOOT ${C0}NONE ${C9}(${_bootc}/4)${R}"
fi
local _prof="${PROFILE_NAME:-}"
[ -n "$_prof" ] && echo " ${C6} PROF ${C2}${_prof}${R}"
fi
# ── TOR STATUS (advanced only) ──
if $ADVANCED; then
echo "$HR"
echo " ${C8}${BLD} ▌TOR STATUS${R}"
local _tor_svc=false _tor_socks=false _ks_armed=false _tor_routed=false
systemctl is-active tor >/dev/null 2>&1 && _tor_svc=true
ss -tln 2>/dev/null | grep -q ':9050 ' && _tor_socks=true
$ADVANCED && _ks_armed=true
[ "$ROUTED_TOR" = "true" ] && _tor_routed=true
if $_tor_svc && $_tor_socks && $_ks_armed && $_tor_routed; then
echo " ${C6} STATE ${C1}PROTECTED${R}"
elif $_tor_svc && ! $_tor_socks; then
echo " ${C6} STATE ${C2}BOOTSTRAP ${C9}(${TOR_BOOTSTRAP:-0}% ${TOR_PHASE:-connecting})${R}"
elif ! $_tor_svc; then
echo " ${C6} STATE ${C0}EXPOSED ${C9}(tor down)${R}"
elif ! $_ks_armed; then
echo " ${C6} STATE ${C0}EXPOSED ${C9}(kill switch off)${R}"
else
echo " ${C6} STATE ${C0}EXPOSED ${C9}(not routed)${R}"
fi
if [ -n "$EXIT_COUNTRY" ] && [ ${#EXIT_COUNTRY} -le 3 ]; then
echo " ${C6} EXIT ${C2}${EXIT_COUNTRY}${R}"
elif $_tor_socks; then
echo " ${C6} EXIT ${C9}resolving...${R}"
else
echo " ${C6} EXIT ${C9}${R}"
fi
if [ -n "$EXIT_CHANGE_TIME" ] && [ "$EXIT_CHANGE_TIME" -gt 0 ] 2>/dev/null; then
local _now=$(date +%s)
local _age=$(( _now - EXIT_CHANGE_TIME ))
if [ "$_age" -lt 60 ]; then
echo " ${C6} CIRCUIT ${C2}${_age}s ago${R}"
elif [ "$_age" -lt 3600 ]; then
echo " ${C6} CIRCUIT ${C2}$(( _age / 60 ))m ago${R}"
else
echo " ${C6} CIRCUIT ${C2}$(( _age / 3600 ))h $(( (_age % 3600) / 60 ))m ago${R}"
fi
else
echo " ${C6} CIRCUIT ${C9}${R}"
fi
local _dns_server=$(awk '/^nameserver/{print $2;exit}' /etc/resolv.conf 2>/dev/null)
local _dns_immutable=false
lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i' && _dns_immutable=true
local _nm_locked=false
[ -f /etc/NetworkManager/conf.d/opsec-dns-lock.conf ] && _nm_locked=true
if [ "$_dns_server" = "127.0.0.1" ] && $_dns_immutable && $_nm_locked; then
echo " ${C6} DNS ${C1}SECURE ${C9}(tor + locked)${R}"
elif [ "$_dns_server" = "127.0.0.1" ] && $_dns_immutable; then
echo " ${C6} DNS ${C2}SECURE ${C9}(tor, NM unlocked)${R}"
elif [ "$_dns_server" = "127.0.0.1" ]; then
echo " ${C6} DNS ${C2}PARTIAL ${C9}(tor, not locked)${R}"
elif echo "$_dns_server" | grep -qE '^(9\.9\.9\.9|149\.112|1\.1\.1\.1|1\.0\.0\.1)'; then
echo " ${C6} DNS ${C2}PRIVACY ${C9}(${_dns_server})${R}"
else
echo " ${C6} DNS ${C0}LEAKED ${C9}(${_dns_server})${R}"
fi
fi
# ── MODE / LEVEL ──
echo "$HR"
if $ADVANCED; then
echo " ${C6} MODE ${C1}ADVANCED${R}"
else
echo " ${C6} MODE ${C0}STANDARD${R}"
fi
local _lvl="${DEPLOYMENT_LEVEL:-}"
[ -n "$_lvl" ] && echo " ${C6} LEVEL ${C7}${_lvl}${R}"
echo "$HR"
echo " ${C9} // $(date +%H:%M:%S) //${R}"
echo ""
}
# ═════════════════════════════════════════════════════════════════════════════
# DISPATCH
# ═════════════════════════════════════════════════════════════════════════════
case "$BANNER_MODE" in
compact) render_compact ;;
full) render_full ;;
esac
+82
View File
@@ -0,0 +1,82 @@
#!/bin/bash
# /usr/local/bin/opsec-boot-init.sh — Level-aware boot initializer
# Called by opsec-boot-advanced.service at boot
# Standard levels: apply base privacy hardening only
# Paranoid levels: activate full ghost mode
set -euo pipefail
BOOT_MARKER="/etc/opsec/boot-advanced.enabled"
STATE_FILE="/var/run/opsec-advanced.enabled"
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
OPSEC_CONF="/etc/opsec/opsec.conf"
# Source shared library if available
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
fi
# Load config directly if lib not available
if [ -f "$OPSEC_CONF" ]; then
. "$OPSEC_CONF"
fi
LEVEL_TYPE="${LEVEL_TYPE:-standard}"
DEPLOYMENT_LEVEL="${DEPLOYMENT_LEVEL:-bare-metal-standard}"
echo "[opsec-boot] Level: ${DEPLOYMENT_LEVEL} (type: ${LEVEL_TYPE})"
# Paranoid levels: always activate full ghost mode
if [ "$LEVEL_TYPE" = "paranoid" ]; then
echo "[opsec-boot] Paranoid level detected — activating full ghost mode"
touch "$STATE_FILE"
if [ -x /usr/local/bin/opsec-mode.sh ]; then
/usr/local/bin/opsec-mode.sh on
else
echo "[opsec-boot] ERROR: opsec-mode.sh not found"
exit 1
fi
echo "[opsec-boot] Ghost mode activated at boot (paranoid level)"
exit 0
fi
# Standard levels: check boot marker, apply base or full accordingly
if [ -f "$BOOT_MARKER" ]; then
echo "[opsec-boot] Boot marker detected — activating ghost mode"
touch "$STATE_FILE"
if [ -x /usr/local/bin/opsec-mode.sh ]; then
/usr/local/bin/opsec-mode.sh on
else
echo "[opsec-boot] ERROR: opsec-mode.sh not found"
exit 1
fi
echo "[opsec-boot] Ghost mode activated at boot (boot marker)"
else
echo "[opsec-boot] Standard level, no boot marker — applying base privacy hardening"
# Apply base hardening inline (cannot source opsec-mode.sh — its exit kills the caller)
echo "[opsec-boot] Applying inline base hardening..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null 2>&1 || true
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null 2>&1 || true
swapoff -a 2>/dev/null || true
sysctl -w kernel.core_pattern='|/bin/false' >/dev/null 2>&1 || true
# Set privacy DNS
base_dns="${BASE_DNS:-quad9}"
chattr -i /etc/resolv.conf 2>/dev/null || true
case "$base_dns" in
quad9)
printf "nameserver 9.9.9.9\nnameserver 149.112.112.112\n" > /etc/resolv.conf
;;
cloudflare)
printf "nameserver 1.1.1.1\nnameserver 1.0.0.1\n" > /etc/resolv.conf
;;
esac
echo "[opsec-boot] Base privacy hardening applied"
fi
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
# OPSEC Status Check
echo "===== OPSEC Status Check ====="
echo ""
# Check for running VPN
echo "[*] VPN Status:"
if pgrep -x openvpn > /dev/null; then
echo " [+] OpenVPN is running"
else
echo " [-] OpenVPN is NOT running"
fi
# Check for Tor
echo ""
echo "[*] Tor Status:"
if systemctl is-active tor >/dev/null 2>&1; then
echo " [+] Tor is running"
else
echo " [-] Tor is NOT running"
fi
# Check current connections
echo ""
echo "[*] Active Connections:"
CONNECTIONS=$(ss -tupn | grep ESTAB | wc -l)
echo " [*] $CONNECTIONS established connections"
# Check listening services
echo ""
echo "[*] Listening Services:"
LISTENERS=$(ss -tupln | grep LISTEN | wc -l)
echo " [*] $LISTENERS listening services"
# Check DNS
echo ""
echo "[*] DNS Configuration:"
grep "nameserver" /etc/resolv.conf | head -3
# Check for history files
echo ""
echo "[*] History Files:"
for hist in ~/.bash_history ~/.zsh_history ~/.python_history ~/.mysql_history; do
if [ -f "$hist" ]; then
SIZE=$(stat -c%s "$hist" 2>/dev/null)
echo " [!] $hist exists (size: $SIZE bytes)"
fi
done
# Check MAC address
echo ""
echo "[*] Network Interfaces:"
for iface in $(ip -o link show | awk -F': ' '{print $2}' | grep -v lo); do
MAC=$(ip link show $iface | awk '/ether/ {print $2}')
echo " [*] $iface: $MAC"
done
# Check timezone
echo ""
echo "[*] System Timezone:"
echo " [*] $(timedatectl | grep "Time zone" | awk '{print $3}')"
# Check for running security tools
echo ""
echo "[*] Running Security Tools:"
# Tool inventory is configurable via OPSEC_TOOL_CHECK in opsec.conf
OPSEC_TOOL_CHECK="${OPSEC_TOOL_CHECK:-curl wget openssl gpg tor}"
for tool in $OPSEC_TOOL_CHECK; do
if pgrep -f $tool > /dev/null; then
echo " [!] $tool is running"
fi
done
echo ""
echo "===== Check Complete ====="
+1044
View File
File diff suppressed because it is too large Load Diff
+377
View File
@@ -0,0 +1,377 @@
#!/bin/bash
# /usr/local/bin/opsec-harden.sh — System hardening apply/revert
# Usage: sudo opsec-harden.sh apply|revert
#
# Reads settings from /etc/opsec/opsec.conf
# Handles: core dumps, swap, timezone, locale, screen lock, WebRTC,
# USB blocking, clipboard auto-clear, traffic jitter
## NOTE: Do NOT use "set -euo pipefail" — it causes silent crashes
## when commands like sysctl, dconf, tc, etc. return non-zero.
## Use explicit error handling instead (|| true).
if [ "$EUID" -ne 0 ]; then
echo "[-] Please run as root"
exit 1
fi
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
BACKUP_DIR="/etc/opsec/.harden-backup"
# Source shared library
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
# Minimal fallback
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_load_config() {
[ -f /etc/opsec/opsec.conf ] && . /etc/opsec/opsec.conf
}
fi
mkdir -p "$BACKUP_DIR"
opsec_load_config 2>/dev/null || true
# ─── CORE DUMPS ────────────────────────────────────────────────────────────────
apply_core_dumps() {
if [ "${HARDEN_CORE_DUMPS:-1}" = "1" ]; then
opsec_info "Disabling core dumps..."
# sysctl
sysctl -w kernel.core_pattern='|/bin/false' >/dev/null 2>&1
sysctl -w fs.suid_dumpable=0 >/dev/null 2>&1
# limits.d
cat > /etc/security/limits.d/opsec-coredump.conf << 'EOF'
# OPSEC: Disable core dumps
* hard core 0
* soft core 0
EOF
# systemd coredump
mkdir -p /etc/systemd/coredump.conf.d
cat > /etc/systemd/coredump.conf.d/opsec.conf << 'EOF'
[Coredump]
Storage=none
ProcessSizeMax=0
EOF
opsec_green "Core dumps disabled"
fi
}
revert_core_dumps() {
sysctl -w kernel.core_pattern='core' >/dev/null 2>&1
sysctl -w fs.suid_dumpable=1 >/dev/null 2>&1
rm -f /etc/security/limits.d/opsec-coredump.conf
rm -f /etc/systemd/coredump.conf.d/opsec.conf
opsec_green "Core dumps restored"
}
# ─── SWAP ──────────────────────────────────────────────────────────────────────
apply_swap() {
if [ "${HARDEN_SWAP:-1}" = "1" ]; then
opsec_info "Disabling swap..."
swapoff -a 2>/dev/null || true
# Comment out swap entries in fstab (backup first)
if [ ! -f "$BACKUP_DIR/fstab.swap" ]; then
grep -E '^\s*[^#].*\sswap\s' /etc/fstab > "$BACKUP_DIR/fstab.swap" 2>/dev/null || true
fi
sed -i '/\sswap\s/s/^/#OPSEC# /' /etc/fstab 2>/dev/null || true
opsec_green "Swap disabled"
fi
}
revert_swap() {
sed -i 's/^#OPSEC# //' /etc/fstab 2>/dev/null || true
swapon -a 2>/dev/null || true
opsec_green "Swap restored"
}
# ─── IPv6 ──────────────────────────────────────────────────────────────────────
apply_ipv6() {
if [ "${HARDEN_IPV6:-1}" = "1" ]; then
opsec_info "Disabling IPv6..."
sysctl -w net.ipv6.conf.all.disable_ipv6=1 >/dev/null
sysctl -w net.ipv6.conf.default.disable_ipv6=1 >/dev/null
opsec_green "IPv6 disabled"
fi
}
revert_ipv6() {
sysctl -w net.ipv6.conf.all.disable_ipv6=0 >/dev/null
sysctl -w net.ipv6.conf.default.disable_ipv6=0 >/dev/null
opsec_green "IPv6 restored"
}
# ─── TIMEZONE SPOOF ────────────────────────────────────────────────────────────
apply_timezone() {
if [ "${HARDEN_TIMEZONE_SPOOF:-0}" = "1" ]; then
local tz="${HARDEN_TIMEZONE_VALUE:-UTC}"
opsec_info "Spoofing timezone to ${tz}..."
# Backup current timezone
timedatectl show -p Timezone --value > "$BACKUP_DIR/timezone.orig" 2>/dev/null || true
timedatectl set-timezone "$tz" 2>/dev/null || {
ln -sf "/usr/share/zoneinfo/${tz}" /etc/localtime
}
opsec_green "Timezone set to ${tz}"
fi
}
revert_timezone() {
if [ -f "$BACKUP_DIR/timezone.orig" ]; then
local orig_tz
orig_tz=$(cat "$BACKUP_DIR/timezone.orig")
timedatectl set-timezone "$orig_tz" 2>/dev/null || true
rm -f "$BACKUP_DIR/timezone.orig"
opsec_green "Timezone restored to ${orig_tz}"
fi
}
# ─── LOCALE SPOOF ─────────────────────────────────────────────────────────────
apply_locale() {
if [ "${HARDEN_LOCALE_SPOOF:-0}" = "1" ]; then
local loc="${HARDEN_LOCALE_VALUE:-en_US.UTF-8}"
opsec_info "Spoofing locale to ${loc}..."
# Backup
locale > "$BACKUP_DIR/locale.orig" 2>/dev/null || true
export LANG="$loc"
export LC_ALL="$loc"
echo "LANG=${loc}" > /etc/default/locale.opsec
opsec_green "Locale set to ${loc}"
fi
}
revert_locale() {
rm -f /etc/default/locale.opsec
if [ -f "$BACKUP_DIR/locale.orig" ]; then
rm -f "$BACKUP_DIR/locale.orig"
fi
opsec_green "Locale restored"
}
# ─── SCREEN LOCK ───────────────────────────────────────────────────────────────
apply_screen_lock() {
if [ "${HARDEN_SCREEN_LOCK:-1}" = "1" ]; then
local timeout="${HARDEN_SCREEN_LOCK_TIMEOUT:-300}"
opsec_info "Setting screen lock timeout to ${timeout}s..."
# Try GNOME dconf (runs as user via sudo)
local real_user="${SUDO_USER:-$USER}"
if command -v dconf >/dev/null 2>&1; then
su - "$real_user" -c "
dconf write /org/gnome/desktop/session/idle-delay 'uint32 ${timeout}' 2>/dev/null
dconf write /org/gnome/desktop/screensaver/lock-enabled 'true' 2>/dev/null
dconf write /org/gnome/desktop/screensaver/lock-delay 'uint32 0' 2>/dev/null
" 2>/dev/null || true
fi
opsec_green "Screen lock set to ${timeout}s"
fi
}
revert_screen_lock() {
local real_user="${SUDO_USER:-$USER}"
if command -v dconf >/dev/null 2>&1; then
su - "$real_user" -c "
dconf write /org/gnome/desktop/session/idle-delay 'uint32 900' 2>/dev/null
" 2>/dev/null || true
fi
opsec_green "Screen lock restored to default"
}
# ─── WEBRTC BLOCKING ──────────────────────────────────────────────────────────
apply_webrtc() {
if [ "${LEAK_WEBRTC_BLOCK:-1}" = "1" ]; then
opsec_info "Blocking WebRTC..."
local real_user="${SUDO_USER:-$USER}"
local user_home
user_home=$(eval echo "~${real_user}")
# Firefox profiles
for profile_dir in "${user_home}"/.mozilla/firefox/*.default* "${user_home}"/.mozilla/firefox/*.opsec*; do
[ -d "$profile_dir" ] || continue
cat >> "${profile_dir}/user.js" << 'EOF'
// OPSEC: Disable WebRTC IP leak
user_pref("media.peerconnection.enabled", false);
user_pref("media.peerconnection.turn.disable", true);
user_pref("media.peerconnection.use_document_iceservers", false);
user_pref("media.peerconnection.video.enabled", false);
user_pref("media.peerconnection.identity.timeout", 1);
EOF
done
# Brave/Chromium policies
mkdir -p /etc/brave/policies/managed /etc/chromium/policies/managed
cat > /etc/brave/policies/managed/opsec-webrtc.json << 'EOF'
{ "WebRtcIPHandling": "disable_non_proxied_udp" }
EOF
cp /etc/brave/policies/managed/opsec-webrtc.json /etc/chromium/policies/managed/ 2>/dev/null || true
opsec_green "WebRTC blocked (Firefox + Brave/Chromium)"
fi
}
revert_webrtc() {
local real_user="${SUDO_USER:-$USER}"
local user_home
user_home=$(eval echo "~${real_user}")
for profile_dir in "${user_home}"/.mozilla/firefox/*.default* "${user_home}"/.mozilla/firefox/*.opsec*; do
[ -d "$profile_dir" ] || continue
sed -i '/OPSEC: Disable WebRTC/,/peerconnection\.identity/d' "${profile_dir}/user.js" 2>/dev/null || true
done
rm -f /etc/brave/policies/managed/opsec-webrtc.json
rm -f /etc/chromium/policies/managed/opsec-webrtc.json
opsec_green "WebRTC restrictions removed"
}
# ─── USB BLOCKING ─────────────────────────────────────────────────────────────
apply_usb_block() {
if [ "${LEAK_USB_BLOCK:-0}" = "1" ]; then
opsec_info "Blocking new USB devices..."
echo 0 > /sys/bus/usb/drivers_autoprobe 2>/dev/null || true
opsec_green "USB auto-probe disabled (new devices blocked)"
fi
}
revert_usb_block() {
echo 1 > /sys/bus/usb/drivers_autoprobe 2>/dev/null || true
opsec_green "USB auto-probe restored"
}
# ─── CLIPBOARD AUTO-CLEAR ─────────────────────────────────────────────────────
apply_clipboard_clear() {
if [ "${HARDEN_CLIPBOARD_CLEAR:-0}" = "1" ]; then
opsec_info "Starting clipboard auto-clear..."
# Launch background clearer (clears clipboard every 30 seconds)
local pidfile="/var/run/opsec-clipboard.pid"
if [ -f "$pidfile" ] && kill -0 "$(cat "$pidfile")" 2>/dev/null; then
opsec_yellow "Clipboard cleaner already running"
return
fi
(
while true; do
sleep 30
# Clear X clipboard
if command -v xclip >/dev/null 2>&1; then
echo -n "" | xclip -selection clipboard 2>/dev/null || true
echo -n "" | xclip -selection primary 2>/dev/null || true
elif command -v xsel >/dev/null 2>&1; then
xsel --clipboard --clear 2>/dev/null || true
xsel --primary --clear 2>/dev/null || true
fi
done
) &
echo $! > "$pidfile"
opsec_green "Clipboard auto-clear active (every 30s)"
fi
}
revert_clipboard_clear() {
local pidfile="/var/run/opsec-clipboard.pid"
if [ -f "$pidfile" ]; then
kill "$(cat "$pidfile")" 2>/dev/null || true
rm -f "$pidfile"
fi
opsec_green "Clipboard auto-clear stopped"
}
# ─── TRAFFIC JITTER ───────────────────────────────────────────────────────────
apply_traffic_jitter() {
if [ "${TRAFFIC_JITTER_ENABLED:-0}" = "1" ]; then
local ms="${TRAFFIC_JITTER_MS:-50}"
opsec_info "Adding ${ms}ms traffic jitter..."
# Find primary outbound interface
local iface
iface=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [ -n "$iface" ]; then
# Remove existing qdisc first
tc qdisc del dev "$iface" root 2>/dev/null || true
tc qdisc add dev "$iface" root netem delay "${ms}ms" "${ms}ms" distribution normal 2>/dev/null || {
opsec_yellow "tc not available — jitter skipped"
return
}
echo "$iface" > "$BACKUP_DIR/jitter-iface"
opsec_green "Traffic jitter active on ${iface} (${ms}ms)"
else
opsec_yellow "No outbound interface found for jitter"
fi
fi
}
revert_traffic_jitter() {
if [ -f "$BACKUP_DIR/jitter-iface" ]; then
local iface
iface=$(cat "$BACKUP_DIR/jitter-iface")
tc qdisc del dev "$iface" root 2>/dev/null || true
rm -f "$BACKUP_DIR/jitter-iface"
opsec_green "Traffic jitter removed from ${iface}"
fi
}
# ─── MAIN ──────────────────────────────────────────────────────────────────────
do_apply() {
echo ""
opsec_hdr "APPLYING SYSTEM HARDENING"
echo ""
apply_core_dumps
apply_swap
apply_ipv6
apply_timezone
apply_locale
apply_screen_lock
apply_webrtc
# Ghost-mode-only features — skip when called from mode_base (OPSEC_BASE_ONLY=1)
if [ "${OPSEC_BASE_ONLY:-0}" != "1" ]; then
apply_usb_block
apply_clipboard_clear
apply_traffic_jitter
else
opsec_info "Base-only mode — skipping USB block, clipboard clear, traffic jitter"
fi
echo ""
opsec_green "All hardening measures applied"
}
do_revert() {
echo ""
opsec_hdr "REVERTING SYSTEM HARDENING"
echo ""
revert_core_dumps
revert_swap
revert_ipv6
revert_timezone
revert_locale
revert_screen_lock
revert_webrtc
revert_usb_block
revert_clipboard_clear
revert_traffic_jitter
echo ""
opsec_green "All hardening measures reverted"
}
case "${1:-}" in
apply) do_apply ;;
revert) do_revert ;;
*)
echo "Usage: $(basename "$0") apply|revert"
exit 1
;;
esac
+64
View File
@@ -0,0 +1,64 @@
#!/bin/bash
# OPSEC Hostname Randomization
# Config-aware: reads HOSTNAME_PATTERN and HOSTNAME_CUSTOM_PREFIX from /etc/opsec/opsec.conf
# Patterns: desktop (desktop-XXXX), random (8 random chars), custom (PREFIX-XXXX)
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Source config if available
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_dim() { echo -e "\033[38;5;244m $*\033[0m"; }
fi
# Config values with defaults
PATTERN="${HOSTNAME_PATTERN:-desktop}"
PREFIX="${HOSTNAME_CUSTOM_PREFIX:-}"
RAND_HEX=$(head -c 2 /dev/urandom | od -An -tx1 | tr -d ' ')
OLD_HOSTNAME=$(hostname)
case "$PATTERN" in
desktop)
NEW_HOSTNAME="desktop-${RAND_HEX}"
;;
random)
NEW_HOSTNAME=$(head -c 4 /dev/urandom | od -An -tx1 | tr -d ' ')
;;
custom)
NEW_HOSTNAME="${PREFIX}-${RAND_HEX}"
;;
*)
NEW_HOSTNAME="desktop-${RAND_HEX}"
;;
esac
opsec_info "Randomizing hostname..."
opsec_dim "Old: ${OLD_HOSTNAME}"
opsec_dim "New: ${NEW_HOSTNAME} (pattern: ${PATTERN})"
# Set hostname via hostnamectl (persistent)
hostnamectl set-hostname "$NEW_HOSTNAME" 2>/dev/null || {
echo "$NEW_HOSTNAME" > /etc/hostname
hostname "$NEW_HOSTNAME"
}
# Update /etc/hosts to match
if grep -q "$OLD_HOSTNAME" /etc/hosts 2>/dev/null; then
sed -i "s/$OLD_HOSTNAME/$NEW_HOSTNAME/g" /etc/hosts
fi
# Ensure localhost entries exist
if ! grep -q "127.0.0.1.*$NEW_HOSTNAME" /etc/hosts; then
sed -i "/127\.0\.0\.1/s/$/ $NEW_HOSTNAME/" /etc/hosts
fi
opsec_green "Hostname randomized to: ${NEW_HOSTNAME}"
+261
View File
@@ -0,0 +1,261 @@
#!/bin/bash
# OPSEC Kill Switch — iptables rules to block non-Tor/VPN traffic
# Config-aware: reads KILLSWITCH_* settings from /etc/opsec/opsec.conf
# Usage: opsec-killswitch.sh on|off|status
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# Source config if available
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
fi
# Config values with defaults
ALLOW_DHCP="${KILLSWITCH_ALLOW_DHCP:-1}"
ALLOW_OPENVPN="${KILLSWITCH_ALLOW_OPENVPN:-1}"
ALLOW_WIREGUARD="${KILLSWITCH_ALLOW_WIREGUARD:-1}"
EXTRA_PORTS="${KILLSWITCH_EXTRA_PORTS:-}"
TRANS_PORT="${TOR_TRANS_PORT:-9040}"
DNS_PORT="${TOR_DNS_PORT:-5353}"
CHAIN="GP_FW"
TOR_UID=$(id -u debian-tor 2>/dev/null || id -u tor 2>/dev/null || echo "")
KS_LOG="/run/opsec/debug.log"
mkdir -p /run/opsec 2>/dev/null || true
KS_ERR=$(mktemp /run/opsec/.ks-err.XXXXXX 2>/dev/null || mktemp /tmp/.ks-err.XXXXXX)
KS_FAIL=0
trap 'rm -f "$KS_ERR"' EXIT
ks_log() { echo "[$(date -Is)] [killswitch] $*" >> "$KS_LOG"; }
# Wrapper that logs failures and tracks failure count
ipt() {
if ! iptables "$@" 2>>"$KS_ERR"; then
ks_log "FAILED: iptables $*$(cat "$KS_ERR" 2>/dev/null)"
cat "$KS_ERR" >&2 2>/dev/null
KS_FAIL=$((KS_FAIL + 1))
: > "$KS_ERR" 2>/dev/null
return 1
fi
: > "$KS_ERR" 2>/dev/null
return 0
}
ipt6() {
if ! ip6tables "$@" 2>>"$KS_ERR"; then
ks_log "FAILED: ip6tables $*$(cat "$KS_ERR" 2>/dev/null)"
KS_FAIL=$((KS_FAIL + 1))
: > "$KS_ERR" 2>/dev/null
return 1
fi
: > "$KS_ERR" 2>/dev/null
return 0
}
ks_filter_on() {
opsec_info "Arming OPSEC kill switch (filter-only)..."
ks_log "=== KILL SWITCH FILTER-ONLY ==="
ks_log "TOR_UID=${TOR_UID:-NONE}"
ks_log "ALLOW_DHCP=${ALLOW_DHCP} ALLOW_OPENVPN=${ALLOW_OPENVPN} ALLOW_WIREGUARD=${ALLOW_WIREGUARD}"
ks_log "EXTRA_PORTS=${EXTRA_PORTS:-none}"
iptables -N "$CHAIN" 2>/dev/null || iptables -F "$CHAIN"
ipt -A "$CHAIN" -o lo -j ACCEPT
ipt -A "$CHAIN" -i lo -j ACCEPT
ipt -A "$CHAIN" -d 127.0.0.0/8 -j ACCEPT
ipt -A "$CHAIN" -m state --state ESTABLISHED,RELATED -j ACCEPT
if [ -n "$TOR_UID" ]; then
ipt -A "$CHAIN" -m owner --uid-owner "$TOR_UID" -j ACCEPT
else
ks_log "WARNING: No TOR_UID — Tor will be blocked by filter!"
fi
ipt -A "$CHAIN" -o tun+ -j ACCEPT
ipt -A "$CHAIN" -i tun+ -j ACCEPT
ipt -A "$CHAIN" -o wg+ -j ACCEPT
ipt -A "$CHAIN" -i wg+ -j ACCEPT
[ "$ALLOW_DHCP" = "1" ] && ipt -A "$CHAIN" -p udp --dport 67:68 --sport 67:68 -j ACCEPT
[ "$ALLOW_OPENVPN" = "1" ] && { ipt -A "$CHAIN" -p udp --dport 1194 -j ACCEPT; ipt -A "$CHAIN" -p tcp --dport 1194 -j ACCEPT; }
[ "$ALLOW_WIREGUARD" = "1" ] && ipt -A "$CHAIN" -p udp --dport 51820 -j ACCEPT
if [ -n "$EXTRA_PORTS" ]; then
local IFS=','
for rule in $EXTRA_PORTS; do
local proto=$(echo "$rule" | cut -d: -f1)
local port=$(echo "$rule" | cut -d: -f2)
[ -n "$proto" ] && [ -n "$port" ] && ipt -A "$CHAIN" -p "$proto" --dport "$port" -j ACCEPT
done
fi
ipt -A "$CHAIN" -j DROP
iptables -D OUTPUT -j "$CHAIN" 2>/dev/null
ipt -I OUTPUT 1 -j "$CHAIN"
# IPv6
ip6tables -N "$CHAIN" 2>/dev/null || ip6tables -F "$CHAIN"
ipt6 -A "$CHAIN" -o lo -j ACCEPT
ipt6 -A "$CHAIN" -j DROP
ip6tables -D OUTPUT -j "$CHAIN" 2>/dev/null
ipt6 -I OUTPUT 1 -j "$CHAIN"
ks_log "Filter-only lockdown active (no NAT)"
opsec_green "Kill switch filter ARMED (pre-bootstrap)"
}
ks_nat_on() {
opsec_info "Arming NAT transparent proxy..."
ks_log "=== NAT PROXY ON ==="
local NAT_CHAIN="GP_NAT"
# Remove jump before flush to prevent brief bypass window
iptables -t nat -D OUTPUT -j "$NAT_CHAIN" 2>/dev/null
iptables -t nat -N "$NAT_CHAIN" 2>/dev/null || iptables -t nat -F "$NAT_CHAIN"
# Tor UID RETURN
[ -n "$TOR_UID" ] && ipt -t nat -A "$NAT_CHAIN" -p tcp -m owner --uid-owner "$TOR_UID" -j RETURN
# VPN RETURN — BEFORE DNS redirect
ipt -t nat -A "$NAT_CHAIN" -o tun+ -j RETURN
ipt -t nat -A "$NAT_CHAIN" -o wg+ -j RETURN
# DNS redirect
ipt -t nat -A "$NAT_CHAIN" -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT"
ipt -t nat -A "$NAT_CHAIN" -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT"
# Loopback RETURN
ipt -t nat -A "$NAT_CHAIN" -d 127.0.0.0/8 -j RETURN
# TCP catch-all TransPort
ipt -t nat -A "$NAT_CHAIN" -p tcp -j REDIRECT --to-ports "$TRANS_PORT"
# Install jump
ipt -t nat -I OUTPUT 1 -j "$NAT_CHAIN"
# Verify chain was populated (at least 3 rules: DNS redirect + TCP catch-all + others)
local nat_count
nat_count=$(iptables -t nat -L "$NAT_CHAIN" --line-numbers 2>/dev/null | tail -n +3 | wc -l)
ks_log "NAT chain rule count: ${nat_count}"
if [ "$nat_count" -lt 3 ]; then
ks_log "WARNING: NAT chain has only ${nat_count} rules — falling back to direct OUTPUT rules"
opsec_info "NAT chain underpopulated (${nat_count} rules) — using direct OUTPUT fallback"
# Remove the jump to the empty/broken chain
iptables -t nat -D OUTPUT -j "$NAT_CHAIN" 2>/dev/null
iptables -t nat -F "$NAT_CHAIN" 2>/dev/null
iptables -t nat -X "$NAT_CHAIN" 2>/dev/null
# Direct OUTPUT rules as fallback
[ -n "$TOR_UID" ] && iptables -t nat -A OUTPUT -p tcp -m owner --uid-owner "$TOR_UID" -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -o tun+ -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -o wg+ -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null
iptables -t nat -A OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null
iptables -t nat -A OUTPUT -d 127.0.0.0/8 -j RETURN 2>/dev/null
iptables -t nat -A OUTPUT -p tcp -j REDIRECT --to-ports "$TRANS_PORT" 2>/dev/null
local fb_count
fb_count=$(iptables -t nat -L OUTPUT --line-numbers 2>/dev/null | tail -n +3 | wc -l)
ks_log "Fallback OUTPUT NAT rule count: ${fb_count}"
fi
if [ "$KS_FAIL" -gt 0 ]; then
ks_log "WARNING: ${KS_FAIL} iptables commands failed during NAT setup"
opsec_info "WARNING: ${KS_FAIL} iptables rule(s) failed — check /run/opsec/debug.log"
return 1
fi
ks_log "NAT transparent proxy armed"
opsec_green "Kill switch fully ARMED — Tor + NAT active"
}
ks_on() {
ks_filter_on
ks_nat_on
}
ks_off() {
opsec_info "Disarming OPSEC kill switch..."
ks_log "=== KILL SWITCH OFF ==="
# Filter chain
iptables -D OUTPUT -j "$CHAIN" 2>/dev/null
iptables -F "$CHAIN" 2>/dev/null
iptables -X "$CHAIN" 2>/dev/null
# NAT chain (config-independent teardown)
local NAT_CHAIN="GP_NAT"
iptables -t nat -D OUTPUT -j "$NAT_CHAIN" 2>/dev/null
iptables -t nat -F "$NAT_CHAIN" 2>/dev/null
iptables -t nat -X "$NAT_CHAIN" 2>/dev/null
# Clean up fallback direct OUTPUT NAT rules (if chain approach failed during arm)
# Loop-delete to catch duplicates — keeps removing until no match left
local _fb_cleaned=0
while iptables -t nat -D OUTPUT -p tcp -j REDIRECT --to-ports "$TRANS_PORT" 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -p udp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -p tcp --dport 53 -j REDIRECT --to-ports "$DNS_PORT" 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -d 127.0.0.0/8 -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -o tun+ -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
while iptables -t nat -D OUTPUT -o wg+ -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
if [ -n "$TOR_UID" ]; then
while iptables -t nat -D OUTPUT -p tcp -m owner --uid-owner "$TOR_UID" -j RETURN 2>/dev/null; do _fb_cleaned=$((_fb_cleaned+1)); done
fi
[ "$_fb_cleaned" -gt 0 ] && ks_log "Cleaned ${_fb_cleaned} fallback NAT OUTPUT rules"
# IPv6
ip6tables -D OUTPUT -j "$CHAIN" 2>/dev/null
ip6tables -F "$CHAIN" 2>/dev/null
ip6tables -X "$CHAIN" 2>/dev/null
# Flush conntrack to clear stale NAT entries
conntrack -F 2>/dev/null && ks_log "conntrack flushed" || true
ks_log "Kill switch disarmed"
opsec_green "Kill switch DISARMED — normal traffic allowed"
}
ks_status() {
if iptables -L "$CHAIN" >/dev/null 2>&1; then
opsec_green "Kill switch is ARMED"
echo ""
echo "IPv4 rules:"
iptables -L "$CHAIN" -v -n --line-numbers
echo ""
echo "NAT chain:"
if iptables -t nat -L GP_NAT >/dev/null 2>&1; then
iptables -t nat -L GP_NAT -v -n --line-numbers
else
echo " (not armed)"
fi
echo ""
echo "IPv6 rules:"
ip6tables -L "$CHAIN" -v -n --line-numbers 2>/dev/null
else
opsec_info "Kill switch is OFF"
fi
}
case "${1}" in
on) ks_on ;;
filter-on) ks_filter_on ;;
nat-on) ks_nat_on ;;
off) ks_off ;;
status) ks_status ;;
*)
echo "Usage: $(basename "$0") on|off|filter-on|nat-on|status"
exit 1
;;
esac
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
# /usr/local/bin/opsec-log-rotate.sh — Secure log rotation and cleanup
# Truncates sensitive logs, shreds rotated files, clears journald + shell histories
# Runs via cron (configurable LOG_ROTATION_HOURS in /etc/opsec/opsec.conf)
set -euo pipefail
if [ "$EUID" -ne 0 ]; then
echo "[-] Please run as root"
exit 1
fi
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
_HAS_LIB=false
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
_HAS_LIB=true
else
opsec_green() { echo "[+] $*"; }
opsec_info() { echo "[~] $*"; }
fi
LOG_FILE="/var/log/opsec-log-rotate.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
log() { echo "[$TIMESTAMP] $*" >> "$LOG_FILE"; }
log "Starting secure log rotation"
# ─── TRUNCATE SENSITIVE SYSTEM LOGS ────────────────────────────────────────────
SENSITIVE_LOGS=(
/var/log/auth.log
/var/log/syslog
/var/log/kern.log
/var/log/daemon.log
/var/log/messages
/var/log/user.log
/var/log/mail.log
/var/log/debug
/var/log/wtmp
/var/log/btmp
/var/log/lastlog
/var/log/faillog
)
for logfile in "${SENSITIVE_LOGS[@]}"; do
if [ -f "$logfile" ]; then
truncate -s 0 "$logfile" 2>/dev/null || true
log "Truncated: $logfile"
fi
done
# ─── SHRED ROTATED LOG FILES (SSD-aware) ─────────────────────────────────────
for rotated in /var/log/*.gz /var/log/*.1 /var/log/*.old; do
if [ -f "$rotated" ]; then
if [ "$_HAS_LIB" = "true" ]; then
opsec_secure_delete "$rotated"
else
shred -fuz "$rotated" 2>/dev/null || rm -f "$rotated"
fi
log "Wiped: $rotated"
fi
done
# ─── CLEAR JOURNALD ───────────────────────────────────────────────────────────
if command -v journalctl >/dev/null 2>&1; then
journalctl --vacuum-time=1h 2>/dev/null || true
log "Journald vacuumed (1h retention)"
fi
# ─── CLEAR SHELL HISTORIES ────────────────────────────────────────────────────
for user_home in /home/* /root; do
[ -d "$user_home" ] || continue
for hist_file in .bash_history .zsh_history .python_history .psql_history .mysql_history .lesshst .viminfo; do
if [ -f "${user_home}/${hist_file}" ]; then
if [ "$_HAS_LIB" = "true" ]; then
opsec_secure_delete "${user_home}/${hist_file}"
else
shred -fuz "${user_home}/${hist_file}" 2>/dev/null || truncate -s 0 "${user_home}/${hist_file}" 2>/dev/null || true
fi
log "Cleared: ${user_home}/${hist_file}"
fi
done
done
# ─── CLEAR RECENTLY USED ──────────────────────────────────────────────────────
for user_home in /home/* /root; do
[ -d "$user_home" ] || continue
rm -f "${user_home}/.local/share/recently-used.xbel" 2>/dev/null || true
done
# ─── CLEAR TEMP FILES ─────────────────────────────────────────────────────────
find /tmp -type f -mmin +60 -delete 2>/dev/null || true
find /var/tmp -type f -mmin +60 -delete 2>/dev/null || true
log "Secure log rotation complete"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# opsec-mode-toggle — Keyboard shortcut wrapper for opsec-mode on/off
# Toggles between standard and advanced OPSEC mode
# Bind to: Ctrl+Alt+\
STATE_FILE="/var/run/opsec-advanced.enabled"
BOOTSTRAP_STOP="/var/run/opsec-bootstrap-stop"
DEBUG_LOG="/var/log/opsec-debug.log"
toggle_log() {
echo "[$(date -Is)] [toggle] $*" >> "$DEBUG_LOG" 2>/dev/null || true
}
if [ -f "$STATE_FILE" ]; then
# Turning OFF — signal bootstrap loop to stop, then run mode_off
toggle_log "Toggling OFF (state file exists)"
# Create stop signal so mode_on's bootstrap loop exits immediately
# (needs root — use pkexec for a one-liner)
pkexec bash -c "touch ${BOOTSTRAP_STOP} && /usr/local/bin/opsec-mode.sh off"
toggle_log "Toggle OFF complete (exit: $?)"
else
toggle_log "Toggling ON (no state file)"
pkexec /usr/local/bin/opsec-mode.sh on
toggle_log "Toggle ON complete (exit: $?)"
fi
+1210
View File
File diff suppressed because it is too large Load Diff
+119
View File
@@ -0,0 +1,119 @@
#!/bin/bash
# /usr/local/bin/opsec-monitor.sh — Background connection monitor
# Watches for non-Tor/non-VPN outbound connections, new listening ports,
# unexpected DNS queries. Sends desktop notifications + logs.
# Usage: sudo opsec-monitor.sh start|stop|status
set -euo pipefail
PIDFILE="/var/run/opsec-monitor.pid"
LOGFILE="/var/log/opsec-monitor.log"
INTERVAL=10
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
opsec_green() { echo "[+] $*"; }
opsec_red() { echo "[-] $*"; }
opsec_info() { echo "[~] $*"; }
fi
notify() {
local msg="$1" urgency="${2:-normal}"
local real_user="${SUDO_USER:-$USER}"
# Desktop notification
su - "$real_user" -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$real_user")/bus notify-send -u '$urgency' 'OPSEC Monitor' '$msg'" 2>/dev/null || true
# Log
echo "[$(date '+%H:%M:%S')] $msg" >> "$LOGFILE"
}
monitor_loop() {
local known_listeners=""
local tor_uid
tor_uid=$(id -u debian-tor 2>/dev/null || id -u tor 2>/dev/null || echo "")
while true; do
# ─── Check for non-Tor/VPN outbound connections ────────────────────
local suspicious
suspicious=$(ss -tunp 2>/dev/null | grep ESTAB | grep -v '127.0.0.1' | grep -v '::1' | \
grep -v "tun\|wg\|tor\|${tor_uid:-NOOP}" | \
grep -v ':9050\|:5353\|:1194\|:51820' || true)
if [ -n "$suspicious" ]; then
local count
count=$(echo "$suspicious" | wc -l)
notify "ALERT: ${count} non-Tor/VPN outbound connection(s) detected" "critical"
fi
# ─── Check for new listening ports ─────────────────────────────────
local current_listeners
current_listeners=$(ss -tlnp 2>/dev/null | tail -n +2 | awk '{print $4}' | sort)
if [ -n "$known_listeners" ] && [ "$current_listeners" != "$known_listeners" ]; then
local new_ports
new_ports=$(comm -13 <(echo "$known_listeners") <(echo "$current_listeners") 2>/dev/null || true)
if [ -n "$new_ports" ]; then
notify "New listening port(s): ${new_ports}" "critical"
fi
fi
known_listeners="$current_listeners"
# ─── Check for unexpected DNS queries ──────────────────────────────
local dns_leaks
dns_leaks=$(ss -tunp 2>/dev/null | grep ':53 ' | grep -v '127.0.0.1' | grep -v '::1' || true)
if [ -n "$dns_leaks" ]; then
notify "DNS LEAK: Non-local DNS query detected" "critical"
fi
sleep "$INTERVAL"
done
}
do_start() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
opsec_info "Monitor already running (PID: $(cat "$PIDFILE"))"
return
fi
monitor_loop &
echo $! > "$PIDFILE"
opsec_green "Monitor started (PID: $!, logging to ${LOGFILE})"
}
do_stop() {
if [ -f "$PIDFILE" ]; then
local pid
pid=$(cat "$PIDFILE")
kill "$pid" 2>/dev/null || true
# Kill child processes
pkill -P "$pid" 2>/dev/null || true
rm -f "$PIDFILE"
opsec_green "Monitor stopped"
else
opsec_info "Monitor not running"
fi
}
do_status() {
if [ -f "$PIDFILE" ] && kill -0 "$(cat "$PIDFILE")" 2>/dev/null; then
opsec_green "Monitor running (PID: $(cat "$PIDFILE"))"
if [ -f "$LOGFILE" ]; then
echo ""
echo "Last 10 log entries:"
tail -10 "$LOGFILE" 2>/dev/null || true
fi
else
opsec_info "Monitor not running"
fi
}
case "${1:-}" in
start) do_start ;;
stop) do_stop ;;
status) do_status ;;
*)
echo "Usage: $(basename "$0") start|stop|status"
exit 1
;;
esac
+291
View File
@@ -0,0 +1,291 @@
#!/bin/bash
# /usr/local/bin/opsec-preflight.sh — OPSEC Preflight Verification Gate
# Verifies all required security controls are active for the current mode.
#
# Usage:
# opsec-preflight.sh Human-readable HUD
# opsec-preflight.sh --score Pass/fail count
# opsec-preflight.sh --enforce Exit 1 if any applicable check fails
# opsec-preflight.sh --base Only run base checks (standard mode)
# opsec-preflight.sh --full Run base + ghost checks
# opsec-preflight.sh --base --enforce Base checks, exit 1 on fail
# opsec-preflight.sh --full --enforce Full checks, exit 1 on fail
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
OPSEC_CONF="/etc/opsec/opsec.conf"
PREFLIGHT_LOG="/var/log/opsec-preflight.log"
# Source library
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
opsec_load_config 2>/dev/null || true
elif [ -f "$OPSEC_CONF" ]; then
. "$OPSEC_CONF"
fi
# Fallback colors if lib not loaded
type opsec_green >/dev/null 2>&1 || {
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>] $*\033[0m"; }
opsec_dim() { echo -e "\033[38;5;244m $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
}
# ─── PARSE ARGS ───────────────────────────────────────────────────────────────
MODE_SCORE=false
MODE_ENFORCE=false
CHECK_LEVEL=""
while [ $# -gt 0 ]; do
case "$1" in
--score) MODE_SCORE=true ;;
--enforce) MODE_ENFORCE=true ;;
--base) CHECK_LEVEL="base" ;;
--full) CHECK_LEVEL="full" ;;
*) echo "Usage: opsec-preflight.sh [--score|--enforce] [--base|--full]"; exit 1 ;;
esac
shift
done
# Auto-detect check level if not specified
if [ -z "$CHECK_LEVEL" ]; then
if [ -f /var/run/opsec-advanced.enabled ]; then
CHECK_LEVEL="full"
else
CHECK_LEVEL="base"
fi
fi
# ─── ENVIRONMENT ──────────────────────────────────────────────────────────────
LEVEL="${DEPLOYMENT_LEVEL:-bare-metal-standard}"
LTYPE="${LEVEL_TYPE:-standard}"
is_cloud() {
case "$LEVEL" in
cloud-*) return 0 ;;
*) return 1 ;;
esac
}
is_bare_metal() {
case "$LEVEL" in
bare-metal-*) return 0 ;;
*) return 1 ;;
esac
}
# ─── CHECK ENGINE ─────────────────────────────────────────────────────────────
PASS=0
FAIL=0
SKIP=0
FAILURES=""
check() {
local name="$1" result="$2" detail="${3:-}"
if [ "$result" = "pass" ]; then
PASS=$((PASS + 1))
$MODE_SCORE || opsec_green "PASS ${name}${detail:+ ${detail}}"
elif [ "$result" = "fail" ]; then
FAIL=$((FAIL + 1))
FAILURES="${FAILURES}\n - ${name}${detail:+: ${detail}}"
$MODE_SCORE || opsec_red "FAIL ${name}${detail:+ ${detail}}"
elif [ "$result" = "skip" ]; then
SKIP=$((SKIP + 1))
$MODE_SCORE || opsec_dim "SKIP ${name}${detail:+ (${detail})}"
fi
}
# ─── BASE CHECKS (always run) ────────────────────────────────────────────────
run_base_checks() {
$MODE_SCORE || opsec_cyan "Base Privacy Checks"
# 1. MAC randomized (bare-metal only)
if is_bare_metal; then
local pif cur_mac first_octet first_dec
pif=$(ip route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1)
if [ -n "$pif" ]; then
cur_mac=$(ip link show "$pif" 2>/dev/null | awk '/ether/ {print $2}')
first_octet=$(echo "$cur_mac" | cut -d: -f1)
first_dec=$((16#${first_octet})) 2>/dev/null || first_dec=0
if (( first_dec & 2 )); then
check "MAC randomized" "pass" "${pif}: ${cur_mac}"
else
check "MAC randomized" "fail" "${pif}: ${cur_mac} (hardware address)"
fi
else
check "MAC randomized" "skip" "no primary interface"
fi
else
check "MAC randomized" "skip" "cloud deployment"
fi
# 2. IPv6 disabled
local ipv6_all
ipv6_all=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0")
if [ "$ipv6_all" = "1" ]; then
check "IPv6 disabled" "pass"
else
check "IPv6 disabled" "fail" "net.ipv6.conf.all.disable_ipv6=${ipv6_all}"
fi
# 3. Core dumps disabled
local core_pat
core_pat=$(sysctl -n kernel.core_pattern 2>/dev/null)
if [[ "$core_pat" == *"/bin/false"* ]] || [[ "$core_pat" == *"devnull"* ]]; then
check "Core dumps disabled" "pass"
else
check "Core dumps disabled" "fail" "core_pattern=${core_pat}"
fi
# 4. Swap disabled
local swap_count
swap_count=$(swapon --show --noheadings 2>/dev/null | wc -l)
if [ "$swap_count" -eq 0 ]; then
check "Swap disabled" "pass"
else
check "Swap disabled" "fail" "${swap_count} swap device(s) active"
fi
# 5. DNS is privacy resolver (not ISP)
local dns_server
dns_server=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf 2>/dev/null)
case "$dns_server" in
127.0.0.1|127.0.0.53|9.9.9.9|149.112.112.112|1.1.1.1|1.0.0.1)
check "DNS privacy resolver" "pass" "${dns_server}"
;;
*)
check "DNS privacy resolver" "fail" "${dns_server} (possibly ISP)"
;;
esac
# 6. WebRTC blocked (if Firefox present)
if [ -d "$HOME/.mozilla/firefox" ] || [ -d "/root/.mozilla/firefox" ]; then
local userjs_found=false
local profile_dirs
profile_dirs=$(find /home/*/.mozilla/firefox /root/.mozilla/firefox -maxdepth 1 -name '*.default*' 2>/dev/null | head -3)
for pd in $profile_dirs; do
if [ -f "$pd/user.js" ] && grep -q "media.peerconnection.enabled.*false" "$pd/user.js" 2>/dev/null; then
userjs_found=true
break
fi
done
if $userjs_found; then
check "WebRTC blocked" "pass" "user.js configured"
else
check "WebRTC blocked" "fail" "no user.js with WebRTC disable found"
fi
else
check "WebRTC blocked" "skip" "Firefox not detected"
fi
}
# ─── GHOST CHECKS (only when ghost mode active) ──────────────────────────────
run_ghost_checks() {
$MODE_SCORE || opsec_cyan "Ghost Mode Checks"
# 1. VPN or Tor active
local vpn_if tor_ok=false
vpn_if=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^(tun|wg)' | head -1)
ss -tln 2>/dev/null | grep -q ':9050 ' && tor_ok=true
if [ -n "$vpn_if" ] || $tor_ok; then
local detail=""
[ -n "$vpn_if" ] && detail="VPN:${vpn_if}"
$tor_ok && detail="${detail:+${detail} + }Tor:9050"
check "VPN/Tor active" "pass" "$detail"
else
check "VPN/Tor active" "fail" "no tun/wg interface, no Tor on :9050"
fi
# 2. Kill switch armed
if [ -f /var/run/opsec-breakglass.active ]; then
check "Kill switch armed" "fail" "BREAK-GLASS active — kill switch intentionally dropped"
elif iptables -L GP_FW >/dev/null 2>&1; then
check "Kill switch armed" "pass"
else
check "Kill switch armed" "fail" "GP_FW chain not found"
fi
# 3. DNS locked to Tor
local dns_server
dns_server=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf 2>/dev/null)
if [ "$dns_server" = "127.0.0.1" ]; then
check "DNS locked to Tor" "pass"
else
check "DNS locked to Tor" "fail" "DNS=${dns_server} (expected 127.0.0.1)"
fi
# 4. Hostname randomized (bare-metal only)
if is_bare_metal; then
local cur_hostname
cur_hostname=$(hostname)
# Check if hostname looks randomized (not default patterns like kali, debian, localhost)
case "$cur_hostname" in
kali|debian|localhost|ubuntu|parrot)
check "Hostname randomized" "fail" "hostname=${cur_hostname} (appears default)"
;;
*)
check "Hostname randomized" "pass" "${cur_hostname}"
;;
esac
else
check "Hostname randomized" "skip" "cloud deployment"
fi
# 5. resolv.conf immutable
if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then
check "resolv.conf immutable" "pass"
else
check "resolv.conf immutable" "fail" "chattr +i not set"
fi
}
# ─── EXECUTE CHECKS ──────────────────────────────────────────────────────────
if ! $MODE_SCORE; then
echo ""
opsec_hdr "OPSEC PREFLIGHT — ${CHECK_LEVEL^^}"
opsec_dim "Level: ${LEVEL} (${LTYPE})"
echo ""
fi
run_base_checks
if [ "$CHECK_LEVEL" = "full" ]; then
$MODE_SCORE || echo ""
run_ghost_checks
fi
# ─── RESULTS ──────────────────────────────────────────────────────────────────
TOTAL=$((PASS + FAIL))
if $MODE_SCORE; then
echo "${PASS}/${TOTAL} passed"
[ "$FAIL" -gt 0 ] && exit 1
exit 0
fi
echo ""
opsec_hdr "PREFLIGHT RESULT"
if [ "$FAIL" -eq 0 ]; then
opsec_green "ALL CHECKS PASSED (${PASS}/${TOTAL})"
[ "$SKIP" -gt 0 ] && opsec_dim "${SKIP} checks skipped (not applicable)"
else
opsec_red "FAILED: ${FAIL}/${TOTAL} checks"
opsec_dim "Passed: ${PASS} | Failed: ${FAIL} | Skipped: ${SKIP}"
echo -e "\033[38;5;196mFailures:${FAILURES}\033[0m"
# Log failures
echo "[$(date -Is)] PREFLIGHT ${CHECK_LEVEL^^}: ${PASS}/${TOTAL} passed, ${FAIL} failed${FAILURES}" >> "$PREFLIGHT_LOG" 2>/dev/null || true
fi
echo ""
if $MODE_ENFORCE && [ "$FAIL" -gt 0 ]; then
exit 1
fi
exit 0
+115
View File
@@ -0,0 +1,115 @@
#!/bin/bash
# /usr/local/bin/opsec-sentry.sh — Continuous Threat Sentry Manager
# Manages the sentry daemon for background threat detection
# Usage: opsec-sentry.sh start|stop|status|restart
set -euo pipefail
SENTRY_BIN="${SENTRY_BIN:-/usr/local/bin/sentry-daemon}"
PID_FILE="/var/run/opsec-sentry.pid"
LOG_FILE="/var/log/opsec-sentry.log"
ALERT_HOOK=""
# Source config for alert hook if available
OPSEC_CONF="/etc/opsec/opsec.conf"
[ -f "$OPSEC_CONF" ] && . "$OPSEC_CONF"
# Color output helpers
_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
is_running() {
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null
}
sentry_start() {
if is_running; then
_yellow "Sentry already running (PID $(cat "$PID_FILE"))"
return 0
fi
if [ ! -x "$SENTRY_BIN" ]; then
_red "Sentry binary not found at ${SENTRY_BIN}"
return 1
fi
local cmd="$SENTRY_BIN --daemon --sentry-pid $PID_FILE --sentry-log $LOG_FILE"
[ -n "$ALERT_HOOK" ] && cmd="$cmd --alert-hook $ALERT_HOOK"
# Run sentry in daemon mode
$cmd &
local pid=$!
echo "$pid" > "$PID_FILE"
# Verify it started
sleep 1
if kill -0 "$pid" 2>/dev/null; then
_green "Sentry daemon started (PID ${pid})"
_info "Log: ${LOG_FILE}"
else
rm -f "$PID_FILE"
_red "Sentry failed to start — check ${LOG_FILE}"
return 1
fi
}
sentry_stop() {
if ! is_running; then
_yellow "Sentry not running"
rm -f "$PID_FILE"
return 0
fi
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
kill "$pid" 2>/dev/null
# Wait for graceful shutdown
local i=0
while kill -0 "$pid" 2>/dev/null && [ $i -lt 10 ]; do
sleep 1
i=$((i + 1))
done
if kill -0 "$pid" 2>/dev/null; then
kill -9 "$pid" 2>/dev/null
fi
rm -f "$PID_FILE"
_green "Sentry daemon stopped"
}
sentry_status() {
if is_running; then
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
_green "Sentry ACTIVE (PID ${pid})"
if [ -f "$LOG_FILE" ]; then
local lines
lines=$(wc -l < "$LOG_FILE" 2>/dev/null || echo "0")
_info "Log entries: ${lines}"
local last
last=$(tail -1 "$LOG_FILE" 2>/dev/null)
[ -n "$last" ] && _info "Last: ${last}"
fi
else
_yellow "Sentry NOT running"
rm -f "$PID_FILE"
fi
}
case "${1:-}" in
start) sentry_start ;;
stop) sentry_stop ;;
restart)
sentry_stop
sleep 1
sentry_start
;;
status) sentry_status ;;
*)
echo "Usage: opsec-sentry.sh start|stop|status|restart"
exit 1
;;
esac
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
# /usr/local/bin/opsec-ssh-check.sh — SSH Honeypot Detection
# Checks target SSH banners for known honeypot signatures
# Alias: ssh-safe
#
# Usage: opsec-ssh-check.sh <host> [port]
set -euo pipefail
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>] $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
fi
HOST="${1:-}"
PORT="${2:-22}"
if [ -z "$HOST" ]; then
echo "Usage: $(basename "$0") <host> [port]"
echo " ssh-safe <host> [port]"
exit 1
fi
opsec_hdr "SSH HONEYPOT CHECK: ${HOST}:${PORT}"
echo ""
SUSPICIOUS=0
# ─── GRAB BANNER ───────────────────────────────────────────────────────────────
opsec_info "Grabbing SSH banner..."
BANNER=$(timeout 5 bash -c "echo '' | nc -w 3 $HOST $PORT 2>/dev/null" || true)
if [ -z "$BANNER" ]; then
opsec_yellow "No banner received — port may be filtered or service is not SSH"
exit 1
fi
opsec_cyan "Banner: ${BANNER}"
echo ""
# ─── KNOWN HONEYPOT SIGNATURES ─────────────────────────────────────────────────
# Cowrie
if echo "$BANNER" | grep -qiE 'SSH-2\.0-OpenSSH_6\.(0|1|2|6)p1.*Debian'; then
opsec_red "COWRIE SIGNATURE: Old OpenSSH version commonly used by Cowrie honeypot"
SUSPICIOUS=$((SUSPICIOUS + 3))
fi
# Kippo
if echo "$BANNER" | grep -qi 'SSH-1\.99-OpenSSH_5\.1p1'; then
opsec_red "KIPPO SIGNATURE: SSH-1.99 with OpenSSH_5.1p1 is a known Kippo default"
SUSPICIOUS=$((SUSPICIOUS + 3))
fi
# HonSSH
if echo "$BANNER" | grep -qi 'HonSSH'; then
opsec_red "HONSH SIGNATURE: Banner explicitly mentions HonSSH"
SUSPICIOUS=$((SUSPICIOUS + 5))
fi
# Generic old version check
if echo "$BANNER" | grep -qE 'OpenSSH_[345]\.' ; then
opsec_yellow "SUSPICIOUS: Very old OpenSSH version (common in honeypots)"
SUSPICIOUS=$((SUSPICIOUS + 2))
fi
# Unusual SSH protocol version
if echo "$BANNER" | grep -q 'SSH-1\.'; then
opsec_yellow "SUSPICIOUS: SSHv1 protocol (deprecated, often honeypot)"
SUSPICIOUS=$((SUSPICIOUS + 2))
fi
# ─── KEY EXCHANGE CHECK ────────────────────────────────────────────────────────
opsec_info "Checking key exchange algorithms..."
KEX_OUTPUT=$(timeout 5 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o BatchMode=yes -o ConnectTimeout=3 -v -p "$PORT" "check@${HOST}" 2>&1 || true)
# Check for weak/unusual key types
if echo "$KEX_OUTPUT" | grep -qi 'ssh-dss'; then
opsec_yellow "SUSPICIOUS: DSA host key (often seen in honeypots)"
SUSPICIOUS=$((SUSPICIOUS + 1))
fi
# Check for unusually fast key exchange (honeypots often respond instantly)
if echo "$KEX_OUTPUT" | grep -qi 'Connection reset\|Connection refused'; then
opsec_info "Connection dropped (may be rate-limited or filtered)"
fi
# ─── TIMING CHECK ─────────────────────────────────────────────────────────────
opsec_info "Checking authentication timing..."
AUTH_START=$(date +%s%N)
timeout 3 ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
-o BatchMode=yes -o ConnectTimeout=3 -p "$PORT" "probe@${HOST}" 2>/dev/null || true
AUTH_END=$(date +%s%N)
AUTH_MS=$(( (AUTH_END - AUTH_START) / 1000000 ))
if [ "$AUTH_MS" -lt 50 ] && [ "$AUTH_MS" -gt 0 ]; then
opsec_yellow "SUSPICIOUS: Unusually fast auth response (${AUTH_MS}ms) — possible honeypot"
SUSPICIOUS=$((SUSPICIOUS + 1))
else
opsec_info "Auth response time: ${AUTH_MS}ms"
fi
# ─── VERDICT ───────────────────────────────────────────────────────────────────
echo ""
if [ "$SUSPICIOUS" -ge 3 ]; then
opsec_red "VERDICT: HIGH RISK — Likely honeypot (score: ${SUSPICIOUS})"
opsec_red "DO NOT connect to this host for operations"
elif [ "$SUSPICIOUS" -ge 1 ]; then
opsec_yellow "VERDICT: MODERATE RISK — Some anomalies detected (score: ${SUSPICIOUS})"
opsec_yellow "Proceed with caution"
else
opsec_green "VERDICT: LOW RISK — No honeypot signatures detected (score: ${SUSPICIOUS})"
fi
echo ""
+23
View File
@@ -0,0 +1,23 @@
#!/bin/bash
# OPSEC Toggle — single entry point for keyboard shortcut + desktop file
# Uses pkexec directly on opsec-mode.sh for proper polkit policy matching
OPSEC_MODE="/usr/local/bin/opsec-mode.sh"
STATE_FILE="/var/run/opsec-advanced.enabled"
[ ! -x "$OPSEC_MODE" ] && exit 1
if [ -f "$STATE_FILE" ]; then
# Turning OFF — opsec-mode.sh off handles bootstrap stop signal internally
if [ "$EUID" -eq 0 ] 2>/dev/null; then
"$OPSEC_MODE" off
else
pkexec "$OPSEC_MODE" off
fi
else
# Turning ON — pkexec prompts for auth via polkit policy
if [ "$EUID" -eq 0 ] 2>/dev/null; then
"$OPSEC_MODE" on
else
pkexec "$OPSEC_MODE" on
fi
fi
+180
View File
@@ -0,0 +1,180 @@
#!/bin/bash
# /usr/local/bin/opsec-traffic-blend.sh — Decoy Traffic Blending Daemon
# Generates background browsing noise through Tor/VPN to blend tool traffic
# with normal-looking web activity. Ghost mode only.
# Usage: opsec-traffic-blend.sh start|stop|status
set -euo pipefail
PID_FILE="/var/run/opsec-traffic-blend.pid"
LOG_FILE="/var/log/opsec-traffic-blend.log"
# Source config
OPSEC_CONF="/etc/opsec/opsec.conf"
[ -f "$OPSEC_CONF" ] && . "$OPSEC_CONF"
SOCKS_PORT="${TOR_SOCKS_PORT:-9050}"
# Mean interval in seconds (Poisson distribution approximation)
BLEND_INTERVAL="${TRAFFIC_BLEND_INTERVAL:-30}"
# Color helpers
_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
# Benign URLs to simulate normal browsing (mix of news, tech, social)
DECOY_URLS=(
"https://en.wikipedia.org/wiki/Special:Random"
"https://www.bbc.com/news"
"https://news.ycombinator.com"
"https://www.reuters.com"
"https://stackoverflow.com/questions"
"https://github.com/trending"
"https://www.reddit.com/r/technology/.json"
"https://www.weather.gov"
"https://httpbin.org/get"
"https://www.kernel.org"
"https://www.python.org"
"https://www.debian.org"
"https://archive.org"
"https://www.mozilla.org"
"https://duckduckgo.com/?q=weather"
"https://lite.cnn.com"
"https://text.npr.org"
)
# Random delay using Poisson-like distribution (exponential inter-arrival)
poisson_delay() {
# Approximate exponential distribution using bash
# -ln(U) * mean where U is uniform(0,1)
local mean="$1"
local rand
rand=$((RANDOM % 1000 + 1))
# Approximate: -ln(rand/1000) * mean
# Using awk for floating point
awk -v r="$rand" -v m="$mean" 'BEGIN {
u = r / 1000.0;
if (u < 0.001) u = 0.001;
delay = -log(u) * m;
if (delay < 5) delay = 5;
if (delay > 120) delay = 120;
printf "%d\n", delay;
}'
}
is_running() {
[ -f "$PID_FILE" ] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null
}
blend_loop() {
local url_count=${#DECOY_URLS[@]}
echo "[$(date -Is)] Traffic blend daemon started (mean interval: ${BLEND_INTERVAL}s)" >> "$LOG_FILE"
while true; do
# Pick a random URL
local idx=$((RANDOM % url_count))
local url="${DECOY_URLS[$idx]}"
# Determine proxy method
local curl_opts=("--silent" "--output" "/dev/null" "--max-time" "15")
# Use Tor SOCKS if available
if ss -tln 2>/dev/null | grep -q ":${SOCKS_PORT} "; then
curl_opts+=("--socks5-hostname" "127.0.0.1:${SOCKS_PORT}")
fi
# Add realistic headers
curl_opts+=("-H" "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0")
curl_opts+=("-H" "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8")
curl_opts+=("-H" "Accept-Language: en-US,en;q=0.5")
# Make the request
curl "${curl_opts[@]}" "$url" 2>/dev/null || true
# Calculate next delay (Poisson-distributed)
local delay
delay=$(poisson_delay "$BLEND_INTERVAL")
sleep "$delay"
done
}
blend_start() {
if ! [ -f /var/run/opsec-advanced.enabled ]; then
_yellow "Traffic blending requires ghost mode to be active"
return 1
fi
if is_running; then
_yellow "Traffic blend already running (PID $(cat "$PID_FILE"))"
return 0
fi
# Start the blend loop as a background process
blend_loop &
local pid=$!
echo "$pid" > "$PID_FILE"
disown "$pid"
sleep 1
if kill -0 "$pid" 2>/dev/null; then
_green "Traffic blend started (PID ${pid}, mean interval ${BLEND_INTERVAL}s)"
else
rm -f "$PID_FILE"
_red "Traffic blend failed to start"
return 1
fi
}
blend_stop() {
if ! is_running; then
_yellow "Traffic blend not running"
rm -f "$PID_FILE"
return 0
fi
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
kill "$pid" 2>/dev/null || true
# Wait for process to stop
local i=0
while kill -0 "$pid" 2>/dev/null && [ $i -lt 5 ]; do
sleep 1
i=$((i + 1))
done
kill -9 "$pid" 2>/dev/null || true
rm -f "$PID_FILE"
echo "[$(date -Is)] Traffic blend daemon stopped" >> "$LOG_FILE"
_green "Traffic blend stopped"
}
blend_status() {
if is_running; then
local pid
pid=$(cat "$PID_FILE" 2>/dev/null)
_green "Traffic blend ACTIVE (PID ${pid})"
_yellow "Mean interval: ${BLEND_INTERVAL}s"
else
_yellow "Traffic blend NOT running"
rm -f "$PID_FILE"
fi
}
case "${1:-}" in
start) blend_start ;;
stop) blend_stop ;;
restart)
blend_stop
sleep 1
blend_start
;;
status) blend_status ;;
*)
echo "Usage: opsec-traffic-blend.sh start|stop|status|restart"
exit 1
;;
esac
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
# OPSEC Widget Launcher — positions and launches the Conky widget
# Usage: opsec-widget-launch.sh [position]
#
# Positions:
# tl = top-left tc = top-center tr = top-right
# bl = bottom-left bc = bottom-center br = bottom-right
#
# Default: tr (top-right)
# Uses the theme system — regenerates config from /etc/opsec/themes/
CONKY_DIR="$HOME/.config/conky"
CONF="$CONKY_DIR/conky-opsec-widget.conf"
POS="${1:-tr}"
# Map shorthand to conky alignment + gaps
case "$POS" in
tl) ALIGN="top_left"; GAP_X=15; GAP_Y=15 ;;
tc) ALIGN="top_middle"; GAP_X=0; GAP_Y=15 ;;
tr) ALIGN="top_right"; GAP_X=15; GAP_Y=60 ;;
bl) ALIGN="bottom_left"; GAP_X=15; GAP_Y=60 ;;
bc) ALIGN="bottom_middle"; GAP_X=0; GAP_Y=60 ;;
br) ALIGN="bottom_right"; GAP_X=15; GAP_Y=60 ;;
*)
echo "Usage: $(basename "$0") [tl|tc|tr|bl|bc|br]"
echo ""
echo " tl = top-left tc = top-center tr = top-right"
echo " bl = bottom-left bc = bottom-center br = bottom-right"
exit 1
;;
esac
# Kill existing widget and stale cache daemon
killall conky 2>/dev/null
pkill -f 'conky-opsec-cache\.sh' 2>/dev/null
rm -f /tmp/.opsec-cache/netinfo 2>/dev/null
sleep 0.3
# Load theme from opsec config
OPSEC_CONF="/etc/opsec/opsec.conf"
THEME="default"
[ -f "$OPSEC_CONF" ] && THEME=$(grep "^WIDGET_THEME=" "$OPSEC_CONF" 2>/dev/null | cut -d'"' -f2)
[ -z "$THEME" ] && THEME="default"
THEME_FILE="/etc/opsec/themes/${THEME}.theme"
if [ -f "$THEME_FILE" ]; then
. "$THEME_FILE"
else
echo "Theme '${THEME}' not found, using defaults"
THEME_LABEL="Default"
CONKY_BG="0d1117"
CONKY_COLOR0="df2020"; CONKY_COLOR1="33ff33"; CONKY_COLOR2="3a8fd6"
CONKY_COLOR3="0d1117"; CONKY_COLOR4="1f6feb"; CONKY_COLOR5="58a6ff"
CONKY_COLOR6="79c0ff"; CONKY_COLOR7="c9d1d9"; CONKY_COLOR8="1f6feb"
CONKY_COLOR9="484f58"
fi
# Generate config with theme colors and chosen position
mkdir -p "$CONKY_DIR"
cat > "$CONF" << EOF
-- OPSEC Status Widget — OPSEC Status Widget
-- Theme: ${THEME} (${THEME_LABEL:-Custom})
-- Position: ${ALIGN}
conky.config = {
alignment = '${ALIGN}',
gap_x = ${GAP_X},
gap_y = ${GAP_Y},
minimum_width = 400,
minimum_height = 200,
maximum_width = 420,
own_window = true,
own_window_type = 'normal',
own_window_transparent = false,
own_window_argb_visual = true,
own_window_argb_value = 210,
own_window_colour = '${CONKY_BG:-0d1117}',
own_window_hints = 'undecorated,below,sticky,skip_taskbar,skip_pager',
xinerama_head = 0,
double_buffer = true,
draw_shades = true,
default_shade_color = '000000',
draw_outline = false,
draw_borders = true,
border_inner_margin = 12,
border_outer_margin = 4,
border_width = 1,
border_colour = '${CONKY_COLOR4:-1f6feb}',
stippled_borders = 0,
use_xft = true,
font = 'JetBrains Mono:size=10',
override_utf8_locale = true,
default_color = 'b0b0b0',
color0 = '${CONKY_COLOR0:-df2020}',
color1 = '${CONKY_COLOR1:-33ff33}',
color2 = '${CONKY_COLOR2:-3a8fd6}',
color3 = '${CONKY_COLOR3:-0d1117}',
color4 = '${CONKY_COLOR4:-1f6feb}',
color5 = '${CONKY_COLOR5:-58a6ff}',
color6 = '${CONKY_COLOR6:-79c0ff}',
color7 = '${CONKY_COLOR7:-c9d1d9}',
color8 = '${CONKY_COLOR8:-1f6feb}',
color9 = '${CONKY_COLOR9:-484f58}',
update_interval = 3,
total_run_times = 0,
cpu_avg_samples = 2,
no_buffers = true,
text_buffer_size = 8192,
short_units = true,
};
conky.text = [[
\${execpi 5 ~/.config/conky/conky-opsec-status.sh}
]];
EOF
# Launch
conky -c "$CONF" &
disown
echo "OPSEC widget launched: $ALIGN (theme: $THEME)"
+120
View File
@@ -0,0 +1,120 @@
#!/bin/bash
# /usr/local/bin/opsec-wifi-check.sh — Wireless Evil Twin Detection
# Periodic scan for AP changes, open networks, BSSID anomalies
# Usage: opsec-wifi-check.sh [scan|watch]
set -euo pipefail
OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh"
if [ -f "$OPSEC_LIB" ]; then
. "$OPSEC_LIB"
else
opsec_green() { echo -e "\033[38;5;49m[+] $*\033[0m"; }
opsec_red() { echo -e "\033[38;5;196m[-] $*\033[0m"; }
opsec_yellow() { echo -e "\033[38;5;214m[*] $*\033[0m"; }
opsec_info() { echo -e "\033[38;5;39m[~] $*\033[0m"; }
opsec_cyan() { echo -e "\033[38;5;51m[>] $*\033[0m"; }
opsec_hdr() { echo -e "\033[38;5;51m━━━ \033[38;5;201m$*\033[38;5;51m ━━━\033[0m"; }
fi
STATE_DIR="/var/run/opsec-wifi"
mkdir -p "$STATE_DIR"
notify() {
local msg="$1" urgency="${2:-normal}"
local real_user="${SUDO_USER:-$USER}"
su - "$real_user" -c "DISPLAY=:0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u "$real_user")/bus notify-send -u '$urgency' 'OPSEC WiFi' '$msg'" 2>/dev/null || true
}
do_scan() {
opsec_hdr "WIRELESS SECURITY SCAN"
echo ""
# Check if connected to WiFi
local connected_ssid connected_bssid connected_security
connected_ssid=$(nmcli -t -f active,ssid dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2)
connected_bssid=$(nmcli -t -f active,bssid dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2-)
connected_security=$(nmcli -t -f active,security dev wifi 2>/dev/null | grep '^yes:' | cut -d: -f2)
if [ -z "$connected_ssid" ]; then
opsec_info "Not connected to any WiFi network"
return
fi
opsec_cyan "Connected: ${connected_ssid} (${connected_bssid})"
echo ""
# ─── Check for open/unencrypted network ────────────────────────────────
if [ -z "$connected_security" ] || [ "$connected_security" = "--" ]; then
opsec_red "WARNING: Connected to OPEN (unencrypted) network!"
notify "Connected to OPEN WiFi: ${connected_ssid}" "critical"
elif echo "$connected_security" | grep -qi 'WEP'; then
opsec_red "WARNING: WEP encryption (trivially crackable)"
notify "WiFi using WEP: ${connected_ssid}" "critical"
else
opsec_green "Encryption: ${connected_security}"
fi
# ─── Scan for duplicate SSIDs (evil twin indicators) ───────────────────
opsec_info "Scanning for duplicate SSIDs..."
local scan_results
scan_results=$(nmcli -t -f ssid,bssid,signal,security dev wifi list --rescan yes 2>/dev/null || true)
if [ -n "$scan_results" ]; then
# Find APs with same SSID as connected but different BSSID
local duplicates
duplicates=$(echo "$scan_results" | grep "^${connected_ssid}:" | grep -v "${connected_bssid}" || true)
if [ -n "$duplicates" ]; then
local dup_count
dup_count=$(echo "$duplicates" | wc -l)
opsec_yellow "ALERT: ${dup_count} other AP(s) broadcasting '${connected_ssid}':"
echo "$duplicates" | while IFS=: read -r ssid bssid signal security; do
echo -e " \033[38;5;214m BSSID: ${bssid} Signal: ${signal} Security: ${security}\033[0m"
done
notify "Evil twin risk: ${dup_count} duplicate AP(s) for ${connected_ssid}" "critical"
else
opsec_green "No duplicate SSIDs detected"
fi
fi
# ─── BSSID change detection ────────────────────────────────────────────
local state_file="${STATE_DIR}/${connected_ssid}.bssid"
if [ -f "$state_file" ]; then
local prev_bssid
prev_bssid=$(cat "$state_file")
if [ "$prev_bssid" != "$connected_bssid" ]; then
opsec_red "BSSID CHANGED for '${connected_ssid}'!"
opsec_red " Previous: ${prev_bssid}"
opsec_red " Current: ${connected_bssid}"
notify "BSSID changed for ${connected_ssid}: ${prev_bssid}${connected_bssid}" "critical"
else
opsec_green "BSSID consistent with last scan"
fi
fi
echo "$connected_bssid" > "$state_file"
echo ""
}
do_watch() {
opsec_info "Starting continuous WiFi monitoring (Ctrl+C to stop)..."
while true; do
do_scan
echo ""
opsec_info "Next scan in 60 seconds..."
sleep 60
done
}
case "${1:-scan}" in
scan) do_scan ;;
watch) do_watch ;;
*)
echo "Usage: $(basename "$0") [scan|watch]"
echo ""
echo " scan — One-time wireless security scan (default)"
echo " watch — Continuous monitoring (60s interval)"
exit 1
;;
esac
+97
View File
@@ -0,0 +1,97 @@
# phantom — Privacy Server Deployer
Deploy self-hosted privacy infrastructure with a single command. Supports cloud providers, existing servers, and local deployment.
## Server Types
| Service | Status | Description |
|---|---|---|
| Matrix + Element | Ready | Encrypted messaging homeserver with web client |
| WireGuard VPN | Ready | Private VPN server with client config generation |
| Pi-hole DNS | Ready | Ad-blocking DNS server (Docker-based) |
| All-in-One | Ready | Multiple services on one server with nginx reverse proxy |
| Nextcloud | Stub | Self-hosted file sync and collaboration |
| Vaultwarden | Stub | Self-hosted Bitwarden password manager |
| Jellyfin | Stub | Self-hosted media server |
| Mail-in-a-Box | Stub | Self-hosted email |
## Deployment Targets
- **Linode** — Automated provisioning via API
- **AWS EC2** — Automated provisioning via API
- **FlokiNET** — Register pre-provisioned server
- **Existing server** — Any server with SSH access
- **Local** — Deploy directly on the current machine
## Quick Start
```bash
# Requirements
pip install ansible pyyaml # or: apt install ansible python3-yaml
# For AWS deployments:
pip install boto3
ansible-galaxy collection install amazon.aws
# Launch
python3 phantom.py
```
Select a service, choose a deployment target, provide configuration, and phantom handles the rest:
1. Provisions infrastructure (if cloud)
2. Applies base hardening (UFW, fail2ban, SSH lockdown, kernel hardening)
3. Deploys the selected service
4. Saves deployment info to `logs/`
## All-in-One Deployment
The all-in-one option deploys multiple services on a single server with:
- Nginx reverse proxy with TLS termination
- Let's Encrypt certificates via Certbot
- Per-service vhost routing
**Warning**: Running multiple services on one server means a compromise of one service risks all services. Use for lab/testing or personal use where convenience outweighs isolation. For production, deploy one service per server.
## Local Deployment
Select "Local (this machine)" as the deployment target to install services directly on your current system. This is useful for:
- Home lab servers
- Raspberry Pi deployments
- LAN-only services
- Testing before cloud deployment
No SSH key generation or remote provisioning is needed for local deployments.
## Provider Setup
### Linode
1. Create an API token at https://cloud.linode.com/profile/tokens
2. Select "Linode" when prompted and paste your token
### AWS
1. Create an IAM user with EC2 permissions
2. Generate access keys
3. Select "AWS" when prompted and provide credentials
### FlokiNET
1. Provision a server through FlokiNET's control panel
2. Select "FlokiNET" and provide the server IP
## Project Structure
```
phantom/
├── phantom.py # Main CLI
├── modules/ # Per-service configuration
├── playbooks/ # Ansible playbooks
│ ├── common/ # Shared hardening
│ ├── matrix/ # Synapse + Element
│ ├── vpn/ # WireGuard
│ ├── dns/ # Pi-hole
│ └── all_in_one/ # Multi-service composer
├── providers/ # Cloud provisioning
└── logs/ # Deployment artifacts
```
## License
MIT
+8
View File
@@ -0,0 +1,8 @@
[defaults]
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
timeout = 30
[ssh_connection]
pipelining = True
+1
View File
@@ -0,0 +1 @@
# phantom modules
+126
View File
@@ -0,0 +1,126 @@
"""All-in-one single-server deployment module.
Composes multiple services onto a single server with nginx reverse proxy
and TLS via Certbot. Includes clear warnings about single-server risks.
"""
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
YELLOW = "\033[38;5;214m"
RED = "\033[38;5;196m"
RESET = "\033[0m"
SERVICES = [
("matrix", "Matrix + Element", "Encrypted messaging"),
("vpn", "WireGuard VPN", "Private VPN server"),
("dns", "Pi-hole DNS", "Ad-blocking DNS"),
("cloud", "Nextcloud", "File sync (stub)"),
("vault", "Vaultwarden", "Password manager (stub)"),
("media", "Jellyfin", "Media server (stub)"),
]
def gather_config(config):
"""Gather all-in-one deployment configuration."""
# Risk warning
print(f"\n{RED} ╔═══════════════════════════════════════════════════════╗{RESET}")
print(f" {RED}{RESET} {YELLOW}WARNING: Single-Server Deployment{RESET} {RED}{RESET}")
print(f" {RED}{RESET} {RED}{RESET}")
print(f" {RED}{RESET} Running multiple services on one server means: {RED}{RESET}")
print(f" {RED}{RESET} - A compromise of one service risks ALL services {RED}{RESET}")
print(f" {RED}{RESET} - Resource contention between services {RED}{RESET}")
print(f" {RED}{RESET} - Single point of failure for all infrastructure {RED}{RESET}")
print(f" {RED}{RESET} - More complex backup and recovery {RED}{RESET}")
print(f" {RED}{RESET} {RED}{RESET}")
print(f" {RED}{RESET} For production use, prefer one service per server. {RED}{RESET}")
print(f" {RED}{RESET} This mode is intended for lab/testing or personal {RED}{RESET}")
print(f" {RED}{RESET} use where convenience outweighs isolation. {RED}{RESET}")
print(f" {RED}╚═══════════════════════════════════════════════════════╝{RESET}")
confirm = input(f"\n {YELLOW}Acknowledge risks and continue? [y/N]:{RESET} ").strip().lower()
if confirm != "y":
return None
# Service selection
print(f"\n{CYAN} ┌─ Select Services ────────────────────────────────────┐{RESET}")
for i, (svc_id, label, desc) in enumerate(SERVICES, 1):
print(f" {CYAN}{RESET} {WHITE}{i}{RESET}) {label:<20} {GREY}{desc}{RESET}")
print(f" {CYAN}└────────────────────────────────────────────────────────┘{RESET}")
selections = input(
f"\n {CYAN}Services to deploy (comma-separated, e.g. 1,2,3):{RESET} "
).strip()
selected_services = []
for s in selections.split(","):
s = s.strip()
try:
idx = int(s) - 1
if 0 <= idx < len(SERVICES):
selected_services.append(SERVICES[idx][0])
except ValueError:
pass
if not selected_services:
print(f" {RED}No services selected.{RESET}")
return None
# Warn about stub services
STUB_SERVICES = {"cloud", "vault", "media", "email"}
stubs_selected = [s for s in selected_services if s in STUB_SERVICES]
if stubs_selected:
stub_labels = {s[0]: s[1] for s in SERVICES}
print(f"\n {YELLOW}Warning: The following services are stubs (playbook not yet implemented):{RESET}")
for s in stubs_selected:
print(f" {YELLOW}- {stub_labels.get(s, s)}{RESET}")
proceed = input(f" {YELLOW}Continue anyway? [y/N]:{RESET} ").strip().lower()
if proceed != "y":
return None
config["services"] = selected_services
config["all_in_one"] = True
# Base domain for nginx vhosts
config["domain"] = input(
f"\n {CYAN}Base domain (e.g. example.com):{RESET} "
).strip()
if not config["domain"]:
print(f" {RED}Domain is required for reverse proxy.{RESET}")
return None
config["certbot_email"] = input(
f" {CYAN}Email for Let's Encrypt [{GREY}optional{RESET}]: "
).strip()
# Gather per-service configs
# Save base domain — each service stores config under its own prefixed keys
base_domain = config["domain"]
for svc in selected_services:
try:
mod = __import__(f"modules.{svc}", fromlist=[svc])
if hasattr(mod, "gather_config"):
# Set per-service subdomain default
svc_subdomains = {
"matrix": f"matrix.{base_domain}",
"cloud": f"cloud.{base_domain}",
"vault": f"vault.{base_domain}",
"media": f"media.{base_domain}",
"email": f"mail.{base_domain}",
"dns": f"dns.{base_domain}",
"vpn": base_domain,
}
if svc in svc_subdomains:
config["domain"] = svc_subdomains[svc]
config = mod.gather_config(config)
if config is None:
return None
except ImportError:
pass # Stub module, skip
# Restore base domain
config["domain"] = base_domain
config["nginx_reverse_proxy"] = True
config["certbot_enabled"] = bool(config.get("certbot_email"))
return config
+27
View File
@@ -0,0 +1,27 @@
"""Nextcloud deployment module (stub — playbook TODO)."""
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
RESET = "\033[0m"
def gather_config(config):
"""Gather Nextcloud configuration."""
print(f"\n{CYAN} ┌─ Nextcloud Configuration ─────────────────────────┐{RESET}")
print(f" {CYAN}{RESET} {GREY}Note: Playbook coming soon{RESET}")
config["domain"] = config.get("domain") or input(
f" {CYAN}{RESET} Domain (e.g. cloud.example.com): "
).strip()
config["cloud_admin_user"] = input(
f" {CYAN}{RESET} Admin username [{WHITE}admin{RESET}]: "
).strip() or "admin"
config["cloud_storage_gb"] = input(
f" {CYAN}{RESET} Storage quota per user (GB) [{WHITE}10{RESET}]: "
).strip() or "10"
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+49
View File
@@ -0,0 +1,49 @@
"""Pi-hole / AdGuard DNS server deployment module."""
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
YELLOW = "\033[38;5;214m"
RESET = "\033[0m"
def gather_config(config):
"""Gather DNS server configuration."""
print(f"\n{CYAN} ┌─ DNS Server Configuration ────────────────────────┐{RESET}")
print(f" {CYAN}{RESET} Upstream DNS provider:")
print(f" {CYAN}{RESET} {WHITE}1{RESET}) Quad9 (9.9.9.9) — security-focused")
print(f" {CYAN}{RESET} {WHITE}2{RESET}) Cloudflare (1.1.1.1) — privacy-focused")
print(f" {CYAN}{RESET} {WHITE}3{RESET}) Custom")
dns_choice = input(f" {CYAN}{RESET} Choice [{WHITE}1{RESET}]: ").strip() or "1"
if dns_choice == "1":
config["dns_upstream"] = "9.9.9.9;149.112.112.112"
config["dns_upstream_name"] = "Quad9"
elif dns_choice == "2":
config["dns_upstream"] = "1.1.1.1;1.0.0.1"
config["dns_upstream_name"] = "Cloudflare"
else:
config["dns_upstream"] = input(
f" {CYAN}{RESET} Custom DNS (semicolon-separated): "
).strip()
config["dns_upstream_name"] = "Custom"
config["dns_domain"] = input(
f" {CYAN}{RESET} Admin interface domain (optional): "
).strip()
print(f" {CYAN}{RESET}")
print(f" {CYAN}{RESET} Blocklist presets:")
print(f" {CYAN}{RESET} {WHITE}1{RESET}) Standard (ads + trackers)")
print(f" {CYAN}{RESET} {WHITE}2{RESET}) Aggressive (+ social media trackers)")
print(f" {CYAN}{RESET} {WHITE}3{RESET}) Minimal (ads only)")
bl_choice = input(f" {CYAN}{RESET} Choice [{WHITE}1{RESET}]: ").strip() or "1"
config["dns_blocklist"] = {"1": "standard", "2": "aggressive", "3": "minimal"}.get(
bl_choice, "standard"
)
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+23
View File
@@ -0,0 +1,23 @@
"""Mail-in-a-Box deployment module (stub — playbook TODO)."""
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
RESET = "\033[0m"
def gather_config(config):
"""Gather Mail-in-a-Box configuration."""
print(f"\n{CYAN} ┌─ Mail-in-a-Box Configuration ─────────────────────┐{RESET}")
print(f" {CYAN}{RESET} {GREY}Note: Playbook coming soon{RESET}")
config["domain"] = config.get("domain") or input(
f" {CYAN}{RESET} Mail domain (e.g. mail.example.com): "
).strip()
config["email_first_user"] = input(
f" {CYAN}{RESET} First email user (e.g. admin@example.com): "
).strip()
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+47
View File
@@ -0,0 +1,47 @@
"""Matrix + Element homeserver deployment module."""
import getpass
import secrets
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
RESET = "\033[0m"
def gather_config(config):
"""Gather Matrix/Synapse + Element configuration."""
print(f"\n{CYAN} ┌─ Matrix Homeserver Configuration ─────────────────┐{RESET}")
config["domain"] = config.get("domain") or input(
f" {CYAN}{RESET} Domain (e.g. matrix.example.com): "
).strip()
if not config["domain"]:
print(f" {CYAN}{RESET} Domain is required.")
return None
config["matrix_admin_user"] = input(
f" {CYAN}{RESET} Admin username [{WHITE}admin{RESET}]: "
).strip() or "admin"
config["matrix_admin_password"] = getpass.getpass(
f" {CYAN}{RESET} Admin password (blank=generate): "
) or secrets.token_urlsafe(20)
config["matrix_registration"] = input(
f" {CYAN}{RESET} Open registration? [{WHITE}no{RESET}]: "
).strip().lower()
config["matrix_registration"] = config["matrix_registration"] in ("yes", "y", "true")
config["matrix_element_web"] = input(
f" {CYAN}{RESET} Deploy Element Web? [{WHITE}yes{RESET}]: "
).strip().lower()
config["matrix_element_web"] = config["matrix_element_web"] not in ("no", "n", "false")
config["matrix_server_name"] = config["domain"].replace("matrix.", "", 1) \
if config["domain"].startswith("matrix.") else config["domain"]
config["matrix_signing_key"] = secrets.token_hex(32)
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+23
View File
@@ -0,0 +1,23 @@
"""Jellyfin media server deployment module (stub — playbook TODO)."""
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
RESET = "\033[0m"
def gather_config(config):
"""Gather Jellyfin configuration."""
print(f"\n{CYAN} ┌─ Jellyfin Configuration ──────────────────────────┐{RESET}")
print(f" {CYAN}{RESET} {GREY}Note: Playbook coming soon{RESET}")
config["domain"] = config.get("domain") or input(
f" {CYAN}{RESET} Domain (e.g. media.example.com): "
).strip()
config["media_library_path"] = input(
f" {CYAN}{RESET} Media library path [{WHITE}/srv/media{RESET}]: "
).strip() or "/srv/media"
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+29
View File
@@ -0,0 +1,29 @@
"""Vaultwarden (Bitwarden) deployment module (stub — playbook TODO)."""
import secrets
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
RESET = "\033[0m"
def gather_config(config):
"""Gather Vaultwarden configuration."""
print(f"\n{CYAN} ┌─ Vaultwarden Configuration ───────────────────────┐{RESET}")
print(f" {CYAN}{RESET} {GREY}Note: Playbook coming soon{RESET}")
config["domain"] = config.get("domain") or input(
f" {CYAN}{RESET} Domain (e.g. vault.example.com): "
).strip()
config["vault_admin_token"] = input(
f" {CYAN}{RESET} Admin token (blank=generate): "
).strip() or secrets.token_urlsafe(32)
config["vault_signups_allowed"] = input(
f" {CYAN}{RESET} Allow signups? [{WHITE}no{RESET}]: "
).strip().lower() in ("yes", "y", "true")
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+39
View File
@@ -0,0 +1,39 @@
"""WireGuard VPN server deployment module."""
CYAN = "\033[38;5;51m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
RESET = "\033[0m"
def gather_config(config):
"""Gather WireGuard VPN configuration."""
print(f"\n{CYAN} ┌─ WireGuard VPN Configuration ──────────────────────┐{RESET}")
config["vpn_port"] = input(
f" {CYAN}{RESET} Listen port [{WHITE}51820{RESET}]: "
).strip() or "51820"
config["vpn_client_count"] = input(
f" {CYAN}{RESET} Number of client configs [{WHITE}3{RESET}]: "
).strip() or "3"
try:
config["vpn_client_count"] = int(config["vpn_client_count"])
except ValueError:
config["vpn_client_count"] = 3
config["vpn_dns"] = input(
f" {CYAN}{RESET} Client DNS server [{WHITE}1.1.1.1{RESET}]: "
).strip() or "1.1.1.1"
config["vpn_allowed_ips"] = input(
f" {CYAN}{RESET} Allowed IPs [{WHITE}0.0.0.0/0, ::/0{RESET}]: "
).strip() or "0.0.0.0/0, ::/0"
config["vpn_subnet"] = input(
f" {CYAN}{RESET} VPN subnet [{WHITE}10.66.66.0/24{RESET}]: "
).strip() or "10.66.66.0/24"
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
return config
+463
View File
@@ -0,0 +1,463 @@
#!/usr/bin/env python3
"""phantom — Privacy Server Deployer
Deploy self-hosted privacy infrastructure with a single command.
Supports Matrix, WireGuard VPN, Pi-hole DNS, Nextcloud, and more.
Providers: Linode, AWS, FlokiNET, or local/existing server.
"""
import json
import os
import random
import subprocess
import sys
import time
from pathlib import Path
# Allow modules to import from phantom
sys.path.insert(0, os.path.dirname(__file__))
BASE_DIR = Path(__file__).resolve().parent
PLAYBOOKS_DIR = BASE_DIR / "playbooks"
PROVIDERS_DIR = BASE_DIR / "providers"
LOGS_DIR = BASE_DIR / "logs"
LOGS_DIR.mkdir(exist_ok=True)
# ─── Color Helpers ──────────────────────────────────────────────────────────
RESET = "\033[0m"
BOLD = "\033[1m"
DIM = "\033[2m"
CYAN = "\033[38;5;51m"
GREEN = "\033[38;5;49m"
RED = "\033[38;5;196m"
YELLOW = "\033[38;5;214m"
MAGENTA = "\033[38;5;201m"
WHITE = "\033[38;5;255m"
GREY = "\033[38;5;244m"
def ok(msg):
print(f"{GREEN}[+]{RESET} {msg}")
def err(msg):
print(f"{RED}[-]{RESET} {msg}")
def warn(msg):
print(f"{YELLOW}[*]{RESET} {msg}")
def info(msg):
print(f"{CYAN}[~]{RESET} {msg}")
def dim(msg):
print(f"{GREY} {msg}{RESET}")
# ─── Banner ─────────────────────────────────────────────────────────────────
BANNER = f"""
{MAGENTA} ██████╗ ██╗ ██╗ █████╗ ███╗ ██╗████████╗ ██████╗ ███╗ ███╗
██╔══██╗██║ ██║██╔══██╗████╗ ██║╚══██╔══╝██╔═══██╗████╗ ████║
██████╔╝███████║███████║██╔██╗ ██║ ██║ ██║ ██║██╔████╔██║
██╔═══╝ ██╔══██║██╔══██║██║╚██╗██║ ██║ ██║ ██║██║╚██╔╝██║
██║ ██║ ██║██║ ██║██║ ╚████║ ██║ ╚██████╔╝██║ ╚═╝ ██║
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚═╝ ╚═══╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝{RESET}
{GREY} Privacy Server Deployer — Your infrastructure, your rules{RESET}
"""
def banner():
print(BANNER)
# ─── Deployment ID Generation ───────────────────────────────────────────────
ADJECTIVES = [
"silent", "hidden", "shadow", "quiet", "swift", "dark", "fading", "lost",
"frozen", "drifting", "hollow", "veiled", "pale", "deep", "still", "wild",
"broken", "burning", "crimson", "golden", "silver", "iron", "copper",
"cobalt", "jade", "amber", "arctic", "lunar", "solar", "astral", "coral",
"misty", "foggy", "dusty", "rusty", "mossy", "stormy", "cloudy", "windy",
"gentle", "fierce", "steady", "rapid", "lazy", "bold", "brave", "calm",
"clever", "cryptic", "cunning", "eager", "faint", "grave", "keen", "noble",
"prime", "rare", "stark", "terse", "vivid", "wary", "zealous", "agile",
"blunt", "coarse", "dense", "eerie", "fleet", "gaunt", "harsh", "lucid",
"muted", "numb", "opaque", "plain", "rigid", "sleek", "taut", "urban",
"vacant", "woven", "binary", "cipher", "delta", "echo", "foxtrot", "gamma",
"hex", "index", "kilo", "lambda", "micro", "nano", "omega", "proxy",
"quantum", "rogue", "sigma", "theta", "ultra", "vector", "xray", "zero",
]
NOUNS = [
"phantom", "spectre", "wraith", "shade", "ghost", "echo", "void", "rift",
"nexus", "pulse", "signal", "cipher", "prism", "beacon", "aegis", "bastion",
"citadel", "forge", "haven", "vault", "harbor", "summit", "ridge", "canyon",
"glacier", "tundra", "steppe", "mesa", "delta", "fjord", "grove", "marsh",
"oasis", "reef", "shoal", "brook", "creek", "falls", "rapids", "spring",
"falcon", "raven", "hawk", "condor", "osprey", "heron", "crane", "wren",
"finch", "swift", "sparrow", "robin", "wolf", "fox", "lynx", "panther",
"tiger", "cobra", "viper", "mantis", "hornet", "spider", "scorpion",
"anchor", "arrow", "blade", "bolt", "chain", "crown", "flint", "glyph",
"helm", "ingot", "jewel", "knot", "lance", "mast", "oar", "pike",
"quill", "rune", "shard", "thorn", "urn", "wand", "atlas", "core",
"dusk", "ember", "frost", "gale", "haze", "iris", "jade", "karma",
"lumen", "myth", "nova", "orbit", "pixel", "quest", "relay", "sage",
"trace", "unity", "vertex", "zenith",
]
def generate_id():
"""Generate a deployment ID with 10k+ unique combinations."""
adj = random.choice(ADJECTIVES)
noun = random.choice(NOUNS)
num = random.randint(10, 99)
return f"{adj}-{noun}-{num}"
# ─── SSH Key Generation ─────────────────────────────────────────────────────
def generate_ssh_key(deploy_id):
"""Generate an RSA 4096 SSH keypair for deployment."""
key_dir = LOGS_DIR / deploy_id
key_dir.mkdir(parents=True, exist_ok=True)
key_path = key_dir / f"deploy_{deploy_id}"
if key_path.exists():
info(f"SSH key already exists: {key_path}")
return str(key_path)
info(f"Generating SSH key: {key_path}")
subprocess.run(
["ssh-keygen", "-t", "rsa", "-b", "4096", "-f", str(key_path),
"-N", "", "-C", f"deploy-{deploy_id}"],
check=True, capture_output=True,
)
os.chmod(key_path, 0o600)
ok(f"SSH key generated: {key_path}")
return str(key_path)
# ─── Ansible Playbook Execution ─────────────────────────────────────────────
def run_playbook(playbook_path, config, extra_vars=None):
"""Execute an Ansible playbook with the given config.
Args:
playbook_path: Path object or string to the playbook file.
config: Deployment config dict.
extra_vars: Optional dict of extra Ansible variables.
Returns:
True if playbook succeeded, False otherwise.
"""
playbook_path = Path(playbook_path)
if not playbook_path.exists():
err(f"Playbook not found: {playbook_path}")
return False
cmd = ["ansible-playbook", str(playbook_path)]
# Build vars file
vars_file = LOGS_DIR / config["deploy_id"] / "vars.yaml"
vars_file.parent.mkdir(parents=True, exist_ok=True)
try:
import yaml # noqa: optional dependency
with open(vars_file, "w") as f:
yaml.dump(config, f, default_flow_style=False)
except ImportError:
with open(vars_file, "w") as f:
for k, v in config.items():
f.write(f"{k}: {json.dumps(v)}\n")
cmd.extend(["-e", f"@{vars_file}"])
if extra_vars:
for k, v in extra_vars.items():
cmd.extend(["-e", f"{k}={v}"])
# Set inventory
target = config.get("target_host", "localhost")
if target == "localhost":
cmd.extend(["-i", "localhost,", "--connection", "local"])
else:
inv_file = LOGS_DIR / config["deploy_id"] / "inventory"
with open(inv_file, "w") as f:
ssh_key = config.get("ssh_key", "")
ssh_user = config.get("ssh_user", "root")
line = f"{target} ansible_user={ssh_user}"
if ssh_key:
line += f" ansible_ssh_private_key_file={ssh_key}"
f.write(f"[servers]\n{line}\n")
cmd.extend(["-i", str(inv_file)])
# Elevate privileges for non-root users
if ssh_user != "root":
cmd.append("--become")
info(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd)
return result.returncode == 0
# ─── Deployment Info Logging ────────────────────────────────────────────────
def save_deploy_info(config):
"""Save deployment metadata to logs."""
deploy_dir = LOGS_DIR / config["deploy_id"]
deploy_dir.mkdir(parents=True, exist_ok=True)
info_file = deploy_dir / "deploy_info.json"
config["timestamp"] = time.strftime("%Y-%m-%dT%H:%M:%S%z")
with open(info_file, "w") as f:
json.dump(config, f, indent=2, default=str)
ok(f"Deployment info saved: {info_file}")
# ─── SSH Connect ────────────────────────────────────────────────────────────
def ssh_connect(config):
"""Offer to SSH into the deployed server."""
target = config.get("target_host", "")
if not target or target == "localhost":
return
ssh_key = config.get("ssh_key", "")
ssh_user = config.get("ssh_user", "root")
cmd = ["ssh"]
if ssh_key:
cmd.extend(["-i", ssh_key])
cmd.append(f"{ssh_user}@{target}")
print()
resp = input(f"{CYAN}[?]{RESET} SSH into {target}? [y/N] ").strip().lower()
if resp == "y":
os.execvp("ssh", cmd)
# ─── Provider Selection ─────────────────────────────────────────────────────
def select_provider():
"""Select deployment target: cloud provider or local/existing server."""
print(f"\n{CYAN} Select deployment target:{RESET}")
print(f" {WHITE}1{RESET}) Linode")
print(f" {WHITE}2{RESET}) AWS (EC2)")
print(f" {WHITE}3{RESET}) FlokiNET (pre-provisioned)")
print(f" {WHITE}4{RESET}) Existing server (SSH)")
print(f" {WHITE}5{RESET}) Local (this machine)")
print()
choice = input(f" {MAGENTA}>{RESET} ").strip()
providers = {"1": "linode", "2": "aws", "3": "flokinet", "4": "existing", "5": "local"}
return providers.get(choice)
def gather_credentials(provider, config):
"""Gather provider-specific credentials."""
if provider == "linode":
config["provider"] = "linode"
config["api_token"] = input(f" {CYAN}Linode API token:{RESET} ").strip()
config["region"] = input(f" {CYAN}Region (e.g. us-east):{RESET} ").strip() or "us-east"
config["plan"] = input(f" {CYAN}Plan (e.g. g6-nanode-1):{RESET} ").strip() or "g6-nanode-1"
elif provider == "aws":
config["provider"] = "aws"
config["aws_access_key"] = input(f" {CYAN}AWS Access Key ID:{RESET} ").strip()
config["aws_secret_key"] = input(f" {CYAN}AWS Secret Access Key:{RESET} ").strip()
config["region"] = input(f" {CYAN}Region (e.g. us-east-1):{RESET} ").strip() or "us-east-1"
config["instance_type"] = input(f" {CYAN}Instance type (e.g. t3.micro):{RESET} ").strip() or "t3.micro"
elif provider == "flokinet":
config["provider"] = "flokinet"
config["target_host"] = input(f" {CYAN}Server IP:{RESET} ").strip()
config["ssh_user"] = input(f" {CYAN}SSH user [root]:{RESET} ").strip() or "root"
elif provider == "existing":
config["provider"] = "existing"
config["target_host"] = input(f" {CYAN}Server IP/hostname:{RESET} ").strip()
config["ssh_user"] = input(f" {CYAN}SSH user [root]:{RESET} ").strip() or "root"
existing_key = input(f" {CYAN}SSH key path (blank to generate):{RESET} ").strip()
if existing_key:
config["ssh_key"] = existing_key
elif provider == "local":
config["provider"] = "local"
config["target_host"] = "localhost"
config["ssh_user"] = os.getenv("USER", "root")
warn("Local deployment will install services directly on this machine.")
confirm = input(f" {YELLOW}Continue? [y/N]:{RESET} ").strip().lower()
if confirm != "y":
return False
return True
# ─── Deployment Orchestration ───────────────────────────────────────────────
def deploy(server_type, config):
"""Full deployment pipeline: summarize -> confirm -> provision -> configure."""
config["server_type"] = server_type
config.setdefault("deploy_id", generate_id())
deploy_id = config["deploy_id"]
# Summary
print(f"\n{CYAN}{'' * 60}{RESET}")
print(f"{MAGENTA} Deployment Summary{RESET}")
print(f"{CYAN}{'' * 60}{RESET}")
print(f" {WHITE}ID:{RESET} {deploy_id}")
print(f" {WHITE}Type:{RESET} {server_type}")
print(f" {WHITE}Provider:{RESET} {config.get('provider', 'unknown')}")
print(f" {WHITE}Target:{RESET} {config.get('target_host', 'TBD (will provision)')}")
if config.get("domain"):
print(f" {WHITE}Domain:{RESET} {config['domain']}")
for k, v in config.items():
if k not in ("deploy_id", "server_type", "provider", "target_host",
"domain", "api_token", "aws_access_key", "aws_secret_key",
"ssh_key", "ssh_user", "timestamp"):
print(f" {WHITE}{k}:{RESET} {v}")
print(f"{CYAN}{'' * 60}{RESET}")
confirm = input(f"\n {MAGENTA}Deploy? [y/N]:{RESET} ").strip().lower()
if confirm != "y":
warn("Deployment cancelled.")
return
# Generate SSH key if needed
if config.get("provider") not in ("local",) and not config.get("ssh_key"):
config["ssh_key"] = generate_ssh_key(deploy_id)
# Provision if cloud provider
if config.get("provider") in ("linode", "aws"):
provider_playbook = PROVIDERS_DIR / f"{config['provider']}.yml"
# Tell the provider playbook where to write the provisioned IP
host_file = LOGS_DIR / deploy_id / "provisioned_host"
config["_host_output_file"] = str(host_file)
info(f"Provisioning {config['provider']} instance...")
if not run_playbook(provider_playbook, config):
err("Provisioning failed.")
return
# Read back the provisioned host IP
if host_file.exists():
config["target_host"] = host_file.read_text().strip()
ok(f"Provisioned server: {config['target_host']}")
else:
err("Provisioning completed but no host IP was returned.")
return
# Base hardening
info("Applying base hardening...")
if not run_playbook(PLAYBOOKS_DIR / "common/base_hardening.yml", config):
err("Base hardening failed — aborting deployment.")
return
# Service-specific playbook
playbook_map = {
"matrix": PLAYBOOKS_DIR / "matrix/main.yml",
"vpn": PLAYBOOKS_DIR / "vpn/main.yml",
"dns": PLAYBOOKS_DIR / "dns/main.yml",
"cloud": PLAYBOOKS_DIR / "cloud/main.yml",
"vault": PLAYBOOKS_DIR / "vault/main.yml",
"media": PLAYBOOKS_DIR / "media/main.yml",
"email": PLAYBOOKS_DIR / "email/main.yml",
"all_in_one": PLAYBOOKS_DIR / "all_in_one/main.yml",
}
playbook = playbook_map.get(server_type)
if playbook:
info(f"Configuring {server_type}...")
if run_playbook(playbook, config):
ok(f"Deployment complete: {deploy_id}")
else:
err(f"Service configuration failed for {server_type}")
return
# Save info and offer SSH
save_deploy_info(config)
ssh_connect(config)
# ─── Main Menu ──────────────────────────────────────────────────────────────
MENU_ITEMS = [
("1", "Matrix + Element", "matrix", "Encrypted messaging homeserver"),
("2", "WireGuard VPN", "vpn", "Private VPN server"),
("3", "Pi-hole DNS", "dns", "Ad-blocking DNS server"),
("4", "Nextcloud", "cloud", "Self-hosted file sync (coming soon)"),
("5", "Vaultwarden", "vault", "Password manager (coming soon)"),
("6", "Jellyfin", "media", "Media server (coming soon)"),
("7", "Mail-in-a-Box", "email", "Email server (coming soon)"),
("8", "All-in-One", "all_in_one", "Multiple services on one server"),
("0", "Exit", None, None),
]
def main_menu():
banner()
while True:
print(f"\n{CYAN} ┌─ Deploy a Privacy Server ──────────────────────────┐{RESET}")
for num, label, _, desc in MENU_ITEMS:
if desc:
print(f" {CYAN}{RESET} {WHITE}{num}{RESET}) {label:<20} {GREY}{desc}{RESET}")
else:
print(f" {CYAN}{RESET} {WHITE}{num}{RESET}) {label}")
print(f" {CYAN}└─────────────────────────────────────────────────────┘{RESET}")
choice = input(f"\n {MAGENTA}>{RESET} ").strip()
if choice == "0":
print(f"\n{GREY} Goodbye.{RESET}\n")
break
# Find matching menu item
selected = None
for num, label, stype, _ in MENU_ITEMS:
if choice == num and stype:
selected = stype
break
if not selected:
warn("Invalid selection.")
continue
# Import the module
try:
mod = __import__(f"modules.{selected}", fromlist=[selected])
except ImportError as e:
err(f"Module not found: {selected} ({e})")
continue
# Select provider
provider = select_provider()
if not provider:
warn("Invalid provider selection.")
continue
config = {"deploy_id": generate_id()}
if not gather_credentials(provider, config):
continue
# Gather service-specific config
if hasattr(mod, "gather_config"):
config = mod.gather_config(config)
if config is None:
continue
# Deploy
deploy(selected, config)
if __name__ == "__main__":
if len(sys.argv) > 1 and sys.argv[1] in ("--help", "-h"):
print("Usage: python3 phantom.py")
print(" Interactive privacy server deployer.")
print(" Supports Matrix, WireGuard, Pi-hole, and more.")
print(" Run without arguments to launch the interactive menu.")
sys.exit(0)
try:
main_menu()
except KeyboardInterrupt:
print(f"\n{GREY} Interrupted.{RESET}\n")
+92
View File
@@ -0,0 +1,92 @@
---
# All-in-One deployment — multiple services on a single server
# Installs nginx as reverse proxy with TLS termination via Certbot,
# then deploys selected services behind vhost routing.
#
# NOTE: This playbook includes task files directly rather than
# importing full playbooks, to allow conditional composition.
- name: All-in-One Privacy Server
hosts: all
become: true
vars:
base_domain: "{{ domain }}"
selected_services: "{{ services | default([]) }}"
certbot_email: "{{ certbot_email | default('') }}"
target_host: "{{ target_host | default('localhost') }}"
tasks:
# ─── Base: Nginx + Certbot ────────────────────────────────────────
- name: Install nginx and certbot
apt:
name:
- nginx
- certbot
- python3-certbot-nginx
state: present
- name: Remove default nginx site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: reload nginx
- name: Allow HTTP/HTTPS through UFW
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
# ─── Deploy Individual Services (task files, not full playbooks) ──
- name: Deploy Matrix — Synapse
include_tasks: "{{ playbook_dir }}/../matrix/tasks/synapse.yml"
when: "'matrix' in selected_services"
- name: Deploy Matrix — Element Web
include_tasks: "{{ playbook_dir }}/../matrix/tasks/element.yml"
when: "'matrix' in selected_services and (matrix_element_web | default(true) | bool)"
- name: Deploy Matrix — Nginx vhost
include_tasks: "{{ playbook_dir }}/../matrix/tasks/nginx.yml"
when: "'matrix' in selected_services"
- name: Deploy WireGuard — Install
include_tasks: "{{ playbook_dir }}/../vpn/tasks/install.yml"
when: "'vpn' in selected_services"
- name: Deploy WireGuard — Configure
include_tasks: "{{ playbook_dir }}/../vpn/tasks/configure.yml"
when: "'vpn' in selected_services"
- name: Deploy Pi-hole — Install
include_tasks: "{{ playbook_dir }}/../dns/tasks/install.yml"
when: "'dns' in selected_services"
- name: Deploy Pi-hole — Configure
include_tasks: "{{ playbook_dir }}/../dns/tasks/configure.yml"
when: "'dns' in selected_services"
# Stub services — print notice
- name: Notice for stub services
debug:
msg: "Service '{{ item }}' playbook not yet implemented — skipping"
loop: "{{ selected_services | select('in', ['cloud', 'vault', 'media', 'email']) | list }}"
handlers:
- name: reload nginx
service:
name: nginx
state: reloaded
- name: restart synapse
service:
name: matrix-synapse
state: restarted
- name: restart sshd
service:
name: sshd
state: restarted
+11
View File
@@ -0,0 +1,11 @@
---
# Cloud deployment — Coming soon
# This is a stub playbook. Full implementation planned.
- name: Deploy Cloud Server
hosts: all
become: true
tasks:
- name: Placeholder
debug:
msg: "Cloud playbook not yet implemented. Check phantom/README.md for status."
+144
View File
@@ -0,0 +1,144 @@
---
# Base server hardening — applied to all deployments
# Covers: updates, firewall, fail2ban, SSH hardening
- name: Base Server Hardening
hosts: all
become: true
vars:
ssh_port: "{{ ssh_port | default(22) }}"
ssh_allow_password: false
tasks:
# ─── System Updates ─────────────────────────────────────────────────
- name: Update apt cache
apt:
update_cache: true
cache_valid_time: 3600
when: ansible_os_family == "Debian"
- name: Upgrade all packages
apt:
upgrade: safe
when: ansible_os_family == "Debian"
- name: Install essential packages
apt:
name:
- ufw
- fail2ban
- unattended-upgrades
- apt-listchanges
- curl
- wget
- gnupg
- ca-certificates
- software-properties-common
state: present
when: ansible_os_family == "Debian"
# ─── UFW Firewall ───────────────────────────────────────────────────
- name: Set UFW default deny incoming
ufw:
direction: incoming
policy: deny
- name: Set UFW default allow outgoing
ufw:
direction: outgoing
policy: allow
- name: Allow SSH through UFW
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
- name: Enable UFW
ufw:
state: enabled
# ─── Fail2ban ───────────────────────────────────────────────────────
- name: Configure fail2ban SSH jail
copy:
dest: /etc/fail2ban/jail.local
content: |
[sshd]
enabled = true
port = {{ ssh_port }}
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 3600
findtime = 600
mode: "0644"
notify: restart fail2ban
- name: Enable fail2ban
service:
name: fail2ban
state: started
enabled: true
# ─── SSH Hardening ──────────────────────────────────────────────────
- name: Disable SSH password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PasswordAuthentication"
line: "PasswordAuthentication no"
when: not ssh_allow_password
notify: restart sshd
- name: Disable SSH root login with password
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?PermitRootLogin"
line: "PermitRootLogin prohibit-password"
notify: restart sshd
- name: Disable SSH X11 forwarding
lineinfile:
path: /etc/ssh/sshd_config
regexp: "^#?X11Forwarding"
line: "X11Forwarding no"
notify: restart sshd
# ─── Automatic Security Updates ─────────────────────────────────────
- name: Enable unattended upgrades for security
copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
APT::Periodic::AutocleanInterval "7";
mode: "0644"
when: ansible_os_family == "Debian"
# ─── Kernel Hardening ───────────────────────────────────────────────
- name: Apply sysctl hardening
sysctl:
name: "{{ item.key }}"
value: "{{ item.value }}"
sysctl_set: true
reload: true
loop:
- { key: "net.ipv4.conf.all.rp_filter", value: "1" }
- { key: "net.ipv4.conf.default.rp_filter", value: "1" }
- { key: "net.ipv4.icmp_echo_ignore_broadcasts", value: "1" }
- { key: "net.ipv4.conf.all.accept_redirects", value: "0" }
- { key: "net.ipv4.conf.default.accept_redirects", value: "0" }
- { key: "net.ipv6.conf.all.accept_redirects", value: "0" }
- { key: "net.ipv6.conf.default.accept_redirects", value: "0" }
- { key: "net.ipv4.conf.all.send_redirects", value: "0" }
- { key: "net.ipv4.conf.default.send_redirects", value: "0" }
handlers:
- name: restart fail2ban
service:
name: fail2ban
state: restarted
- name: restart sshd
service:
name: sshd
state: restarted
+18
View File
@@ -0,0 +1,18 @@
---
# Pi-hole DNS server deployment (Docker-based)
- name: Deploy Pi-hole DNS Server
hosts: all
become: true
vars:
pihole_upstream: "{{ dns_upstream | default('9.9.9.9;149.112.112.112') }}"
pihole_domain: "{{ dns_domain | default('') }}"
pihole_blocklist: "{{ dns_blocklist | default('standard') }}"
target_host: "{{ target_host | default('localhost') }}"
tasks:
- name: Include Pi-hole installation
include_tasks: tasks/install.yml
- name: Include Pi-hole configuration
include_tasks: tasks/configure.yml
+66
View File
@@ -0,0 +1,66 @@
---
# Pi-hole Docker configuration and launch
- name: Create Pi-hole directories
file:
path: "{{ item }}"
state: directory
mode: "0755"
loop:
- /opt/pihole
- /opt/pihole/etc-pihole
- /opt/pihole/etc-dnsmasq.d
- name: Generate Pi-hole admin password
shell: "openssl rand -base64 16"
register: pihole_password
args:
creates: /opt/pihole/.password
- name: Save admin password
copy:
content: "{{ pihole_password.stdout }}"
dest: /opt/pihole/.password
mode: "0600"
when: pihole_password.changed
- name: Deploy Pi-hole Docker Compose
copy:
dest: /opt/pihole/docker-compose.yml
content: |
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
environment:
TZ: UTC
WEBPASSWORD_FILE: /run/secrets/webpassword
PIHOLE_DNS_: "{{ pihole_upstream }}"
DNSSEC: "true"
QUERY_LOGGING: "false"
volumes:
- /opt/pihole/etc-pihole:/etc/pihole
- /opt/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
secrets:
- webpassword
restart: unless-stopped
dns:
- 127.0.0.1
- 9.9.9.9
secrets:
webpassword:
file: /opt/pihole/.password
mode: "0644"
- name: Start Pi-hole
shell: cd /opt/pihole && docker compose up -d
args:
creates: /opt/pihole/etc-pihole/pihole-FTL.db
- name: Display admin password
debug:
msg: "Pi-hole admin password: {{ pihole_password.stdout | default('(see /opt/pihole/.password)') }}"
+62
View File
@@ -0,0 +1,62 @@
---
# Pi-hole installation via Docker
- name: Install Docker prerequisites
apt:
name:
- apt-transport-https
- ca-certificates
- curl
- gnupg
- lsb-release
state: present
- name: Add Docker GPG key
apt_key:
url: https://download.docker.com/linux/{{ ansible_distribution | lower }}/gpg
state: present
- name: Add Docker repository
apt_repository:
repo: "deb https://download.docker.com/linux/{{ ansible_distribution | lower }} {{ ansible_distribution_release }} stable"
state: present
- name: Install Docker
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-compose-plugin
state: present
- name: Enable Docker service
service:
name: docker
state: started
enabled: true
- name: Stop systemd-resolved (conflicts with Pi-hole on port 53)
service:
name: systemd-resolved
state: stopped
enabled: false
failed_when: false
- name: Set DNS fallback
copy:
dest: /etc/resolv.conf
content: |
nameserver 9.9.9.9
nameserver 1.1.1.1
mode: "0644"
- name: Allow DNS through UFW
ufw:
rule: allow
port: "{{ item.port }}"
proto: "{{ item.proto }}"
loop:
- { port: "53", proto: "tcp" }
- { port: "53", proto: "udp" }
- { port: "80", proto: "tcp" }
+11
View File
@@ -0,0 +1,11 @@
---
# Email deployment — Coming soon
# This is a stub playbook. Full implementation planned.
- name: Deploy Email Server
hosts: all
become: true
tasks:
- name: Placeholder
debug:
msg: "Email playbook not yet implemented. Check phantom/README.md for status."
+38
View File
@@ -0,0 +1,38 @@
---
# Matrix (Synapse) + Element Web deployment
# Installs Synapse homeserver with optional Element Web frontend
- name: Deploy Matrix Homeserver
hosts: all
become: true
vars:
matrix_domain: "{{ domain }}"
matrix_server_name: "{{ matrix_server_name | default(domain) }}"
matrix_admin: "{{ matrix_admin_user | default('admin') }}"
matrix_admin_pass: "{{ matrix_admin_password }}"
matrix_registration_enabled: "{{ matrix_registration | default(false) }}"
element_enabled: "{{ matrix_element_web | default(true) }}"
matrix_signing_key: "{{ matrix_signing_key }}"
target_host: "{{ target_host | default('localhost') }}"
tasks:
- name: Include Synapse installation tasks
include_tasks: tasks/synapse.yml
- name: Include Element Web tasks
include_tasks: tasks/element.yml
when: element_enabled | bool
- name: Include nginx reverse proxy tasks
include_tasks: tasks/nginx.yml
handlers:
- name: restart synapse
service:
name: matrix-synapse
state: restarted
- name: reload nginx
service:
name: nginx
state: reloaded
@@ -0,0 +1,44 @@
---
# Element Web frontend installation
- name: Install nginx (if not already)
apt:
name: nginx
state: present
- name: Create Element Web directory
file:
path: /var/www/element
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Download latest Element Web release
shell: |
LATEST=$(curl -s https://api.github.com/repos/element-hq/element-web/releases/latest | grep -oP '"tag_name": "\K[^"]+')
curl -sL "https://github.com/element-hq/element-web/releases/download/${LATEST}/element-${LATEST}.tar.gz" | tar xz -C /var/www/element --strip-components=1
args:
creates: /var/www/element/index.html
- name: Deploy Element Web config
copy:
dest: /var/www/element/config.json
content: |
{
"default_server_config": {
"m.homeserver": {
"base_url": "https://{{ matrix_domain }}",
"server_name": "{{ matrix_server_name }}"
}
},
"brand": "Element",
"integrations_ui_url": "",
"integrations_rest_url": "",
"disable_guests": true,
"disable_3pid_login": false,
"default_theme": "dark"
}
owner: www-data
group: www-data
mode: "0644"
+115
View File
@@ -0,0 +1,115 @@
---
# Nginx reverse proxy for Matrix + Element
# Two-phase: HTTP-only first for certbot, then full SSL config
- name: Create certbot webroot
file:
path: /var/www/certbot
state: directory
owner: www-data
group: www-data
mode: "0755"
- name: Deploy HTTP-only nginx config (for certbot)
copy:
dest: /etc/nginx/sites-available/matrix
content: |
server {
listen 80;
server_name {{ matrix_domain }};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 200 'phantom setup in progress';
add_header Content-Type text/plain;
}
}
mode: "0644"
notify: reload nginx
- name: Enable Matrix nginx site
file:
src: /etc/nginx/sites-available/matrix
dest: /etc/nginx/sites-enabled/matrix
state: link
notify: reload nginx
- name: Flush handlers to apply HTTP config
meta: flush_handlers
- name: Obtain TLS certificate
command: >
certbot certonly --webroot -w /var/www/certbot
-d {{ matrix_domain }}
--non-interactive
--agree-tos
{% if certbot_email is defined and certbot_email %}
--email {{ certbot_email }}
{% else %}
--register-unsafely-without-email
{% endif %}
args:
creates: "/etc/letsencrypt/live/{{ matrix_domain }}/fullchain.pem"
- name: Deploy full SSL nginx config
copy:
dest: /etc/nginx/sites-available/matrix
content: |
server {
listen 80;
server_name {{ matrix_domain }};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl http2;
listen 8448 ssl http2;
server_name {{ matrix_domain }};
ssl_certificate /etc/letsencrypt/live/{{ matrix_domain }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ matrix_domain }}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
client_max_body_size 50m;
# Synapse
location ~* ^(\/_matrix|\/_synapse\/client) {
proxy_pass http://127.0.0.1:8008;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
}
# .well-known delegation
location /.well-known/matrix/server {
return 200 '{"m.server": "{{ matrix_domain }}:443"}';
add_header Content-Type application/json;
}
location /.well-known/matrix/client {
return 200 '{"m.homeserver": {"base_url": "https://{{ matrix_domain }}"}}';
add_header Content-Type application/json;
add_header Access-Control-Allow-Origin *;
}
# Element Web (if enabled)
location / {
root /var/www/element;
try_files $uri $uri/ /index.html;
}
}
mode: "0644"
notify: reload nginx
@@ -0,0 +1,99 @@
---
# Synapse homeserver installation and configuration
- name: Install dependencies
apt:
name:
- python3
- python3-pip
- python3-venv
- libpq-dev
- postgresql
- postgresql-contrib
- nginx
- certbot
- python3-certbot-nginx
state: present
- name: Add Matrix Synapse apt repository key
apt_key:
url: https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
state: present
- name: Add Matrix Synapse apt repository
apt_repository:
repo: "deb https://packages.matrix.org/debian/ {{ ansible_distribution_release }} main"
state: present
filename: matrix-org
- name: Install Synapse
apt:
name: matrix-synapse-py3
state: present
environment:
DEBIAN_FRONTEND: noninteractive
- name: Create PostgreSQL database
become_user: postgres
postgresql_db:
name: synapse
encoding: UTF-8
lc_collate: C
lc_ctype: C
template: template0
- name: Create PostgreSQL user
become_user: postgres
postgresql_user:
name: synapse
password: "{{ matrix_signing_key[:32] }}"
db: synapse
priv: ALL
- name: Deploy Synapse homeserver config
template:
src: ../templates/homeserver.yaml.j2
dest: /etc/matrix-synapse/homeserver.yaml
owner: matrix-synapse
group: matrix-synapse
mode: "0640"
notify: restart synapse
- name: Allow Matrix federation port through UFW
ufw:
rule: allow
port: "8448"
proto: tcp
- name: Allow HTTP/HTTPS through UFW
ufw:
rule: allow
port: "{{ item }}"
proto: tcp
loop:
- "80"
- "443"
- name: Enable and start Synapse
service:
name: matrix-synapse
state: started
enabled: true
- name: Wait for Synapse to be ready
wait_for:
port: 8008
host: 127.0.0.1
timeout: 30
- name: Create admin user
command: >
register_new_matrix_user
-u {{ matrix_admin }}
-p {{ matrix_admin_pass }}
-a
-c /etc/matrix-synapse/homeserver.yaml
http://localhost:8008
register: admin_create
failed_when: false
changed_when: admin_create.rc == 0
@@ -0,0 +1,53 @@
## Synapse Homeserver Configuration
## Generated by phantom
server_name: "{{ matrix_server_name }}"
pid_file: /run/matrix-synapse.pid
listeners:
- port: 8008
tls: false
type: http
x_forwarded: true
resources:
- names: [client, federation]
compress: false
database:
name: psycopg2
args:
user: synapse
password: "{{ matrix_signing_key[:32] }}"
database: synapse
host: localhost
cp_min: 5
cp_max: 10
log_config: "/etc/matrix-synapse/log.yaml"
media_store_path: /var/lib/matrix-synapse/media
signing_key_path: "/etc/matrix-synapse/{{ matrix_server_name }}.signing.key"
enable_registration: {{ matrix_registration_enabled | lower }}
{% if not matrix_registration_enabled %}
enable_registration_without_verification: false
{% endif %}
suppress_key_server_warning: true
trusted_key_servers:
- server_name: "matrix.org"
max_upload_size: 50M
url_preview_enabled: true
url_preview_ip_range_blacklist:
- '127.0.0.0/8'
- '10.0.0.0/8'
- '172.16.0.0/12'
- '192.168.0.0/16'
- '100.64.0.0/10'
- '192.0.0.0/24'
- '169.254.0.0/16'
- '::1/128'
- 'fe80::/10'
- 'fc00::/7'
+11
View File
@@ -0,0 +1,11 @@
---
# Media deployment — Coming soon
# This is a stub playbook. Full implementation planned.
- name: Deploy Media Server
hosts: all
become: true
tasks:
- name: Placeholder
debug:
msg: "Media playbook not yet implemented. Check phantom/README.md for status."
+11
View File
@@ -0,0 +1,11 @@
---
# Vault deployment — Coming soon
# This is a stub playbook. Full implementation planned.
- name: Deploy Vault Server
hosts: all
become: true
tasks:
- name: Placeholder
debug:
msg: "Vault playbook not yet implemented. Check phantom/README.md for status."
+20
View File
@@ -0,0 +1,20 @@
---
# WireGuard VPN server deployment
- name: Deploy WireGuard VPN Server
hosts: all
become: true
vars:
wg_port: "{{ vpn_port | default(51820) }}"
wg_clients: "{{ vpn_client_count | default(3) }}"
wg_dns: "{{ vpn_dns | default('1.1.1.1') }}"
wg_subnet: "{{ vpn_subnet | default('10.66.66.0/24') }}"
wg_allowed_ips: "{{ vpn_allowed_ips | default('0.0.0.0/0, ::/0') }}"
target_host: "{{ target_host | default('localhost') }}"
tasks:
- name: Include WireGuard installation tasks
include_tasks: tasks/install.yml
- name: Include WireGuard configuration tasks
include_tasks: tasks/configure.yml
+88
View File
@@ -0,0 +1,88 @@
---
# WireGuard server and client configuration
- name: Detect primary network interface
shell: ip route get 1.1.1.1 | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}'
register: primary_interface
changed_when: false
- name: Generate client keys
shell: |
mkdir -p /etc/wireguard/clients
for i in $(seq 1 {{ wg_clients }}); do
if [ ! -f /etc/wireguard/clients/client${i}_private.key ]; then
wg genkey | tee /etc/wireguard/clients/client${i}_private.key | wg pubkey > /etc/wireguard/clients/client${i}_public.key
wg genpsk > /etc/wireguard/clients/client${i}_psk.key
chmod 600 /etc/wireguard/clients/client${i}_*.key
fi
done
args:
creates: /etc/wireguard/clients/client1_private.key
- name: Build WireGuard server config
shell: |
SERVER_PRIVKEY=$(cat /etc/wireguard/server_private.key)
IFACE={{ primary_interface.stdout | trim }}
cat > /etc/wireguard/wg0.conf << EOF
[Interface]
Address = {{ wg_subnet | regex_replace('/\\d+$', '') | regex_replace('\\.[0-9]+$', '.1/24') }}
ListenPort = {{ wg_port }}
PrivateKey = ${SERVER_PRIVKEY}
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -o ${IFACE} -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT; iptables -t nat -D POSTROUTING -o ${IFACE} -j MASQUERADE
EOF
for i in $(seq 1 {{ wg_clients }}); do
CLIENT_PUBKEY=$(cat /etc/wireguard/clients/client${i}_public.key)
CLIENT_PSK=$(cat /etc/wireguard/clients/client${i}_psk.key)
cat >> /etc/wireguard/wg0.conf << EOF
[Peer]
# Client ${i}
PublicKey = ${CLIENT_PUBKEY}
PresharedKey = ${CLIENT_PSK}
AllowedIPs = {{ wg_subnet | regex_replace('/\\d+$', '') | regex_replace('\\.[0-9]+$', '') }}.$(( i + 1 ))/32
EOF
done
chmod 600 /etc/wireguard/wg0.conf
args:
creates: /etc/wireguard/wg0.conf
- name: Generate client config files
shell: |
SERVER_PUBKEY=$(echo '{{ server_privkey_content.content | b64decode | trim }}' | wg pubkey)
SERVER_ENDPOINT="{{ target_host }}:{{ wg_port }}"
for i in $(seq 1 {{ wg_clients }}); do
CLIENT_PRIVKEY=$(cat /etc/wireguard/clients/client${i}_private.key)
CLIENT_PSK=$(cat /etc/wireguard/clients/client${i}_psk.key)
CLIENT_IP="{{ wg_subnet | regex_replace('/\\d+$', '') | regex_replace('\\.[0-9]+$', '') }}.$(( i + 1 ))"
cat > /etc/wireguard/clients/client${i}.conf << EOF
[Interface]
PrivateKey = ${CLIENT_PRIVKEY}
Address = ${CLIENT_IP}/32
DNS = {{ wg_dns }}
[Peer]
PublicKey = ${SERVER_PUBKEY}
PresharedKey = ${CLIENT_PSK}
Endpoint = ${SERVER_ENDPOINT}
AllowedIPs = {{ wg_allowed_ips }}
PersistentKeepalive = 25
EOF
# Generate QR code
qrencode -t ansiutf8 < /etc/wireguard/clients/client${i}.conf > /etc/wireguard/clients/client${i}_qr.txt 2>/dev/null || true
done
args:
creates: /etc/wireguard/clients/client1.conf
- name: Enable and start WireGuard
systemd:
name: wg-quick@wg0
state: started
enabled: true
+54
View File
@@ -0,0 +1,54 @@
---
# WireGuard installation and server key generation
- name: Install WireGuard
apt:
name:
- wireguard
- wireguard-tools
- qrencode
state: present
when: ansible_os_family == "Debian"
- name: Enable IP forwarding (IPv4)
sysctl:
name: net.ipv4.ip_forward
value: "1"
sysctl_set: true
reload: true
- name: Enable IP forwarding (IPv6)
sysctl:
name: net.ipv6.conf.all.forwarding
value: "1"
sysctl_set: true
reload: true
- name: Generate server private key
shell: wg genkey
register: wg_server_privkey
args:
creates: /etc/wireguard/server_private.key
- name: Save server private key
copy:
content: "{{ wg_server_privkey.stdout }}"
dest: /etc/wireguard/server_private.key
mode: "0600"
when: wg_server_privkey.changed
- name: Read server private key
slurp:
src: /etc/wireguard/server_private.key
register: server_privkey_content
- name: Generate server public key
shell: "echo '{{ server_privkey_content.content | b64decode | trim }}' | wg pubkey"
register: wg_server_pubkey
changed_when: false
- name: Allow WireGuard port through UFW
ufw:
rule: allow
port: "{{ wg_port }}"
proto: udp
+105
View File
@@ -0,0 +1,105 @@
---
# Provision an AWS EC2 instance for phantom deployment
# Requires: aws_access_key, aws_secret_key variables
- name: Provision AWS EC2 Instance
hosts: localhost
connection: local
gather_facts: false
vars:
aws_region: "{{ region | default('us-east-1') }}"
ec2_instance_type: "{{ instance_type | default('t3.micro') }}"
ec2_ami: "{{ ami | default('') }}"
ec2_key_name: "phantom-{{ deploy_id }}"
ssh_key_path: "{{ ssh_key }}.pub"
environment:
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
AWS_DEFAULT_REGION: "{{ aws_region }}"
tasks:
- name: Read SSH public key
slurp:
src: "{{ ssh_key_path }}"
register: ssh_pubkey
- name: Import SSH key to AWS
amazon.aws.ec2_key:
name: "{{ ec2_key_name }}"
key_material: "{{ ssh_pubkey.content | b64decode | trim }}"
region: "{{ aws_region }}"
- name: Find latest Debian AMI (if not specified)
amazon.aws.ec2_ami_info:
region: "{{ aws_region }}"
owners: ["136693071363"]
filters:
name: "debian-12-amd64-*"
architecture: x86_64
virtualization-type: hvm
register: ami_results
when: ec2_ami == ""
- name: Set AMI fact
set_fact:
ec2_ami: "{{ (ami_results.images | sort(attribute='creation_date') | last).image_id }}"
when: ec2_ami == ""
- name: Create security group
amazon.aws.ec2_security_group:
name: "phantom-{{ deploy_id }}"
description: "phantom deployment {{ deploy_id }}"
region: "{{ aws_region }}"
rules:
- proto: tcp
from_port: 22
to_port: 22
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 80
to_port: 80
cidr_ip: 0.0.0.0/0
- proto: tcp
from_port: 443
to_port: 443
cidr_ip: 0.0.0.0/0
register: sg_result
- name: Launch EC2 instance
amazon.aws.ec2_instance:
name: "phantom-{{ deploy_id }}"
key_name: "{{ ec2_key_name }}"
instance_type: "{{ ec2_instance_type }}"
image_id: "{{ ec2_ami }}"
region: "{{ aws_region }}"
security_group: "{{ sg_result.group_id }}"
wait: true
state: running
register: ec2_result
- name: Set target host fact
set_fact:
target_host: "{{ ec2_result.instances[0].public_ip_address }}"
- name: Display instance info
debug:
msg: |
EC2 instance provisioned:
ID: {{ ec2_result.instances[0].instance_id }}
IP: {{ target_host }}
Region: {{ aws_region }}
Type: {{ ec2_instance_type }}
- name: Write provisioned host IP for phantom
copy:
content: "{{ target_host }}"
dest: "{{ _host_output_file }}"
when: _host_output_file is defined
- name: Wait for SSH
wait_for:
host: "{{ target_host }}"
port: 22
delay: 15
timeout: 300
+30
View File
@@ -0,0 +1,30 @@
---
# Register a pre-provisioned FlokiNET server for phantom deployment
# FlokiNET servers are provisioned manually via their control panel.
# This playbook only validates SSH connectivity and prepares the server.
- name: Register FlokiNET Server
hosts: all
become: true
gather_facts: false
tasks:
- name: Ensure Python is available for Ansible
raw: test -e /usr/bin/python3 || (apt-get update && apt-get install -y python3)
changed_when: false
- name: Gather facts now that Python is available
setup:
- name: Verify SSH connectivity
ping:
- name: Display server info
debug:
msg: |
FlokiNET server registered:
Hostname: {{ ansible_hostname }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
IP: {{ ansible_default_ipv4.address | default(target_host) }}
RAM: {{ ansible_memtotal_mb }}MB
Cores: {{ ansible_processor_vcpus }}
+66
View File
@@ -0,0 +1,66 @@
---
# Provision a Linode instance for phantom deployment
# Requires: LINODE_API_TOKEN or api_token variable
- name: Provision Linode Instance
hosts: localhost
connection: local
gather_facts: false
vars:
linode_token: "{{ api_token }}"
linode_region: "{{ region | default('us-east') }}"
linode_plan: "{{ plan | default('g6-nanode-1') }}"
linode_image: "{{ image | default('linode/debian12') }}"
linode_label: "phantom-{{ deploy_id }}"
ssh_key_path: "{{ ssh_key }}.pub"
tasks:
- name: Read SSH public key
slurp:
src: "{{ ssh_key_path }}"
register: ssh_pubkey
- name: Create Linode instance
uri:
url: https://api.linode.com/v4/linode/instances
method: POST
headers:
Authorization: "Bearer {{ linode_token }}"
Content-Type: application/json
body_format: json
body:
type: "{{ linode_plan }}"
region: "{{ linode_region }}"
image: "{{ linode_image }}"
label: "{{ linode_label }}"
authorized_keys:
- "{{ ssh_pubkey.content | b64decode | trim }}"
booted: true
status_code: 200
register: linode_result
- name: Set target host fact
set_fact:
target_host: "{{ linode_result.json.ipv4[0] }}"
- name: Display instance info
debug:
msg: |
Linode provisioned:
ID: {{ linode_result.json.id }}
IP: {{ linode_result.json.ipv4[0] }}
Region: {{ linode_region }}
Plan: {{ linode_plan }}
- name: Write provisioned host IP for phantom
copy:
content: "{{ target_host }}"
dest: "{{ _host_output_file }}"
when: _host_output_file is defined
- name: Wait for SSH
wait_for:
host: "{{ target_host }}"
port: 22
delay: 10
timeout: 300