Initial release: ghost_protocol privacy toolkit
This commit is contained in:
+22
@@ -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
|
||||
@@ -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.
|
||||
@@ -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)
|
||||
@@ -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.
|
||||
Executable
+1409
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||
|
||||
======================================================================
|
||||
@@ -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
|
||||
|
||||
======================================================================
|
||||
@@ -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"
|
||||
|
||||
======================================================================
|
||||
Executable
+88
@@ -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
|
||||
Executable
+137
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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>
|
||||
Executable
+14
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
# Minimal stock Tor configuration
|
||||
# Restored by opsec-mode off
|
||||
|
||||
SocksPort 9050
|
||||
Log notice file /var/log/tor/notices.log
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+146
@@ -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
|
||||
Executable
+225
@@ -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}"
|
||||
@@ -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}
|
||||
]];
|
||||
Executable
+127
@@ -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)"
|
||||
Executable
+39
@@ -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
|
||||
Executable
+289
@@ -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 ""
|
||||
Executable
+717
@@ -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
|
||||
Executable
+35
@@ -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
|
||||
Executable
+320
@@ -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
|
||||
Executable
+82
@@ -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
|
||||
Executable
+76
@@ -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 ====="
|
||||
Executable
+1044
File diff suppressed because it is too large
Load Diff
Executable
+377
@@ -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
|
||||
Executable
+64
@@ -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}"
|
||||
Executable
+261
@@ -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
|
||||
Executable
+97
@@ -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"
|
||||
Executable
+25
@@ -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
|
||||
Executable
+1210
File diff suppressed because it is too large
Load Diff
Executable
+119
@@ -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
|
||||
Executable
+291
@@ -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
|
||||
Executable
+115
@@ -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
|
||||
Executable
+123
@@ -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 ""
|
||||
Executable
+23
@@ -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
|
||||
Executable
+180
@@ -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
|
||||
Executable
+127
@@ -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)"
|
||||
Executable
+120
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
[defaults]
|
||||
host_key_checking = False
|
||||
retry_files_enabled = False
|
||||
stdout_callback = yaml
|
||||
timeout = 30
|
||||
|
||||
[ssh_connection]
|
||||
pipelining = True
|
||||
@@ -0,0 +1 @@
|
||||
# phantom modules
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Executable
+463
@@ -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")
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)') }}"
|
||||
@@ -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" }
|
||||
@@ -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."
|
||||
@@ -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"
|
||||
@@ -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'
|
||||
@@ -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."
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user