From 973cede31f3545a01ddfd64d3515d95fc60b20b7 Mon Sep 17 00:00:00 2001 From: ghost Date: Wed, 4 Mar 2026 09:13:16 -0500 Subject: [PATCH] Initial release: ghost_protocol privacy toolkit --- .gitignore | 22 + LICENSE | 21 + README.md | 90 ++ covert_sd/README.md | 334 ++++ covert_sd/covert_sd_card_tool.py | 1409 +++++++++++++++++ covert_sd/tools/MOBILE_INSTRUCTIONS.txt | 239 +++ covert_sd/tools/README.txt | 194 +++ covert_sd/tools/WINDOWS_INSTRUCTIONS.txt | 93 ++ covert_sd/tools/lock_storage.sh | 88 + covert_sd/tools/mount_storage.sh | 137 ++ .../tools/veracrypt/DOWNLOAD_INSTRUCTIONS.md | 93 ++ opsec/README.md | 97 ++ opsec/configs/cron/opsec-banner-cache | 3 + opsec/configs/cron/opsec-log-rotate | 6 + opsec/configs/desktop/opsec-toggle.desktop | 9 + opsec/configs/levels/bare-metal-paranoid.conf | 85 + opsec/configs/levels/bare-metal-standard.conf | 86 + opsec/configs/levels/cloud-normal.conf | 85 + opsec/configs/levels/cloud-paranoid.conf | 84 + opsec/configs/opsec-aliases | 100 ++ opsec/configs/opsec-country-codes.conf | 28 + opsec/configs/opsec.conf | 96 ++ opsec/configs/polkit/com.opsec.mode.policy | 17 + opsec/configs/resolv.conf.head | 14 + opsec/configs/resolv.conf.opsec | 3 + .../systemd/opsec-boot-advanced.service | 18 + .../systemd/opsec-hostname-randomize.service | 14 + .../configs/systemd/opsec-killswitch.service | 15 + .../systemd/opsec-mac-randomize.service | 14 + opsec/configs/themes/aurora.theme | 14 + opsec/configs/themes/cyberpunk.theme | 14 + opsec/configs/themes/default.theme | 14 + opsec/configs/themes/ember.theme | 14 + opsec/configs/themes/frost.theme | 14 + opsec/configs/themes/slate.theme | 14 + opsec/configs/themes/terminal.theme | 14 + opsec/configs/torrc/torrc-default | 5 + opsec/configs/torrc/torrc-opsec | 32 + opsec/configs/udev/99-opsec-usb.rules | 15 + .../double-hop-ovpn.conf.template | 69 + .../wireguard-multihop.conf.template | 72 + opsec/conky/conky-opsec-cache.sh | 146 ++ opsec/conky/conky-opsec-status.sh | 225 +++ opsec/conky/conky-opsec-widget.conf | 71 + opsec/conky/opsec-widget-launch.sh | 127 ++ opsec/docs/OPSEC-CHECKLIST.md | 39 + opsec/install.sh | 289 ++++ opsec/lib/opsec-lib.sh | 717 +++++++++ opsec/scripts/opsec-banner-cache.sh | 35 + opsec/scripts/opsec-banner.sh | 320 ++++ opsec/scripts/opsec-boot-init.sh | 82 + opsec/scripts/opsec-check.sh | 76 + opsec/scripts/opsec-config.sh | 1044 ++++++++++++ opsec/scripts/opsec-harden.sh | 377 +++++ opsec/scripts/opsec-hostname-randomize.sh | 64 + opsec/scripts/opsec-killswitch.sh | 261 +++ opsec/scripts/opsec-log-rotate.sh | 97 ++ opsec/scripts/opsec-mode-toggle.sh | 25 + opsec/scripts/opsec-mode.sh | 1210 ++++++++++++++ opsec/scripts/opsec-monitor.sh | 119 ++ opsec/scripts/opsec-preflight.sh | 291 ++++ opsec/scripts/opsec-sentry.sh | 115 ++ opsec/scripts/opsec-ssh-check.sh | 123 ++ opsec/scripts/opsec-toggle.sh | 23 + opsec/scripts/opsec-traffic-blend.sh | 180 +++ opsec/scripts/opsec-widget-launch.sh | 127 ++ opsec/scripts/opsec-wifi-check.sh | 120 ++ phantom/README.md | 97 ++ phantom/ansible.cfg | 8 + phantom/modules/__init__.py | 1 + phantom/modules/all_in_one.py | 126 ++ phantom/modules/cloud.py | 27 + phantom/modules/dns.py | 49 + phantom/modules/email.py | 23 + phantom/modules/matrix.py | 47 + phantom/modules/media.py | 23 + phantom/modules/vault.py | 29 + phantom/modules/vpn.py | 39 + phantom/phantom.py | 463 ++++++ phantom/playbooks/all_in_one/main.yml | 92 ++ phantom/playbooks/cloud/main.yml | 11 + phantom/playbooks/common/base_hardening.yml | 144 ++ phantom/playbooks/dns/main.yml | 18 + phantom/playbooks/dns/tasks/configure.yml | 66 + phantom/playbooks/dns/tasks/install.yml | 62 + phantom/playbooks/email/main.yml | 11 + phantom/playbooks/matrix/main.yml | 38 + phantom/playbooks/matrix/tasks/element.yml | 44 + phantom/playbooks/matrix/tasks/nginx.yml | 115 ++ phantom/playbooks/matrix/tasks/synapse.yml | 99 ++ .../matrix/templates/homeserver.yaml.j2 | 53 + phantom/playbooks/media/main.yml | 11 + phantom/playbooks/vault/main.yml | 11 + phantom/playbooks/vpn/main.yml | 20 + phantom/playbooks/vpn/tasks/configure.yml | 88 + phantom/playbooks/vpn/tasks/install.yml | 54 + phantom/providers/aws.yml | 105 ++ phantom/providers/flokinet.yml | 30 + phantom/providers/linode.yml | 66 + 99 files changed, 12158 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 covert_sd/README.md create mode 100755 covert_sd/covert_sd_card_tool.py create mode 100644 covert_sd/tools/MOBILE_INSTRUCTIONS.txt create mode 100644 covert_sd/tools/README.txt create mode 100644 covert_sd/tools/WINDOWS_INSTRUCTIONS.txt create mode 100755 covert_sd/tools/lock_storage.sh create mode 100755 covert_sd/tools/mount_storage.sh create mode 100644 covert_sd/tools/veracrypt/DOWNLOAD_INSTRUCTIONS.md create mode 100644 opsec/README.md create mode 100644 opsec/configs/cron/opsec-banner-cache create mode 100644 opsec/configs/cron/opsec-log-rotate create mode 100644 opsec/configs/desktop/opsec-toggle.desktop create mode 100644 opsec/configs/levels/bare-metal-paranoid.conf create mode 100644 opsec/configs/levels/bare-metal-standard.conf create mode 100644 opsec/configs/levels/cloud-normal.conf create mode 100644 opsec/configs/levels/cloud-paranoid.conf create mode 100644 opsec/configs/opsec-aliases create mode 100644 opsec/configs/opsec-country-codes.conf create mode 100644 opsec/configs/opsec.conf create mode 100644 opsec/configs/polkit/com.opsec.mode.policy create mode 100755 opsec/configs/resolv.conf.head create mode 100644 opsec/configs/resolv.conf.opsec create mode 100644 opsec/configs/systemd/opsec-boot-advanced.service create mode 100644 opsec/configs/systemd/opsec-hostname-randomize.service create mode 100644 opsec/configs/systemd/opsec-killswitch.service create mode 100644 opsec/configs/systemd/opsec-mac-randomize.service create mode 100644 opsec/configs/themes/aurora.theme create mode 100644 opsec/configs/themes/cyberpunk.theme create mode 100644 opsec/configs/themes/default.theme create mode 100644 opsec/configs/themes/ember.theme create mode 100644 opsec/configs/themes/frost.theme create mode 100644 opsec/configs/themes/slate.theme create mode 100644 opsec/configs/themes/terminal.theme create mode 100644 opsec/configs/torrc/torrc-default create mode 100644 opsec/configs/torrc/torrc-opsec create mode 100644 opsec/configs/udev/99-opsec-usb.rules create mode 100644 opsec/configs/vpn-templates/double-hop-ovpn.conf.template create mode 100644 opsec/configs/vpn-templates/wireguard-multihop.conf.template create mode 100755 opsec/conky/conky-opsec-cache.sh create mode 100755 opsec/conky/conky-opsec-status.sh create mode 100644 opsec/conky/conky-opsec-widget.conf create mode 100755 opsec/conky/opsec-widget-launch.sh create mode 100755 opsec/docs/OPSEC-CHECKLIST.md create mode 100755 opsec/install.sh create mode 100755 opsec/lib/opsec-lib.sh create mode 100755 opsec/scripts/opsec-banner-cache.sh create mode 100755 opsec/scripts/opsec-banner.sh create mode 100755 opsec/scripts/opsec-boot-init.sh create mode 100755 opsec/scripts/opsec-check.sh create mode 100755 opsec/scripts/opsec-config.sh create mode 100755 opsec/scripts/opsec-harden.sh create mode 100755 opsec/scripts/opsec-hostname-randomize.sh create mode 100755 opsec/scripts/opsec-killswitch.sh create mode 100755 opsec/scripts/opsec-log-rotate.sh create mode 100755 opsec/scripts/opsec-mode-toggle.sh create mode 100755 opsec/scripts/opsec-mode.sh create mode 100755 opsec/scripts/opsec-monitor.sh create mode 100755 opsec/scripts/opsec-preflight.sh create mode 100755 opsec/scripts/opsec-sentry.sh create mode 100755 opsec/scripts/opsec-ssh-check.sh create mode 100755 opsec/scripts/opsec-toggle.sh create mode 100755 opsec/scripts/opsec-traffic-blend.sh create mode 100755 opsec/scripts/opsec-widget-launch.sh create mode 100755 opsec/scripts/opsec-wifi-check.sh create mode 100644 phantom/README.md create mode 100644 phantom/ansible.cfg create mode 100644 phantom/modules/__init__.py create mode 100644 phantom/modules/all_in_one.py create mode 100644 phantom/modules/cloud.py create mode 100644 phantom/modules/dns.py create mode 100644 phantom/modules/email.py create mode 100644 phantom/modules/matrix.py create mode 100644 phantom/modules/media.py create mode 100644 phantom/modules/vault.py create mode 100644 phantom/modules/vpn.py create mode 100755 phantom/phantom.py create mode 100644 phantom/playbooks/all_in_one/main.yml create mode 100644 phantom/playbooks/cloud/main.yml create mode 100644 phantom/playbooks/common/base_hardening.yml create mode 100644 phantom/playbooks/dns/main.yml create mode 100644 phantom/playbooks/dns/tasks/configure.yml create mode 100644 phantom/playbooks/dns/tasks/install.yml create mode 100644 phantom/playbooks/email/main.yml create mode 100644 phantom/playbooks/matrix/main.yml create mode 100644 phantom/playbooks/matrix/tasks/element.yml create mode 100644 phantom/playbooks/matrix/tasks/nginx.yml create mode 100644 phantom/playbooks/matrix/tasks/synapse.yml create mode 100644 phantom/playbooks/matrix/templates/homeserver.yaml.j2 create mode 100644 phantom/playbooks/media/main.yml create mode 100644 phantom/playbooks/vault/main.yml create mode 100644 phantom/playbooks/vpn/main.yml create mode 100644 phantom/playbooks/vpn/tasks/configure.yml create mode 100644 phantom/playbooks/vpn/tasks/install.yml create mode 100644 phantom/providers/aws.yml create mode 100644 phantom/providers/flokinet.yml create mode 100644 phantom/providers/linode.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ed99016 --- /dev/null +++ b/.gitignore @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a903e33 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..6b372ef --- /dev/null +++ b/README.md @@ -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 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 ` 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) diff --git a/covert_sd/README.md b/covert_sd/README.md new file mode 100644 index 0000000..d3f4623 --- /dev/null +++ b/covert_sd/README.md @@ -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. \ No newline at end of file diff --git a/covert_sd/covert_sd_card_tool.py b/covert_sd/covert_sd_card_tool.py new file mode 100755 index 0000000..09a0ae4 --- /dev/null +++ b/covert_sd/covert_sd_card_tool.py @@ -0,0 +1,1409 @@ +#!/usr/bin/env python3 + +import argparse +import subprocess +import sys +import os +import shutil +from datetime import datetime +import time +import json +import urllib.request +import tempfile + +# Global variables +DEBUG = False +FAST_MODE = False +PARANOID_MODE = False +TIMESTAMP = datetime.now().strftime('%Y%m%d_%H%M%S') +LOG_FILE = f"covert_sd_setup_{TIMESTAMP}.log" +CREATE_KALI = False +CREATE_DOCS = False +CREATE_TAILS = False +CREATE_CUSTOM = False +KALI_ISO = "" +TAILS_ISO = "" +CUSTOM_ISO = "" +DRIVE = "" +STATE_FILE = "/tmp/covert_sd_state.json" + +# Checkpoint states +CHECKPOINTS = { + "VERACRYPT_DOWNLOADED": False, + "DRIVE_SELECTED": False, + "DRIVE_WIPED": False, + "ISO_WRITTEN": False, + "PARTITIONS_CREATED": False, + "PERSISTENCE_SETUP": False, + "DOCS_ENCRYPTED": False, + "TOOLS_SETUP": False, + "COMPLETE": False +} + +def log(message): + """Logs a message to both the log file and the terminal.""" + with open(LOG_FILE, "a") as log_file: + log_file.write(message + "\n") + print(message) + +def save_state(checkpoint_name, data=None): + """Save current progress to state file.""" + global CHECKPOINTS + CHECKPOINTS[checkpoint_name] = True + + state = { + "checkpoints": CHECKPOINTS, + "drive": DRIVE, + "timestamp": TIMESTAMP, + "log_file": LOG_FILE, + "create_kali": CREATE_KALI, + "create_docs": CREATE_DOCS, + "create_tails": CREATE_TAILS, + "create_custom": CREATE_CUSTOM, + "kali_iso": KALI_ISO, + "tails_iso": TAILS_ISO, + "custom_iso": CUSTOM_ISO, + "fast_mode": FAST_MODE, + "paranoid_mode": PARANOID_MODE + } + + if data: + state.update(data) + + try: + with open(STATE_FILE, 'w') as f: + json.dump(state, f, indent=2) + log(f"✓ Checkpoint saved: {checkpoint_name}") + except Exception as e: + log(f"⚠ Warning: Could not save checkpoint: {e}") + +def load_state(): + """Load previous progress from state file.""" + global CHECKPOINTS, DRIVE, TIMESTAMP, LOG_FILE + global CREATE_KALI, CREATE_DOCS, CREATE_TAILS, CREATE_CUSTOM + global KALI_ISO, TAILS_ISO, CUSTOM_ISO, FAST_MODE, PARANOID_MODE + + if not os.path.exists(STATE_FILE): + return None + + try: + with open(STATE_FILE, 'r') as f: + state = json.load(f) + + CHECKPOINTS = state.get("checkpoints", CHECKPOINTS) + DRIVE = state.get("drive", "") + TIMESTAMP = state.get("timestamp", TIMESTAMP) + LOG_FILE = state.get("log_file", LOG_FILE) + CREATE_KALI = state.get("create_kali", False) + CREATE_DOCS = state.get("create_docs", False) + CREATE_TAILS = state.get("create_tails", False) + CREATE_CUSTOM = state.get("create_custom", False) + KALI_ISO = state.get("kali_iso", "") + TAILS_ISO = state.get("tails_iso", "") + CUSTOM_ISO = state.get("custom_iso", "") + FAST_MODE = state.get("fast_mode", False) + PARANOID_MODE = state.get("paranoid_mode", False) + + return state + except Exception as e: + log(f"⚠ Warning: Could not load state: {e}") + return None + +def clear_state(): + """Remove state file when setup is complete.""" + if os.path.exists(STATE_FILE): + os.remove(STATE_FILE) + log("✓ State file cleared") + +def run_command(command, shell=False, interactive=False, ignore_enospc=False): + """ + Runs a system command. + + Args: + command (list or str): The command to execute. + shell (bool): Whether to execute the command through the shell. + interactive (bool): If True, streams the command's output live. + ignore_enospc (bool): If True, don't exit on "No space left" error (for dd filling disk). + """ + if DEBUG: + log(f"Running command: {command}") + try: + if interactive: + # Use subprocess.run with no stdout/stderr capture for truly interactive commands + # This allows the command to directly interact with the terminal + # Don't use check=True if we want to ignore ENOSPC + result = subprocess.run(command, shell=shell, check=not ignore_enospc) + if ignore_enospc and result.returncode != 0: + # For dd, exit code 1 with ENOSPC is success (filled the disk) + return + elif result.returncode != 0: + raise subprocess.CalledProcessError(result.returncode, command) + return + else: + # Handle non-interactive commands + if isinstance(command, list): + result = subprocess.run( + command, + shell=shell, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True + ) + else: + result = subprocess.run( + command, + shell=shell, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True + ) + if result.stdout: + log(result.stdout.strip()) + if result.stderr: + log(result.stderr.strip()) + except subprocess.CalledProcessError as e: + log(f"Command failed: {e}\nOutput: {e.stdout}\nError: {e.stderr}") + sys.exit(1) + +def download_file(url, destination, description="file"): + """ + Downloads a file with progress indication. + + Args: + url (str): URL to download from + destination (str): Local path to save file + description (str): Description for user feedback + """ + try: + log(f"Downloading {description}...") + log(f" From: {url}") + log(f" To: {destination}") + + def progress_hook(block_num, block_size, total_size): + if total_size > 0: + downloaded = block_num * block_size + percent = min(100, (downloaded * 100) // total_size) + mb_downloaded = downloaded / (1024 * 1024) + mb_total = total_size / (1024 * 1024) + # Use \r to overwrite the same line + print(f"\r Progress: {percent}% ({mb_downloaded:.1f} MB / {mb_total:.1f} MB)", end='', flush=True) + + urllib.request.urlretrieve(url, destination, reporthook=progress_hook) + print() # New line after progress + log(f"✓ {description} downloaded successfully") + return True + except Exception as e: + log(f"✗ Failed to download {description}: {e}") + return False + +def download_portable_veracrypt(): + """ + Downloads portable VeraCrypt binaries for Windows, Linux, and macOS. + Returns the path to a temporary directory containing the downloads. + """ + log("\n" + "="*70) + log("DOWNLOADING PORTABLE VERACRYPT") + log("="*70) + log("\nTo make this device work on ANY computer without software installation,") + log("we need to download portable VeraCrypt for Windows, Linux, and macOS.") + log("") + log("Total download size: ~86-90 MB") + log(" • Windows Portable: ~39 MB") + log(" • Linux AppImage: ~13 MB") + log(" • Linux .deb Installer: ~13 MB") + log(" • macOS DMG: ~22 MB") + log("") + log("This is a one-time download that will be stored on your device.") + log("") + + # Use consistent temp directory for caching across runs + temp_dir = "/tmp/veracrypt_download" + + # Check if we already have cached downloads + if os.path.exists(temp_dir): + log(f"✓ Found existing VeraCrypt cache at: {temp_dir}") + log("Checking if all required files are present...") + + # Check if all files exist and are recent enough + cache_valid = True + cached_files = [] + + for platform, info in { + "Windows": "VeraCrypt-Portable-Windows.exe", + "Linux AppImage": "VeraCrypt-Portable-Linux.AppImage", + "Linux DEB": "veracrypt-Ubuntu-22.04-amd64.deb", + "macOS": "VeraCrypt-MacOS.dmg" + }.items(): + file_path = os.path.join(temp_dir, info) + if os.path.exists(file_path): + size_mb = os.path.getsize(file_path) / (1024 * 1024) + cached_files.append(f" ✓ {platform}: {size_mb:.1f} MB") + else: + cache_valid = False + break + + if cache_valid: + log("✓ All VeraCrypt files found in cache!") + for cf in cached_files: + log(cf) + log("") + use_cache = input("Use cached files? (y/n) [Default: y]: ") or "y" + if use_cache.lower() == "y": + log("✓ Using cached VeraCrypt downloads") + return temp_dir + else: + log("Removing old cache and downloading fresh copies...") + shutil.rmtree(temp_dir) + else: + log("⚠ Cache incomplete, will re-download") + shutil.rmtree(temp_dir) + + # Create temp directory for downloads + os.makedirs(temp_dir, exist_ok=True) + log(f"Downloading to: {temp_dir}") + log("") + + # Get latest VeraCrypt version from GitHub + log("Fetching latest VeraCrypt version from GitHub...") + try: + import urllib.request + import json + with urllib.request.urlopen("https://api.github.com/repos/veracrypt/VeraCrypt/releases/latest") as response: + release_data = json.loads(response.read().decode()) + VC_TAG = release_data["tag_name"] # e.g., "VeraCrypt_1.26.24" + VC_VERSION = VC_TAG.replace("VeraCrypt_", "") # e.g., "1.26.24" + log(f"✓ Latest version: {VC_VERSION}") + except Exception as e: + log(f"⚠ Warning: Could not fetch latest version ({e})") + log(" Falling back to version 1.26.24") + VC_TAG = "VeraCrypt_1.26.24" + VC_VERSION = "1.26.24" + + log("") + + downloads = { + "Windows Portable": { + "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/VeraCrypt.Portable.{VC_VERSION}.exe", + "filename": "VeraCrypt-Portable-Windows.exe", + "required": True + }, + "Linux Portable AppImage": { + "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/VeraCrypt-{VC_VERSION}-x86_64.AppImage", + "filename": "VeraCrypt-Portable-Linux.AppImage", + "required": True + }, + "Linux DEB Installer": { + "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/veracrypt-{VC_VERSION}-Ubuntu-22.04-amd64.deb", + "filename": "veracrypt-Ubuntu-22.04-amd64.deb", + "required": False + }, + "macOS": { + "url": f"https://github.com/veracrypt/VeraCrypt/releases/download/{VC_TAG}/VeraCrypt_{VC_VERSION}.dmg", + "filename": "VeraCrypt-MacOS.dmg", + "required": False + } + } + + success_count = 0 + required_count = 0 + + for platform, info in downloads.items(): + log(f"\n[{platform}]") + dest_path = os.path.join(temp_dir, info["filename"]) + + if info.get("required"): + required_count += 1 + + if download_file(info["url"], dest_path, f"{platform} VeraCrypt"): + success_count += 1 + + # Make Linux AppImage executable + if "AppImage" in info["filename"]: + try: + os.chmod(dest_path, 0o755) + log(f" ✓ Made executable") + except Exception as e: + log(f" ⚠ Warning: Could not make executable: {e}") + else: + if info.get("required"): + log(f" ✗ ERROR: {platform} is required but download failed!") + + log("\n" + "="*70) + if success_count >= required_count: + log("✓ PORTABLE VERACRYPT DOWNLOAD COMPLETE") + log("="*70) + log(f"\nDownloaded {success_count} of {len(downloads)} platform(s)") + log(f"Files saved to: {temp_dir}") + return temp_dir + else: + log("✗ DOWNLOAD FAILED") + log("="*70) + log(f"\nOnly {success_count} of {required_count} required downloads succeeded.") + log("Cannot proceed without portable VeraCrypt binaries.") + log("\nTroubleshooting:") + log(" • Check your internet connection") + log(" • Verify firewall settings") + log(" • Try again later (download servers may be busy)") + sys.exit(1) + +def check_dependencies(): + """Checks and installs missing dependencies.""" + dependencies = [ + "parted", "cryptsetup", "lsblk", "dd", "sgdisk", "wipefs", + "bc", "fdisk", "veracrypt", "lsof", "fuser", "mountpoint", "udevadm" + ] + missing = [] + for dep in dependencies: + if not shutil.which(dep): + missing.append(dep) + if missing: + log(f"Missing dependencies: {', '.join(missing)}") + install = input(f"Do you want to install the missing dependencies? (y/n) [Default: y]: ") or "y" + if install.lower() == "y": + run_command(["sudo", "apt", "update"]) + run_command(["sudo", "apt", "install", "-y"] + missing) + else: + log("Cannot proceed without installing dependencies. Exiting.") + sys.exit(1) + +def list_drives(): + """Lists all available drives.""" + log("Available drives:") + result = subprocess.run(["lsblk", "-J", "-o", "NAME,SIZE,TYPE"], capture_output=True, text=True) + try: + lsblk_output = json.loads(result.stdout) + for device in lsblk_output['blockdevices']: + if device['type'] == 'disk': + name = device['name'] + size = device['size'] + drive = f"/dev/{name} {size}" + log(drive) + except json.JSONDecodeError: + log("Error: Unable to parse lsblk output.") + sys.exit(1) + +def get_partition_name(drive, partition_number): + """ + Generates the partition name based on the drive and partition number. + + Args: + drive (str): The drive path (e.g., /dev/sda). + partition_number (int): The partition number. + + Returns: + str: The full partition path (e.g., /dev/sda1 or /dev/nvme0n1p1). + """ + if 'nvme' in drive or 'mmcblk' in drive: + return f"{drive}p{partition_number}" + else: + return f"{drive}{partition_number}" + +def prepare_drive(drive): + """ + Unmounts any mounted partitions, disables swap, and kills processes using the drive. + + Args: + drive (str): The drive to prepare (e.g., /dev/sda). + """ + # Unmount all mounted partitions + result = subprocess.run(["lsblk", "-lnp", drive], capture_output=True, text=True) + for line in result.stdout.strip().splitlines(): + parts = line.strip().split() + if len(parts) >= 7 and parts[6]: # If mountpoint is not empty + part = parts[0] + log(f"Unmounting {part}...") + run_command(["sudo", "umount", "-l", part]) + + # Disable swap if it's on the drive + with open("/proc/swaps") as swaps_file: + for line in swaps_file: + if drive in line: + swap_part = line.strip().split()[0] + log(f"Disabling swap on {swap_part}...") + run_command(["sudo", "swapoff", swap_part]) + + # Kill any processes using the drive + log(f"Checking for processes using {drive}...") + result = subprocess.run(["sudo", "lsof", drive], capture_output=True, text=True) + if result.stdout.strip(): + log(f"Processes using {drive}:\n{result.stdout}") + kill = input(f"Do you want to kill these processes? (y/n) [Default: y]: ") or "y" + if kill.lower() == "y": + run_command(["sudo", "fuser", "-k", drive]) + log(f"Killed processes using {drive}.") + else: + log("Cannot proceed while processes are using the drive. Exiting.") + sys.exit(1) + else: + log(f"No processes are using {drive}.") + +def setup_usb(): + """ + Sets up the bootable USB (Tails or Kali) and creates additional partitions if required. + """ + global DRIVE + + # Download portable VeraCrypt first (so it's ready to copy later) + veracrypt_dir = None + if CREATE_DOCS and not CHECKPOINTS["VERACRYPT_DOWNLOADED"]: + veracrypt_dir = download_portable_veracrypt() + save_state("VERACRYPT_DOWNLOADED", {"veracrypt_dir": veracrypt_dir}) + elif CREATE_DOCS: + # Already downloaded, use cached version + veracrypt_dir = "/tmp/veracrypt_download" + log("✓ Skipping VeraCrypt download (already completed)") + + if not CHECKPOINTS["DRIVE_SELECTED"]: + log("\n" + "="*70) + log("STEP 1: DRIVE SELECTION") + log("="*70) + log("Available storage devices on your system:") + list_drives() + log("\n⚠️ WARNING: This will repartition and format the selected drive.") + log("Make sure you select the correct device!") + + while True: + DRIVE = input("\nEnter the drive to use (e.g., /dev/sdb): ") + + if not DRIVE: + log("Error: No drive specified. Please enter a drive path.") + continue + + # Check if drive exists + if not os.path.exists(DRIVE): + log(f"Error: Drive {DRIVE} does not exist!") + log("Please check the available drives listed above and try again.") + retry = input("Try again? (y/n) [Default: y]: ") or "y" + if retry.lower() != "y": + log("Drive selection canceled. Exiting.") + sys.exit(1) + continue + + # Check if it's a block device + import stat as _stat + if not _stat.S_ISBLK(os.stat(DRIVE).st_mode): + log(f"Error: {DRIVE} is not a valid block device!") + continue + + # Confirm selection - more accurate warning + log(f"\nYou selected: {DRIVE}") + log("The script will:") + log(" • Clear partition table") + log(" • Create new partitions") + log(" • Optionally wipe (you'll be asked)") + log(" • Format with new filesystems") + confirm = input(f"\nProceed with {DRIVE}? (yes/no) [Default: no]: ") + if confirm.lower() == "yes": + break + else: + log("Drive selection canceled.") + retry = input("Select a different drive? (y/n) [Default: y]: ") or "y" + if retry.lower() != "y": + log("Exiting.") + sys.exit(1) + + log("\n" + "="*70) + log("STEP 2: PREPARING DRIVE") + log("="*70) + prepare_drive(DRIVE) + save_state("DRIVE_SELECTED") + else: + log(f"✓ Skipping drive selection (already selected: {DRIVE})") + + if CREATE_KALI or CREATE_TAILS or CREATE_DOCS: + log("\n" + "="*70) + log("STEP 3: DRIVE WIPING (OPTIONAL)") + log("="*70) + log("Wiping destroys all existing data on the drive.") + log("This prevents recovery of old files and ensures a clean setup.") + log("") + if PARANOID_MODE: + log("PARANOID MODE: 3-pass overwrite (very slow, ~2-3 hours for 64GB)") + else: + log("STANDARD MODE: 1-pass zero overwrite (~10-20 min for 64GB)") + log("Skip wiping if this is already a blank/new drive.") + log("") + + wipe = input(f"Do you want to wipe {DRIVE} before starting? (y/n) [Default: n]: ") or "n" + if wipe.lower() == "y": + if PARANOID_MODE: + log("\n" + "-"*70) + log("PARANOID WIPE: 3 passes with random data + final zero pass") + log("WARNING: This will take HOURS on large drives!") + log("Estimated time: 2-3 hours for 64GB drive") + log("-"*70) + confirm_wipe = input(f"Type 'WIPE' to confirm complete wipe of {DRIVE}: ") + if confirm_wipe == "WIPE": + log("\nClearing partition table and filesystem signatures...") + run_command(["sudo", "wipefs", "--all", DRIVE]) + run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) + log("\nStarting 3-pass shred (this will take a long time)...") + log("Pass 1/3: Random data") + log("Pass 2/3: Random data") + log("Pass 3/3: Random data") + log("Final pass: Zeros") + log("You can monitor progress below:") + # Paranoid: 3 passes with random data, then zeros + run_command(["sudo", "shred", "-vfz", "-n", "3", DRIVE], interactive=True) + log(f"\n✓ {DRIVE} securely wiped successfully (paranoid mode).") + else: + log("\nFull wipe canceled. Clearing partition table only...") + run_command(["sudo", "wipefs", "--all", DRIVE]) + run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) + log("✓ Partition table cleared.") + else: + log("\n" + "-"*70) + log("STANDARD WIPE: Single pass with zeros") + log("Estimated time: ~10-20 minutes for 64GB drive") + log("-"*70) + confirm_wipe = input(f"Type 'WIPE' to confirm wipe of {DRIVE}: ") + if confirm_wipe == "WIPE": + log("\nClearing partition table and filesystem signatures...") + run_command(["sudo", "wipefs", "--all", DRIVE]) + run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) + log("\nOverwriting entire drive with zeros...") + log("Progress will be shown below (this may take 10-20 minutes):") + log("(Note: dd will show 'No space left on device' when complete - this is normal)") + log("") + # Standard: Single pass with zeros (much faster) + # Note: dd will report "No space left on device" when it fills the drive - this is expected and means success + run_command(["sudo", "dd", "if=/dev/zero", f"of={DRIVE}", "bs=1M", "status=progress", "conv=fdatasync"], interactive=True, ignore_enospc=True) + log(f"\n✓ {DRIVE} wiped successfully (standard mode).") + else: + log("\nFull wipe canceled. Clearing partition table only...") + run_command(["sudo", "wipefs", "--all", DRIVE]) + run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) + log("✓ Partition table cleared.") + else: + log("\nSkipping full drive wipe. Clearing partition table only...") + run_command(["sudo", "wipefs", "--all", DRIVE]) + run_command(["sudo", "sgdisk", "--zap-all", DRIVE]) + log("✓ Partition table cleared.") + + # Initialize variables to track if ISO has been written + iso_written = False + + if CREATE_KALI or CREATE_TAILS or CREATE_CUSTOM: + log("\n" + "="*70) + log("STEP 4: WRITING BOOTABLE ISO") + log("="*70) + global KALI_ISO, TAILS_ISO, CUSTOM_ISO + if CREATE_KALI: + if not KALI_ISO: + KALI_ISO = input("Enter the path to the Kali ISO file: ") + if not os.path.isfile(KALI_ISO): + log(f"Error: Kali ISO file not found at {KALI_ISO}") + sys.exit(1) + ISO_PATH = KALI_ISO + iso_name = "Kali Linux" + elif CREATE_TAILS: + if not TAILS_ISO: + TAILS_ISO = input("Enter the path to the Tails ISO file: ") + if not os.path.isfile(TAILS_ISO): + log(f"Error: Tails ISO file not found at {TAILS_ISO}") + sys.exit(1) + ISO_PATH = TAILS_ISO + iso_name = "Tails" + elif CREATE_CUSTOM: + if not CUSTOM_ISO: + CUSTOM_ISO = input("Enter the path to your custom ISO file: ") + if not os.path.isfile(CUSTOM_ISO): + log(f"Error: Custom ISO file not found at {CUSTOM_ISO}") + sys.exit(1) + ISO_PATH = CUSTOM_ISO + iso_name = "Custom" + + # Get ISO size for time estimate + iso_size_bytes = os.path.getsize(ISO_PATH) + iso_size_gb = iso_size_bytes / (1024**3) + estimated_time = int((iso_size_gb / 0.5) * 60) # Rough estimate: 30 seconds per GB at 50MB/s + + log(f"ISO file: {ISO_PATH}") + log(f"ISO size: {iso_size_gb:.2f} GB") + log(f"Estimated write time: ~{estimated_time} seconds ({estimated_time//60} min)") + log("") + + if CREATE_TAILS: + # For Tails, flash the ISO to the entire drive without any partitioning + log(f"Writing {iso_name} ISO to {DRIVE}...") + log("Progress will be shown below:") + log("") + run_command(f"sudo dd if='{ISO_PATH}' of='{DRIVE}' bs=64M status=progress conv=fdatasync", shell=True, interactive=True) + log(f"\n✓ {iso_name} ISO written to {DRIVE} successfully.") + iso_written = True + else: + # For Kali or Custom ISO, flash and proceed with partition setup + log(f"Writing {iso_name} ISO to {DRIVE}...") + log("Progress will be shown below:") + log("") + run_command(f"sudo dd if='{ISO_PATH}' of='{DRIVE}' bs=64M status=progress conv=fdatasync", shell=True, interactive=True) + log(f"\n✓ {iso_name} ISO written to {DRIVE} successfully.") + iso_written = True + + # After writing ISO, set up partitions accordingly + if iso_written: + if CREATE_KALI or CREATE_CUSTOM: + # Both Kali and custom ISOs use the same partitioning approach + fix_partition_table(veracrypt_dir) + elif CREATE_TAILS: + if CREATE_DOCS: + # For Tails with docs, add partitions after Tails + fix_partition_table_tails(veracrypt_dir) + else: + # Tails only, no additional partitions + log("Tails installation complete. No additional partitions requested.") + elif CREATE_DOCS: + # If no ISO was written, just set up documents partition + fix_partition_table_docs_only(veracrypt_dir) + +def fix_partition_table_docs_only(veracrypt_dir=None): + """ + Sets up partitions solely for encrypted documents without altering existing OS partitions. + """ + log("\n" + "="*70) + log("STEP 5: CREATING PARTITION LAYOUT") + log("="*70) + log("Creating 2 partitions:") + log(" 1. Encrypted documents partition (VeraCrypt)") + log(" 2. Tools partition (1GB, unencrypted, contains mount scripts)") + log("") + + # Clear existing GPT label to start fresh + log("Creating fresh GPT partition table...") + run_command(f"sudo parted -a optimal -s {DRIVE} mklabel gpt", shell=True) + run_command(f"sudo partprobe {DRIVE}", shell=True) + run_command("sudo udevadm settle", shell=True) + time.sleep(5) # Increased sleep to ensure partitions are recognized + log("✓ GPT partition table created") + + # Get the total size of the drive in bytes + log("\nCalculating partition sizes...") + result = subprocess.run(["lsblk", "-b", "-d", "-n", "-o", "SIZE", DRIVE], capture_output=True, text=True) + try: + total_size_bytes = int(result.stdout.strip()) + except ValueError: + log(f"Error: Unable to determine drive size. lsblk output: {result.stdout}") + sys.exit(1) + total_size_mib = total_size_bytes / (1024 * 1024) # Convert to MiB + total_size_gb = total_size_mib / 1024 # Convert to GiB + available_gb = (total_size_mib - 1024) / 1024 # Minus 1GB reserved for scripts + + log(f" Total drive size: {total_size_gb:.2f} GB ({total_size_mib:.0f} MiB)") + log(f" Available for documents: {available_gb:.2f} GB (after reserving 1GB for tools)") + log("") + + # Ask for document partition size + size_docs = input("Enter size for documents partition in GB (leave blank to use remaining space minus 1GB): ") + start_docs_mib = 1 # Starting immediately after the first MiB + if size_docs: + try: + size_docs_gb = float(size_docs) + end_docs_mib = start_docs_mib + (size_docs_gb * 1024) + if end_docs_mib > (total_size_mib - 1024): + log("Error: Documents partition size exceeds available space when reserving 1GB for unencrypted partition.") + sys.exit(1) + except ValueError: + log("Invalid size entered for documents partition. Exiting.") + sys.exit(1) + else: + end_docs_mib = total_size_mib - 1024 # Reserve 1GB for unencrypted partition + + # Create documents partition + log(f"\nCreating partition 1 (documents): {(end_docs_mib - start_docs_mib)/1024:.2f} GB...") + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_docs_mib}MiB {end_docs_mib}MiB", shell=True) + log("✓ Documents partition created") + + # Create unencrypted partition + start_unencrypted_mib = end_docs_mib + log(f"Creating partition 2 (tools): ~1 GB...") + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_unencrypted_mib}MiB 100%", shell=True) + log("✓ Tools partition created") + + # Refresh partition table to recognize new partitions + log("\nRefreshing partition table...") + run_command(f"sudo partprobe {DRIVE}", shell=True) + run_command("sudo udevadm settle", shell=True) + time.sleep(5) # Increased sleep to ensure partitions are recognized + log("✓ Partition table refreshed") + + setup_unencrypted_partition(veracrypt_dir) + setup_docs_partition() + +def fix_partition_table_tails(veracrypt_dir=None): + """ + Adds encrypted documents partition after Tails installation without breaking boot. + Tails creates its own partition table, so we extend it carefully. + """ + log("Adding encrypted documents partition after Tails installation...") + + # Wait for Tails partitions to settle + run_command(f"sudo partprobe {DRIVE}", shell=True) + run_command("sudo udevadm settle", shell=True) + time.sleep(5) + + # Get current partition information + result = subprocess.run(["sudo", "parted", "-s", DRIVE, "unit", "MiB", "print"], capture_output=True, text=True) + log("Current partition table after Tails installation:") + log(result.stdout) + + # Find the end of the last Tails partition + last_partition_end = None + partition_lines = [line for line in result.stdout.strip().splitlines() if line.strip() and line.strip()[0].isdigit()] + + if partition_lines: + # Get the last partition's end position + last_line = partition_lines[-1] + parts = last_line.strip().split() + if len(parts) >= 3: + last_partition_end = parts[2].replace('MiB', '') + + if last_partition_end is None: + log("Error: Could not determine end of Tails partitions.") + sys.exit(1) + + log(f"Last Tails partition ends at: {last_partition_end}MiB") + + # Get the total size of the drive + result = subprocess.run(["lsblk", "-b", "-d", "-n", "-o", "SIZE", DRIVE], capture_output=True, text=True) + try: + total_size_bytes = int(result.stdout.strip()) + except ValueError: + log(f"Error: Unable to determine drive size. lsblk output: {result.stdout}") + sys.exit(1) + total_size_mib = total_size_bytes / (1024 * 1024) + total_size_gb = total_size_mib / 1024 + available_after_tails_mib = total_size_mib - float(last_partition_end) + available_after_tails_gb = available_after_tails_mib / 1024 + + log(f"Total drive size: {total_size_gb:.2f} GB ({total_size_mib:.0f} MiB)") + log(f"Available space after Tails: {available_after_tails_gb:.2f} GB") + + if available_after_tails_gb < 2: + log(f"Warning: Only {available_after_tails_gb:.2f} GB available after Tails. Need at least 2GB for docs + tools partitions.") + proceed = input("Continue anyway? (y/n) [Default: n]: ") or "n" + if proceed.lower() != "y": + log("Partition setup canceled.") + return + + # Set up documents partition + start_docs_mib = float(last_partition_end) + remaining_after_tails_gb = (total_size_mib - start_docs_mib) / 1024 + log(f"Remaining space for documents: {remaining_after_tails_gb:.2f} GB (will reserve 1GB for tools partition)") + + size_docs = input("Enter size for documents partition in GB (leave blank to use remaining space minus 1GB): ") + if size_docs: + try: + size_docs_gb = float(size_docs) + end_docs_mib = start_docs_mib + (size_docs_gb * 1024) + if end_docs_mib > (total_size_mib - 1024): + log("Error: Documents partition size exceeds available space when reserving 1GB for tools partition.") + sys.exit(1) + except ValueError: + log("Invalid size entered for documents partition. Exiting.") + sys.exit(1) + else: + end_docs_mib = total_size_mib - 1024 # Reserve 1GB for tools partition + + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_docs_mib}MiB {end_docs_mib}MiB", shell=True) + log("Created documents partition after Tails.") + + # Create unencrypted tools partition + start_unencrypted_mib = end_docs_mib + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_unencrypted_mib}MiB 100%", shell=True) + log("Created tools partition for scripts/instructions.") + + # Refresh partition table + run_command(f"sudo partprobe {DRIVE}", shell=True) + run_command("sudo udevadm settle", shell=True) + time.sleep(5) + + setup_docs_partition() + setup_unencrypted_partition(veracrypt_dir) + + log("Tails + encrypted documents partition setup complete!") + +def fix_partition_table(veracrypt_dir=None): + """ + Fixes the partition table for Kali Linux by adding persistence and documents partitions. + """ + log("Fixing partition table to reclaim remaining space...") + + # Attempt to delete partition 2 if it exists + try: + run_command(f"sudo parted -a optimal -s {DRIVE} rm 2", shell=True) + log("Deleted partition 2.") + except SystemExit: + log("No partition 2 to delete.") + + # Get the end of partition 1 + result = subprocess.run(["sudo", "parted", "-s", DRIVE, "unit", "MiB", "print"], capture_output=True, text=True) + end_of_p1 = None + for line in result.stdout.strip().splitlines(): + if line.strip().startswith("1"): + parts = line.strip().split() + if len(parts) >= 3: + end_of_p1 = parts[2].replace('MiB', '') + break + if end_of_p1 is None: + log("Error: Could not find end of partition 1.") + sys.exit(1) + + log(f"End of partition 1: {end_of_p1}MiB") + + # Get the total size of the drive in bytes + result = subprocess.run(["lsblk", "-b", "-d", "-n", "-o", "SIZE", DRIVE], capture_output=True, text=True) + try: + total_size_bytes = int(result.stdout.strip()) + except ValueError: + log(f"Error: Unable to determine drive size. lsblk output: {result.stdout}") + sys.exit(1) + total_size_mib = total_size_bytes / (1024 * 1024) # Convert to MiB + total_size_gb = total_size_mib / 1024 # Convert to GiB + available_after_p1_gb = (total_size_mib - float(end_of_p1)) / 1024 + + log(f"Total drive size: {total_size_gb:.2f} GB ({total_size_mib:.0f} MiB)") + log(f"Available space after partition 1: {available_after_p1_gb:.2f} GB") + + # Ask for persistence partition size + size_persistence = input("Enter size for persistence partition in GB (e.g., 4): ") or "4" + try: + size_persistence_gb = float(size_persistence) + except ValueError: + log("Invalid size entered for persistence partition. Exiting.") + sys.exit(1) + + start_persistence_mib = float(end_of_p1) + end_persistence_mib = start_persistence_mib + (size_persistence_gb * 1024) + + if end_persistence_mib > total_size_mib: + log("Error: Persistence partition size exceeds available space.") + sys.exit(1) + + # Create persistence partition + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary ext4 {start_persistence_mib}MiB {end_persistence_mib}MiB", shell=True) + log("Created persistence partition.") + + # Set up documents partition + start_docs_mib = end_persistence_mib + remaining_after_persistence_gb = (total_size_mib - end_persistence_mib) / 1024 + log(f"Remaining space after persistence partition: {remaining_after_persistence_gb:.2f} GB (will reserve 1GB for scripts)") + + size_docs = input("Enter size for documents partition in GB (leave blank to use remaining space minus 1GB): ") + if size_docs: + try: + size_docs_gb = float(size_docs) + end_docs_mib = start_docs_mib + (size_docs_gb * 1024) + if end_docs_mib > (total_size_mib - 1024): + log("Error: Documents partition size exceeds available space when reserving 1GB for unencrypted partition.") + sys.exit(1) + except ValueError: + log("Invalid size entered for documents partition. Exiting.") + sys.exit(1) + else: + end_docs_mib = total_size_mib - 1024 # Reserve 1GB for unencrypted partition + + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_docs_mib}MiB {end_docs_mib}MiB", shell=True) + log("Created documents partition.") + + # Create unencrypted partition + start_unencrypted_mib = end_docs_mib + run_command(f"sudo parted -a optimal -s {DRIVE} mkpart primary {start_unencrypted_mib}MiB 100%", shell=True) + log("Created unencrypted partition for scripts/instructions.") + + # Refresh partition table to recognize new partitions + run_command(f"sudo partprobe {DRIVE}", shell=True) + run_command("sudo udevadm settle", shell=True) + time.sleep(5) # Increased sleep to ensure partitions are recognized + + setup_kali_partition() + if CREATE_DOCS: + setup_docs_partition() + setup_unencrypted_partition(veracrypt_dir) + +def setup_kali_partition(): + """ + Configures the persistence partition for Kali Linux. + """ + global DRIVE + PERSIST_PART = get_partition_name(DRIVE, 2) + + # Check if partition exists + if not os.path.exists(PERSIST_PART): + log(f"Error: Partition {PERSIST_PART} does not exist.") + sys.exit(1) + + run_command(["sudo", "wipefs", "--all", PERSIST_PART]) + + log("Configuring encrypted persistence partition...") + + log("You will be prompted to enter a passphrase for the persistence partition.") + log("Please choose a strong passphrase and remember it!") + log("When asked 'Are you sure? (Type YES in capital letters):', type: YES") + log("Then enter your passphrase twice when prompted.") + + if FAST_MODE: + luks_format_cmd = ( + f"sudo cryptsetup luksFormat '{PERSIST_PART}' " + f"--type luks1 " + f"--cipher aes-cbc-essiv:sha256 " + f"--key-size 256 " + f"--hash sha256 " + f"--iter-time 1000 " + f"--verify-passphrase" + ) + mkfs_cmd = f"sudo mkfs.ext3 -L persistence /dev/mapper/kali_USB" + elif PARANOID_MODE: + luks_format_cmd = ( + f"sudo cryptsetup luksFormat '{PERSIST_PART}' " + f"--type luks2 " + f"--cipher aes-xts-plain64 " + f"--key-size 512 " + f"--hash sha512 " + f"--pbkdf argon2id " + f"--iter-time 10000 " + f"--pbkdf-memory 1048576 " + f"--pbkdf-parallel 4 " + f"--verify-passphrase" + ) + mkfs_cmd = f"sudo mkfs.ext4 -L persistence /dev/mapper/kali_USB" + else: + luks_format_cmd = ( + f"sudo cryptsetup luksFormat '{PERSIST_PART}' " + f"--type luks2 " + f"--cipher aes-xts-plain64 " + f"--key-size 512 " + f"--hash sha512 " + f"--iter-time 5000 " + f"--verify-passphrase" + ) + mkfs_cmd = f"sudo mkfs.ext4 -L persistence /dev/mapper/kali_USB" + + run_command(luks_format_cmd, shell=True, interactive=True) + time.sleep(2) + run_command(f"sudo cryptsetup luksOpen '{PERSIST_PART}' kali_USB", shell=True, interactive=True) + run_command(mkfs_cmd, shell=True) + + run_command("sudo mkdir -p /mnt/kali_USB", shell=True) + run_command("sudo mount /dev/mapper/kali_USB /mnt/kali_USB", shell=True) + run_command('echo "/ union" | sudo tee /mnt/kali_USB/persistence.conf', shell=True) + run_command("sudo umount /mnt/kali_USB", shell=True) + run_command("sudo cryptsetup luksClose kali_USB", shell=True) + + log("Kali persistence setup complete.") + +def setup_docs_partition(): + """ + Configures the VeraCrypt encrypted documents partition. + """ + global DRIVE + DOCS_PART = get_partition_name(DRIVE, get_last_partition_number() - 1) + + log("\n" + "="*70) + log("STEP 6: ENCRYPTING DOCUMENTS PARTITION") + log("="*70) + + # Check if the partition exists + if not os.path.exists(DOCS_PART): + log(f"Error: Partition {DOCS_PART} does not exist.") + sys.exit(1) + + # Force wipe existing filesystem signatures + log(f"Preparing partition {DOCS_PART}...") + run_command(["sudo", "wipefs", "--all", "--force", DOCS_PART]) + log(f"✓ Partition prepared") + + log("\n" + "="*70) + log("ENCRYPTED VOLUME SETUP") + log("="*70) + log("\nYou will now create an encrypted volume for sensitive documents.") + log("") + log("SECURITY SETTINGS:") + log(" • Encryption: AES-Twofish-Serpent (triple cascade)") + log(" • Hash: SHA-512") + if PARANOID_MODE: + log(" • PIM: 5000 (PARANOID mode - maximum security)") + log(" • Unlock time: ~5-7 seconds") + else: + log(" • PIM: 2000 (standard high security)") + log(" • Unlock time: ~2-3 seconds") + log("") + log("PASSWORD REQUIREMENTS:") + log(" • Minimum 20 characters (longer is better)") + log(" • Mix uppercase, lowercase, numbers, and symbols") + log(" • Avoid dictionary words or personal information") + log(" • Example: My$ecur3Tr@v3lD0cs!2025#Paris") + log("") + log("⚠ CRITICAL: There is NO password recovery!") + log(" If you forget your password, your data is GONE FOREVER.") + log("") + log("="*70) + input("Press ENTER when you're ready to create your password...") + + log("\nConfiguring VeraCrypt encryption for documents partition...") + + # Always use maximum security for documents partition (ignore FAST_MODE) + # Triple cascade encryption with strongest hash + pim_value = 5000 if PARANOID_MODE else 2000 + veracrypt_create_cmd = ( + f"veracrypt --text --create '{DOCS_PART}' " + f"--encryption AES-Twofish-Serpent " + f"--hash SHA-512 " + f"--filesystem ext4 " + f"--volume-type normal " + f"--pim {pim_value} " # 2000 standard, 5000 paranoid + ) + + log("\nStarting VeraCrypt encryption (this may take a few minutes)...") + log("="*70) + log("VERACRYPT PROMPTS - FOLLOW THESE STEPS:") + log("="*70) + log("") + log("1. 'Enter password:' → Type your strong password (20+ characters)") + log(" Then press ENTER") + log("") + log("2. 'Re-enter password:' → Type the SAME password again") + log(" Then press ENTER") + log("") + log("3. 'Enter keyfile path [none]:' → Just press ENTER") + log(" (Keyfiles not needed for most users)") + log("") + log("4. 'Please type at least 320 randomly chosen characters'") + log(" → Smash random keys on your keyboard") + log(" → Keep typing until it shows 'Characters remaining: 0'") + log(" → Then press ENTER") + log(" (This creates random entropy for encryption)") + log("") + log(f"ℹ️ NOTE: PIM is set to {pim_value} automatically (not prompted)") + log("") + log("="*70) + log("Creating volume now...") + log("="*70) + log("") + run_command(veracrypt_create_cmd, shell=True, interactive=True) + log("\n✓ Documents partition encrypted successfully!") + log(f" Encryption: AES-Twofish-Serpent (triple cascade)") + log(f" Hash: SHA-512") + log(f" PIM: {pim_value}") + log(f" Filesystem: ext4") + save_state("DOCS_ENCRYPTED") + +def setup_unencrypted_partition(veracrypt_dir=None): + """ + Sets up the unencrypted partition for scripts and instructions. + + Args: + veracrypt_dir (str): Path to directory containing portable VeraCrypt files + """ + global DRIVE + UNENCRYPTED_PART = get_partition_name(DRIVE, get_last_partition_number()) + + log("\n" + "="*70) + log("STEP 7: SETTING UP TOOLS PARTITION") + log("="*70) + + # Check if the partition exists + if not os.path.exists(UNENCRYPTED_PART): + log(f"Error: Partition {UNENCRYPTED_PART} does not exist.") + sys.exit(1) + + log(f"Formatting {UNENCRYPTED_PART} as FAT32...") + run_command(f"sudo mkfs.vfat -n 'TOOLS' {UNENCRYPTED_PART}", shell=True) + log("✓ FAT32 filesystem created") + + log("Mounting tools partition...") + run_command("sudo mkdir -p /mnt/unencrypted", shell=True) + run_command(f"sudo mount {UNENCRYPTED_PART} /mnt/unencrypted", shell=True) + log("✓ Tools partition mounted") + + # Get the directory where this script is located + script_dir = os.path.dirname(os.path.abspath(__file__)) + tools_dir = os.path.join(script_dir, "tools") + + log("\nCopying helper files from tools/ directory...") + + # Copy README.txt + readme_path = os.path.join(tools_dir, "README.txt") + if os.path.exists(readme_path): + run_command(f"sudo cp '{readme_path}' /mnt/unencrypted/README.txt", shell=True) + log(" ✓ README.txt copied") + else: + log(f" ⚠ Warning: {readme_path} not found, skipping README") + + # Copy WINDOWS_INSTRUCTIONS.txt + windows_inst_path = os.path.join(tools_dir, "WINDOWS_INSTRUCTIONS.txt") + if os.path.exists(windows_inst_path): + run_command(f"sudo cp '{windows_inst_path}' /mnt/unencrypted/WINDOWS_INSTRUCTIONS.txt", shell=True) + log(" ✓ WINDOWS_INSTRUCTIONS.txt copied") + else: + log(f" ⚠ Warning: {windows_inst_path} not found") + + # Copy MOBILE_INSTRUCTIONS.txt + mobile_inst_path = os.path.join(tools_dir, "MOBILE_INSTRUCTIONS.txt") + if os.path.exists(mobile_inst_path): + run_command(f"sudo cp '{mobile_inst_path}' /mnt/unencrypted/MOBILE_INSTRUCTIONS.txt", shell=True) + log(" ✓ MOBILE_INSTRUCTIONS.txt copied") + else: + log(f" ⚠ Warning: {mobile_inst_path} not found") + + # Copy mount_storage.sh + mount_script_path = os.path.join(tools_dir, "mount_storage.sh") + if os.path.exists(mount_script_path): + run_command(f"sudo cp '{mount_script_path}' /mnt/unencrypted/mount_storage.sh", shell=True) + run_command("sudo chmod +x /mnt/unencrypted/mount_storage.sh", shell=True) + log(" ✓ mount_storage.sh copied") + else: + log(f" ⚠ Warning: {mount_script_path} not found, skipping mount script") + + # Copy lock_storage.sh + lock_script_path = os.path.join(tools_dir, "lock_storage.sh") + if os.path.exists(lock_script_path): + run_command(f"sudo cp '{lock_script_path}' /mnt/unencrypted/lock_storage.sh", shell=True) + run_command("sudo chmod +x /mnt/unencrypted/lock_storage.sh", shell=True) + log(" ✓ lock_storage.sh copied") + else: + log(f" ⚠ Warning: {lock_script_path} not found, skipping lock script") + + # Copy portable VeraCrypt files + if veracrypt_dir and os.path.exists(veracrypt_dir): + log("\nCopying portable VeraCrypt binaries...") + run_command("sudo mkdir -p /mnt/unencrypted/VeraCrypt", shell=True) + + veracrypt_files = [ + ("VeraCrypt-Portable-Windows.exe", "Windows portable"), + ("VeraCrypt-Portable-Linux.AppImage", "Linux portable AppImage"), + ("veracrypt-Ubuntu-22.04-amd64.deb", "Linux DEB installer"), + ("VeraCrypt-MacOS.dmg", "macOS installer") + ] + + for filename, description in veracrypt_files: + src = os.path.join(veracrypt_dir, filename) + if os.path.exists(src): + run_command(f"sudo cp '{src}' /mnt/unencrypted/VeraCrypt/{filename}", shell=True) + if "Linux" in filename: + run_command(f"sudo chmod +x /mnt/unencrypted/VeraCrypt/{filename}", shell=True) + log(f" ✓ {description} copied") + else: + log(f" ⚠ {description} not found (optional)") + + log(" ✓ Portable VeraCrypt binaries installed") + log("\n 📌 IMPORTANT: These binaries allow accessing encrypted files") + log(" on ANY Windows, Linux, or Mac computer WITHOUT installing software!") + else: + log("\n ⚠ No portable VeraCrypt binaries provided - skipping") + + log("\nUnmounting tools partition...") + run_command("sudo umount /mnt/unencrypted", shell=True) + log("✓ Tools partition setup complete") + save_state("TOOLS_SETUP") + +def get_last_partition_number(): + """ + Returns the highest partition number on the DRIVE. + + Returns: + int: The highest partition number. + """ + result = subprocess.run(["lsblk", "-ln", "-o", "NAME", DRIVE], capture_output=True, text=True) + partitions = [line.strip() for line in result.stdout.strip().splitlines() if line.strip()] + partition_numbers = [] + for part in partitions: + if 'p' in part: + # Handle /dev/nvme0n1p1 format + if part.startswith(os.path.basename(DRIVE)): + num = part.replace(os.path.basename(DRIVE), '').replace('p', '') + if num.isdigit(): + partition_numbers.append(int(num)) + else: + # Handle /dev/sda1 format + if part.startswith(os.path.basename(DRIVE)): + num = part.replace(os.path.basename(DRIVE), '') + if num.isdigit(): + partition_numbers.append(int(num)) + if not partition_numbers: + log(f"No partitions found on {DRIVE}.") + sys.exit(1) + return max(partition_numbers) + +def main(): + """ + The main function that parses arguments and orchestrates the setup process. + """ + global DEBUG, FAST_MODE, PARANOID_MODE, CREATE_KALI, CREATE_DOCS, CREATE_TAILS, KALI_ISO, TAILS_ISO, DRIVE, CREATE_CUSTOM, CUSTOM_ISO + + CREATE_CUSTOM = False + CUSTOM_ISO = "" + + parser = argparse.ArgumentParser(description="Covert SD Card Tool") + + # Creating a mutually exclusive group for -a and -t to prevent their simultaneous use + group = parser.add_mutually_exclusive_group() + group.add_argument("-a", "--all", action="store_true", help="Set up both OS bootable USB and documents partition") + group.add_argument("-t", "--tails", action="store_true", help="Create Tails bootable USB (no persistence)") + + # Other arguments remain outside the mutually exclusive group + parser.add_argument("-k", "--kali", action="store_true", help="Create Kali bootable USB and persistence partition") + parser.add_argument("-d", "--docs", action="store_true", help="Create encrypted documents partition") + parser.add_argument("-c", "--custom", action="store_true", help="Create custom ISO bootable USB (like Kali, with persistence)") + parser.add_argument("-i", "--iso", help="Path to the ISO file (Kali, Tails, or custom)") + parser.add_argument("--fast", action="store_true", help="Enable fast setup with less secure encryption") + parser.add_argument("--paranoid", action="store_true", help="Enable paranoid mode: maximum security, 3-pass shred, highest encryption") + parser.add_argument("--debug", action="store_true", help="Enable debug mode") + parser.add_argument("-r", "--resume", action="store_true", help="Resume from previous incomplete setup") + + args = parser.parse_args() + + # Prevent using -a with -t; argparse handles this due to mutually exclusive group + # However, if you want to provide a custom error message or additional handling, you can add it here + + # Handle resume flag + if args.resume: + previous_state = load_state() + if previous_state: + log("\n" + "="*70) + log("🔄 RESUMING PREVIOUS SETUP") + log("="*70) + log("") + log("Found incomplete setup from previous run:") + log(f" Drive: {previous_state.get('drive', 'Unknown')}") + log(f" Started: {previous_state.get('timestamp', 'Unknown')}") + log("") + log("Completed steps:") + for checkpoint, completed in previous_state.get('checkpoints', {}).items(): + status = "✓" if completed else "✗" + log(f" {status} {checkpoint}") + log("") + log("✓ Resuming from last checkpoint...") + log(f"Continuing log in: {LOG_FILE}") + log("") + # State already loaded by load_state() + # Skip to setup_usb() - it will check checkpoints + check_dependencies() + setup_usb() + return + else: + log("Error: No previous setup found to resume.") + log("State file not found at: /tmp/covert_sd_state.json") + log("") + log("To start a new setup, run without --resume flag:") + log(" sudo ./covert_sd_card_tool.py -d -a -t /path/to/tails.iso") + sys.exit(1) + + # Check if state exists but user didn't use --resume flag + previous_state = load_state() + if previous_state: + log("\n" + "="*70) + log("⚠️ PREVIOUS INCOMPLETE SETUP FOUND") + log("="*70) + log("") + log(f"Drive: {previous_state.get('drive', 'Unknown')}") + log(f"Started: {previous_state.get('timestamp', 'Unknown')}") + log("") + log("You have two options:") + log(" 1. Resume previous setup: sudo ./covert_sd_card_tool.py --resume") + log(" 2. Start fresh (WARNING: will overwrite any existing setup)") + log("") + choice = input("Continue with fresh setup? (yes/no) [Default: no]: ") or "no" + if choice.lower() != "yes": + log("Exiting. Use --resume flag to continue previous setup.") + sys.exit(0) + else: + log("Starting fresh setup (old state will be cleared)...") + clear_state() + + if not any([args.all, args.kali, args.docs, args.tails, args.custom]): + parser.print_help() + sys.exit(1) + + DEBUG = args.debug + if DEBUG: + log("Debug mode enabled") + + FAST_MODE = args.fast + PARANOID_MODE = args.paranoid + + # Paranoid and Fast are mutually exclusive + if FAST_MODE and PARANOID_MODE: + log("Error: Cannot use --fast and --paranoid modes together.") + sys.exit(1) + + if FAST_MODE: + log("Fast mode enabled: Using less secure encryption for quicker setup.") + elif PARANOID_MODE: + log("PARANOID MODE enabled: Maximum security - 3-pass wipe, strongest encryption, highest PIM.") + + if args.all: + CREATE_DOCS = True + # When -a is used without -t, default to creating Kali + CREATE_KALI = True + else: + CREATE_KALI = args.kali + CREATE_DOCS = args.docs + CREATE_TAILS = args.tails + CREATE_CUSTOM = args.custom + + if args.iso: + if CREATE_KALI: + KALI_ISO = args.iso + elif CREATE_TAILS: + TAILS_ISO = args.iso + elif CREATE_CUSTOM: + CUSTOM_ISO = args.iso + + check_dependencies() + setup_usb() + + # Final summary + log("\n" + "="*70) + log("SETUP COMPLETE!") + log("="*70) + log("") + log("Your secure storage device is ready!") + log("") + log("What was created:") + if CREATE_KALI: + log(" ✓ Kali Linux bootable drive") + log(" ✓ LUKS encrypted persistence partition") + elif CREATE_TAILS: + log(" ✓ Tails bootable drive") + elif CREATE_CUSTOM: + log(" ✓ Custom bootable drive") + log(" ✓ LUKS encrypted persistence partition") + + if CREATE_DOCS: + log(" ✓ VeraCrypt encrypted documents partition") + if PARANOID_MODE: + log(" - Triple cascade encryption (AES-Twofish-Serpent)") + log(" - PIM 5000 (PARANOID mode)") + else: + log(" - Triple cascade encryption (AES-Twofish-Serpent)") + log(" - PIM 2000 (standard security)") + log(" ✓ Tools partition with helper scripts") + log("") + log("Next steps:") + log(" 1. Safely eject the device") + log(" 2. On your target system, mount the TOOLS partition") + log(" 3. Read README.txt for instructions") + log(" 4. Run: sudo ./mount_storage.sh") + log("") + + log(f"Log file saved: {LOG_FILE}") + log("") + if CREATE_DOCS and os.path.exists("/tmp/veracrypt_download"): + cache_size = sum(os.path.getsize(os.path.join("/tmp/veracrypt_download", f)) + for f in os.listdir("/tmp/veracrypt_download") + if os.path.isfile(os.path.join("/tmp/veracrypt_download", f))) / (1024 * 1024) + log("💡 TIP: VeraCrypt downloads cached at /tmp/veracrypt_download") + log(f" Size: {cache_size:.1f} MB") + log(" Next SD card creation will skip downloads!") + log(" To clean up: rm -rf /tmp/veracrypt_download") + log("") + log("="*70) + + # Mark as complete and clear state file + save_state("COMPLETE") + clear_state() + +if __name__ == "__main__": + main() diff --git a/covert_sd/tools/MOBILE_INSTRUCTIONS.txt b/covert_sd/tools/MOBILE_INSTRUCTIONS.txt new file mode 100644 index 0000000..485f8b6 --- /dev/null +++ b/covert_sd/tools/MOBILE_INSTRUCTIONS.txt @@ -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. + +====================================================================== diff --git a/covert_sd/tools/README.txt b/covert_sd/tools/README.txt new file mode 100644 index 0000000..d5b454a --- /dev/null +++ b/covert_sd/tools/README.txt @@ -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 + +====================================================================== diff --git a/covert_sd/tools/WINDOWS_INSTRUCTIONS.txt b/covert_sd/tools/WINDOWS_INSTRUCTIONS.txt new file mode 100644 index 0000000..e0a9f9c --- /dev/null +++ b/covert_sd/tools/WINDOWS_INSTRUCTIONS.txt @@ -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" + +====================================================================== diff --git a/covert_sd/tools/lock_storage.sh b/covert_sd/tools/lock_storage.sh new file mode 100755 index 0000000..d9ce73e --- /dev/null +++ b/covert_sd/tools/lock_storage.sh @@ -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 diff --git a/covert_sd/tools/mount_storage.sh b/covert_sd/tools/mount_storage.sh new file mode 100755 index 0000000..cb225b3 --- /dev/null +++ b/covert_sd/tools/mount_storage.sh @@ -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 diff --git a/covert_sd/tools/veracrypt/DOWNLOAD_INSTRUCTIONS.md b/covert_sd/tools/veracrypt/DOWNLOAD_INSTRUCTIONS.md new file mode 100644 index 0000000..1921a14 --- /dev/null +++ b/covert_sd/tools/veracrypt/DOWNLOAD_INSTRUCTIONS.md @@ -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. diff --git a/opsec/README.md b/opsec/README.md new file mode 100644 index 0000000..e376f12 --- /dev/null +++ b/opsec/README.md @@ -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 diff --git a/opsec/configs/cron/opsec-banner-cache b/opsec/configs/cron/opsec-banner-cache new file mode 100644 index 0000000..4c8db67 --- /dev/null +++ b/opsec/configs/cron/opsec-banner-cache @@ -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 diff --git a/opsec/configs/cron/opsec-log-rotate b/opsec/configs/cron/opsec-log-rotate new file mode 100644 index 0000000..690a556 --- /dev/null +++ b/opsec/configs/cron/opsec-log-rotate @@ -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 diff --git a/opsec/configs/desktop/opsec-toggle.desktop b/opsec/configs/desktop/opsec-toggle.desktop new file mode 100644 index 0000000..1610ef0 --- /dev/null +++ b/opsec/configs/desktop/opsec-toggle.desktop @@ -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; diff --git a/opsec/configs/levels/bare-metal-paranoid.conf b/opsec/configs/levels/bare-metal-paranoid.conf new file mode 100644 index 0000000..e09b592 --- /dev/null +++ b/opsec/configs/levels/bare-metal-paranoid.conf @@ -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" diff --git a/opsec/configs/levels/bare-metal-standard.conf b/opsec/configs/levels/bare-metal-standard.conf new file mode 100644 index 0000000..f5ddc4c --- /dev/null +++ b/opsec/configs/levels/bare-metal-standard.conf @@ -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" diff --git a/opsec/configs/levels/cloud-normal.conf b/opsec/configs/levels/cloud-normal.conf new file mode 100644 index 0000000..bafa332 --- /dev/null +++ b/opsec/configs/levels/cloud-normal.conf @@ -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" diff --git a/opsec/configs/levels/cloud-paranoid.conf b/opsec/configs/levels/cloud-paranoid.conf new file mode 100644 index 0000000..01ada3d --- /dev/null +++ b/opsec/configs/levels/cloud-paranoid.conf @@ -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" diff --git a/opsec/configs/opsec-aliases b/opsec/configs/opsec-aliases new file mode 100644 index 0000000..61348ad --- /dev/null +++ b/opsec/configs/opsec-aliases @@ -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 diff --git a/opsec/configs/opsec-country-codes.conf b/opsec/configs/opsec-country-codes.conf new file mode 100644 index 0000000..7e6a291 --- /dev/null +++ b/opsec/configs/opsec-country-codes.conf @@ -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" diff --git a/opsec/configs/opsec.conf b/opsec/configs/opsec.conf new file mode 100644 index 0000000..695422d --- /dev/null +++ b/opsec/configs/opsec.conf @@ -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" diff --git a/opsec/configs/polkit/com.opsec.mode.policy b/opsec/configs/polkit/com.opsec.mode.policy new file mode 100644 index 0000000..c839bd9 --- /dev/null +++ b/opsec/configs/polkit/com.opsec.mode.policy @@ -0,0 +1,17 @@ + + + + + Toggle OPSEC Advanced Mode + Authentication is required to toggle OPSEC mode + + auth_admin + auth_admin + yes + + /usr/local/bin/opsec-mode.sh + true + + diff --git a/opsec/configs/resolv.conf.head b/opsec/configs/resolv.conf.head new file mode 100755 index 0000000..ebbae63 --- /dev/null +++ b/opsec/configs/resolv.conf.head @@ -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 \ No newline at end of file diff --git a/opsec/configs/resolv.conf.opsec b/opsec/configs/resolv.conf.opsec new file mode 100644 index 0000000..e878425 --- /dev/null +++ b/opsec/configs/resolv.conf.opsec @@ -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 diff --git a/opsec/configs/systemd/opsec-boot-advanced.service b/opsec/configs/systemd/opsec-boot-advanced.service new file mode 100644 index 0000000..2fd23fc --- /dev/null +++ b/opsec/configs/systemd/opsec-boot-advanced.service @@ -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 diff --git a/opsec/configs/systemd/opsec-hostname-randomize.service b/opsec/configs/systemd/opsec-hostname-randomize.service new file mode 100644 index 0000000..1dc486a --- /dev/null +++ b/opsec/configs/systemd/opsec-hostname-randomize.service @@ -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 diff --git a/opsec/configs/systemd/opsec-killswitch.service b/opsec/configs/systemd/opsec-killswitch.service new file mode 100644 index 0000000..9909bb2 --- /dev/null +++ b/opsec/configs/systemd/opsec-killswitch.service @@ -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 diff --git a/opsec/configs/systemd/opsec-mac-randomize.service b/opsec/configs/systemd/opsec-mac-randomize.service new file mode 100644 index 0000000..3fad081 --- /dev/null +++ b/opsec/configs/systemd/opsec-mac-randomize.service @@ -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 diff --git a/opsec/configs/themes/aurora.theme b/opsec/configs/themes/aurora.theme new file mode 100644 index 0000000..be8eba1 --- /dev/null +++ b/opsec/configs/themes/aurora.theme @@ -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" diff --git a/opsec/configs/themes/cyberpunk.theme b/opsec/configs/themes/cyberpunk.theme new file mode 100644 index 0000000..0ef20a2 --- /dev/null +++ b/opsec/configs/themes/cyberpunk.theme @@ -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" diff --git a/opsec/configs/themes/default.theme b/opsec/configs/themes/default.theme new file mode 100644 index 0000000..ed0ea5b --- /dev/null +++ b/opsec/configs/themes/default.theme @@ -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" diff --git a/opsec/configs/themes/ember.theme b/opsec/configs/themes/ember.theme new file mode 100644 index 0000000..dd6ee2e --- /dev/null +++ b/opsec/configs/themes/ember.theme @@ -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" diff --git a/opsec/configs/themes/frost.theme b/opsec/configs/themes/frost.theme new file mode 100644 index 0000000..4a9aa8e --- /dev/null +++ b/opsec/configs/themes/frost.theme @@ -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" diff --git a/opsec/configs/themes/slate.theme b/opsec/configs/themes/slate.theme new file mode 100644 index 0000000..c1cae34 --- /dev/null +++ b/opsec/configs/themes/slate.theme @@ -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" diff --git a/opsec/configs/themes/terminal.theme b/opsec/configs/themes/terminal.theme new file mode 100644 index 0000000..f788889 --- /dev/null +++ b/opsec/configs/themes/terminal.theme @@ -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" diff --git a/opsec/configs/torrc/torrc-default b/opsec/configs/torrc/torrc-default new file mode 100644 index 0000000..43a0d43 --- /dev/null +++ b/opsec/configs/torrc/torrc-default @@ -0,0 +1,5 @@ +# Minimal stock Tor configuration +# Restored by opsec-mode off + +SocksPort 9050 +Log notice file /var/log/tor/notices.log diff --git a/opsec/configs/torrc/torrc-opsec b/opsec/configs/torrc/torrc-opsec new file mode 100644 index 0000000..1db6bc1 --- /dev/null +++ b/opsec/configs/torrc/torrc-opsec @@ -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 diff --git a/opsec/configs/udev/99-opsec-usb.rules b/opsec/configs/udev/99-opsec-usb.rules new file mode 100644 index 0000000..1de6435 --- /dev/null +++ b/opsec/configs/udev/99-opsec-usb.rules @@ -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'" diff --git a/opsec/configs/vpn-templates/double-hop-ovpn.conf.template b/opsec/configs/vpn-templates/double-hop-ovpn.conf.template new file mode 100644 index 0000000..430c879 --- /dev/null +++ b/opsec/configs/vpn-templates/double-hop-ovpn.conf.template @@ -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 diff --git a/opsec/configs/vpn-templates/wireguard-multihop.conf.template b/opsec/configs/vpn-templates/wireguard-multihop.conf.template new file mode 100644 index 0000000..6af4378 --- /dev/null +++ b/opsec/configs/vpn-templates/wireguard-multihop.conf.template @@ -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 diff --git a/opsec/conky/conky-opsec-cache.sh b/opsec/conky/conky-opsec-cache.sh new file mode 100755 index 0000000..61a6897 --- /dev/null +++ b/opsec/conky/conky-opsec-cache.sh @@ -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" </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 diff --git a/opsec/conky/conky-opsec-status.sh b/opsec/conky/conky-opsec-status.sh new file mode 100755 index 0000000..92333a1 --- /dev/null +++ b/opsec/conky/conky-opsec-status.sh @@ -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}" diff --git a/opsec/conky/conky-opsec-widget.conf b/opsec/conky/conky-opsec-widget.conf new file mode 100644 index 0000000..016f072 --- /dev/null +++ b/opsec/conky/conky-opsec-widget.conf @@ -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} +]]; diff --git a/opsec/conky/opsec-widget-launch.sh b/opsec/conky/opsec-widget-launch.sh new file mode 100755 index 0000000..646c0d6 --- /dev/null +++ b/opsec/conky/opsec-widget-launch.sh @@ -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)" diff --git a/opsec/docs/OPSEC-CHECKLIST.md b/opsec/docs/OPSEC-CHECKLIST.md new file mode 100755 index 0000000..5b41d61 --- /dev/null +++ b/opsec/docs/OPSEC-CHECKLIST.md @@ -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 ` +- [ ] 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 diff --git a/opsec/install.sh b/opsec/install.sh new file mode 100755 index 0000000..1aae308 --- /dev/null +++ b/opsec/install.sh @@ -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 "" diff --git a/opsec/lib/opsec-lib.sh b/opsec/lib/opsec-lib.sh new file mode 100755 index 0000000..994d87a --- /dev/null +++ b/opsec/lib/opsec-lib.sh @@ -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 [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 diff --git a/opsec/scripts/opsec-banner-cache.sh b/opsec/scripts/opsec-banner-cache.sh new file mode 100755 index 0000000..6939b63 --- /dev/null +++ b/opsec/scripts/opsec-banner-cache.sh @@ -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 diff --git a/opsec/scripts/opsec-banner.sh b/opsec/scripts/opsec-banner.sh new file mode 100755 index 0000000..b894afd --- /dev/null +++ b/opsec/scripts/opsec-banner.sh @@ -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 diff --git a/opsec/scripts/opsec-boot-init.sh b/opsec/scripts/opsec-boot-init.sh new file mode 100755 index 0000000..fa4a748 --- /dev/null +++ b/opsec/scripts/opsec-boot-init.sh @@ -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 diff --git a/opsec/scripts/opsec-check.sh b/opsec/scripts/opsec-check.sh new file mode 100755 index 0000000..6924dea --- /dev/null +++ b/opsec/scripts/opsec-check.sh @@ -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 =====" \ No newline at end of file diff --git a/opsec/scripts/opsec-config.sh b/opsec/scripts/opsec-config.sh new file mode 100755 index 0000000..f967ebc --- /dev/null +++ b/opsec/scripts/opsec-config.sh @@ -0,0 +1,1044 @@ +#!/bin/bash +# /usr/local/bin/opsec-config.sh — Interactive OPSEC Configuration Manager +# TUI: whiptail primary, pure ANSI fallback +# CLI: --boot, --profile, --theme, --set, --get, --apply +# +# Usage: +# sudo opsec-config.sh # Interactive TUI +# sudo opsec-config.sh --boot on|off # Toggle boot-into-advanced +# sudo opsec-config.sh --profile save|load|list|delete NAME +# sudo opsec-config.sh --set KEY VALUE # Set config value +# sudo opsec-config.sh --get KEY # Get config value +# sudo opsec-config.sh --apply # Regenerate configs and restart services + +set -euo pipefail + +if [ "$EUID" -ne 0 ]; then + echo "[!] Please run as root: sudo opsec-config.sh" + exit 1 +fi + +OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh" +BOOT_MARKER="/etc/opsec/boot-advanced.enabled" + +# Source shared library +if [ -f "$OPSEC_LIB" ]; then + . "$OPSEC_LIB" + opsec_load_config 2>/dev/null || true +else + echo "[-] OPSEC library not found at $OPSEC_LIB" + echo " Deploy with: sudo mkdir -p /usr/local/lib/opsec && sudo cp opsec-lib.sh $OPSEC_LIB" + exit 1 +fi + +# ─── TUI DETECTION ───────────────────────────────────────────────────────────── +USE_WHIPTAIL=false +if command -v whiptail >/dev/null 2>&1 && [ -t 0 ]; then + USE_WHIPTAIL=true +fi + +TERM_ROWS=$(tput lines 2>/dev/null || echo 24) +TERM_COLS=$(tput cols 2>/dev/null || echo 80) +WT_HEIGHT=$((TERM_ROWS - 4)) +WT_WIDTH=$((TERM_COLS - 10)) +[ "$WT_HEIGHT" -gt 30 ] && WT_HEIGHT=30 +[ "$WT_WIDTH" -gt 78 ] && WT_WIDTH=78 + +# ─── ANSI COLORS ─────────────────────────────────────────────────────────────── +C_CYAN="\033[38;5;51m" +C_MAG="\033[38;5;201m" +C_GREEN="\033[38;5;49m" +C_RED="\033[38;5;196m" +C_AMBER="\033[38;5;214m" +C_DIM="\033[38;5;244m" +C_WHITE="\033[38;5;255m" +C_BLUE="\033[38;5;75m" +C_RST="\033[0m" + +# ─── ANSI FALLBACK HELPERS ───────────────────────────────────────────────────── + +ansi_banner() { + echo -e "${C_CYAN}╔══════════════════════════════════════════════════╗${C_RST}" + echo -e "${C_CYAN}║${C_MAG} OPSEC CONFIGURATION MANAGER ${C_CYAN}║${C_RST}" + echo -e "${C_CYAN}║${C_DIM} Profile: ${PROFILE_NAME:-default}${C_CYAN}$(printf '%*s' $((36 - ${#PROFILE_NAME:-7})) '')║${C_RST}" + echo -e "${C_CYAN}╚══════════════════════════════════════════════════╝${C_RST}" + echo "" +} + +ansi_menu() { + local title="$1" + shift + local items=("$@") + local choice="" + + echo -e "${C_CYAN}━━━ ${C_MAG}${title}${C_CYAN} ━━━${C_RST}" + echo "" + + local i=1 + for item in "${items[@]}"; do + local num label + num=$(echo "$item" | cut -d'|' -f1) + label=$(echo "$item" | cut -d'|' -f2) + echo -e " ${C_CYAN}${num}${C_RST}) ${C_WHITE}${label}${C_RST}" + i=$((i + 1)) + done + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back / Exit${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + read -r choice + echo "$choice" +} + +ansi_toggle() { + local label="$1" current="$2" + local display + if [ "$current" = "1" ]; then + display="${C_GREEN}ON${C_RST}" + else + display="${C_RED}OFF${C_RST}" + fi + echo -e " ${C_WHITE}${label}: ${display}${C_RST}" +} + +ansi_input() { + local prompt="$1" default="$2" + echo -ne "${C_BLUE}${prompt}${C_RST} [${C_DIM}${default}${C_RST}]: " + local val + read -r val + echo "${val:-$default}" +} + +ansi_confirm() { + local prompt="$1" + echo -ne "${C_AMBER}${prompt} (y/N): ${C_RST}" + local ans + read -r ans + [[ "$ans" =~ ^[Yy] ]] +} + +# ─── WHIPTAIL HELPERS ────────────────────────────────────────────────────────── + +wt_menu() { + local title="$1" + shift + local items=() + while [ $# -gt 0 ]; do + items+=("$1" "$2") + shift 2 + done + whiptail --title "$title" --menu "Profile: ${PROFILE_NAME:-default}" \ + "$WT_HEIGHT" "$WT_WIDTH" $((WT_HEIGHT - 8)) "${items[@]}" 3>&1 1>&2 2>&3 || echo "" +} + +wt_yesno() { + whiptail --title "$1" --yesno "$2" 10 60 3>&1 1>&2 2>&3 +} + +wt_input() { + whiptail --title "$1" --inputbox "$2" 10 60 "$3" 3>&1 1>&2 2>&3 || echo "$3" +} + +wt_toggle() { + local key="$1" label="$2" current + current=$(opsec_get_value "$key") + if [ "$current" = "1" ]; then + opsec_set_value "$key" "0" + opsec_yellow "${label}: OFF" + else + opsec_set_value "$key" "1" + opsec_green "${label}: ON" + fi + opsec_load_config +} + +# ─── MENU IMPLEMENTATIONS ───────────────────────────────────────────────────── + +menu_tor() { + while true; do + opsec_load_config 2>/dev/null || true + local choice + if $USE_WHIPTAIL; then + choice=$(wt_menu "Tor Settings" \ + "1" "Circuit Rotation: ${TOR_CIRCUIT_ROTATION:-30}s" \ + "2" "Exit Blacklist: ${TOR_BLACKLIST:-us,gb,ca,au,nz}" \ + "3" "Strict Nodes: $([ "${TOR_STRICT_NODES:-1}" = "1" ] && echo ON || echo OFF)" \ + "4" "Stream Isolation: $([ "${TOR_ISOLATION:-1}" = "1" ] && echo ON || echo OFF)" \ + "5" "Traffic Padding: $([ "${TOR_PADDING:-1}" = "1" ] && echo ON || echo OFF)" \ + "6" "SOCKS Port: ${TOR_SOCKS_PORT:-9050}" \ + "7" "DNS Port: ${TOR_DNS_PORT:-5353}" \ + "8" "Entry Guards: ${TOR_NUM_GUARDS:-3}" \ + "9" "Blacklist Editor...") + else + clear + ansi_banner + echo -e "${C_CYAN}━━━ ${C_MAG}TOR SETTINGS${C_CYAN} ━━━${C_RST}" + echo "" + echo -e " ${C_CYAN}1${C_RST}) Circuit Rotation: ${C_WHITE}${TOR_CIRCUIT_ROTATION:-30}s${C_RST}" + echo -e " ${C_CYAN}2${C_RST}) Exit Blacklist: ${C_WHITE}${TOR_BLACKLIST:-us,gb,ca,au,nz}${C_RST}" + ansi_toggle "3) Strict Nodes" "${TOR_STRICT_NODES:-1}" + ansi_toggle "4) Stream Isolation" "${TOR_ISOLATION:-1}" + ansi_toggle "5) Traffic Padding" "${TOR_PADDING:-1}" + echo -e " ${C_CYAN}6${C_RST}) SOCKS Port: ${C_WHITE}${TOR_SOCKS_PORT:-9050}${C_RST}" + echo -e " ${C_CYAN}7${C_RST}) DNS Port: ${C_WHITE}${TOR_DNS_PORT:-5353}${C_RST}" + echo -e " ${C_CYAN}8${C_RST}) Entry Guards: ${C_WHITE}${TOR_NUM_GUARDS:-3}${C_RST}" + echo -e " ${C_CYAN}9${C_RST}) ${C_MAG}Blacklist Editor...${C_RST}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + read -r choice + fi + + case "$choice" in + 1) local val; val=$(ansi_input "Circuit rotation (seconds)" "${TOR_CIRCUIT_ROTATION:-30}") + opsec_set_value TOR_CIRCUIT_ROTATION "$val" ;; + 2) local val; val=$(ansi_input "Blacklist (comma-separated country codes)" "${TOR_BLACKLIST:-us,gb,ca,au,nz}") + opsec_set_value TOR_BLACKLIST "$val" ;; + 3) wt_toggle TOR_STRICT_NODES "Strict Nodes" ;; + 4) wt_toggle TOR_ISOLATION "Stream Isolation" ;; + 5) wt_toggle TOR_PADDING "Traffic Padding" ;; + 6) local val; val=$(ansi_input "SOCKS port" "${TOR_SOCKS_PORT:-9050}") + opsec_set_value TOR_SOCKS_PORT "$val" ;; + 7) local val; val=$(ansi_input "DNS port" "${TOR_DNS_PORT:-5353}") + opsec_set_value TOR_DNS_PORT "$val" ;; + 8) local val; val=$(ansi_input "Number of entry guards" "${TOR_NUM_GUARDS:-3}") + opsec_set_value TOR_NUM_GUARDS "$val" ;; + 9) menu_blacklist_editor ;; + 0|"") return ;; + esac + done +} + +menu_blacklist_editor() { + while true; do + opsec_load_config 2>/dev/null || true + opsec_load_country_presets 2>/dev/null || true + local current="${TOR_BLACKLIST:-}" + + clear + echo -e "${C_CYAN}━━━ ${C_MAG}BLACKLIST EDITOR${C_CYAN} ━━━${C_RST}" + echo "" + echo -e "${C_DIM}Current blacklist:${C_RST} ${C_WHITE}${current:-none}${C_RST}" + echo "" + echo -e " ${C_CYAN}1${C_RST}) Add country code" + echo -e " ${C_CYAN}2${C_RST}) Remove country code" + echo -e " ${C_CYAN}3${C_RST}) Load preset: ${C_GREEN}Five Eyes${C_RST}" + echo -e " ${C_CYAN}4${C_RST}) Load preset: ${C_AMBER}Nine Eyes${C_RST}" + echo -e " ${C_CYAN}5${C_RST}) Load preset: ${C_RED}Fourteen Eyes${C_RST}" + echo -e " ${C_CYAN}6${C_RST}) Load preset: ${C_MAG}Max Exclusion${C_RST}" + echo -e " ${C_CYAN}7${C_RST}) Clear blacklist" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) + local code + code=$(ansi_input "Country code to add (e.g. us, de, cn)" "") + if [ -n "$code" ]; then + if [ -z "$current" ]; then + opsec_set_value TOR_BLACKLIST "$code" + else + # Avoid duplicates + if ! echo ",$current," | grep -q ",$code,"; then + opsec_set_value TOR_BLACKLIST "${current},${code}" + else + opsec_yellow "${code} already in blacklist" + sleep 1 + fi + fi + fi + ;; + 2) + local code + code=$(ansi_input "Country code to remove" "") + if [ -n "$code" ]; then + local new_list + new_list=$(echo "$current" | tr ',' '\n' | grep -v "^${code}$" | paste -sd,) + opsec_set_value TOR_BLACKLIST "$new_list" + fi + ;; + 3) opsec_set_value TOR_BLACKLIST "$FIVE_EYES"; opsec_green "Five Eyes preset loaded" ; sleep 1 ;; + 4) opsec_set_value TOR_BLACKLIST "$NINE_EYES"; opsec_green "Nine Eyes preset loaded" ; sleep 1 ;; + 5) opsec_set_value TOR_BLACKLIST "$FOURTEEN_EYES"; opsec_green "Fourteen Eyes preset loaded" ; sleep 1 ;; + 6) opsec_set_value TOR_BLACKLIST "$MAX_EXCLUSION"; opsec_green "Max exclusion preset loaded" ; sleep 1 ;; + 7) opsec_set_value TOR_BLACKLIST ""; opsec_yellow "Blacklist cleared" ; sleep 1 ;; + 0|"") return ;; + esac + done +} + +menu_dns() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}DNS SETTINGS${C_CYAN} ━━━${C_RST}" + echo "" + echo -e "${C_DIM}Current mode:${C_RST} ${C_WHITE}${DNS_MODE:-tor}${C_RST}" + [ -n "${DNS_CUSTOM_SERVERS:-}" ] && echo -e "${C_DIM}Custom servers:${C_RST} ${C_WHITE}${DNS_CUSTOM_SERVERS}${C_RST}" + echo "" + echo -e " ${C_CYAN}1${C_RST}) ${C_GREEN}tor${C_RST} — Route DNS through Tor (127.0.0.1)" + echo -e " ${C_CYAN}2${C_RST}) ${C_WHITE}quad9${C_RST} — Quad9 (9.9.9.9 / 149.112.112.112)" + echo -e " ${C_CYAN}3${C_RST}) ${C_WHITE}cloudflare${C_RST} — Cloudflare (1.1.1.1 / 1.0.0.1)" + echo -e " ${C_CYAN}4${C_RST}) ${C_AMBER}doh${C_RST} — DNS-over-HTTPS via systemd-resolved" + echo -e " ${C_CYAN}5${C_RST}) ${C_AMBER}dot${C_RST} — DNS-over-TLS via systemd-resolved" + echo -e " ${C_CYAN}6${C_RST}) ${C_MAG}custom${C_RST} — Specify custom servers" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) opsec_set_value DNS_MODE "tor" ;; + 2) opsec_set_value DNS_MODE "quad9" ;; + 3) opsec_set_value DNS_MODE "cloudflare" ;; + 4) opsec_set_value DNS_MODE "doh" ;; + 5) opsec_set_value DNS_MODE "dot" ;; + 6) + local servers + servers=$(ansi_input "DNS servers (comma-separated IPs)" "${DNS_CUSTOM_SERVERS:-}") + opsec_set_value DNS_MODE "custom" + opsec_set_value DNS_CUSTOM_SERVERS "$servers" + ;; + 0|"") return ;; + esac + done +} + +menu_killswitch() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}KILL SWITCH SETTINGS${C_CYAN} ━━━${C_RST}" + echo "" + ansi_toggle "1) Allow DHCP" "${KILLSWITCH_ALLOW_DHCP:-1}" + ansi_toggle "2) Allow OpenVPN" "${KILLSWITCH_ALLOW_OPENVPN:-1}" + ansi_toggle "3) Allow WireGuard" "${KILLSWITCH_ALLOW_WIREGUARD:-1}" + echo -e " ${C_CYAN}4${C_RST}) Extra Ports: ${C_WHITE}${KILLSWITCH_EXTRA_PORTS:-none}${C_RST}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) wt_toggle KILLSWITCH_ALLOW_DHCP "DHCP" ;; + 2) wt_toggle KILLSWITCH_ALLOW_OPENVPN "OpenVPN" ;; + 3) wt_toggle KILLSWITCH_ALLOW_WIREGUARD "WireGuard" ;; + 4) local val; val=$(ansi_input "Extra ports (format: tcp:443,udp:8080)" "${KILLSWITCH_EXTRA_PORTS:-}") + opsec_set_value KILLSWITCH_EXTRA_PORTS "$val" ;; + 0|"") return ;; + esac + done +} + +menu_mac() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}MAC ADDRESS SETTINGS${C_CYAN} ━━━${C_RST}" + echo "" + echo -e " ${C_CYAN}1${C_RST}) Interfaces: ${C_WHITE}${MAC_INTERFACES:-auto}${C_RST}" + echo -e " ${C_CYAN}2${C_RST}) Vendor Spoof OUI: ${C_WHITE}${MAC_VENDOR_SPOOF:-random}${C_RST}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) local val; val=$(ansi_input "Interfaces (auto or comma-separated, e.g. eth0,wlan0)" "${MAC_INTERFACES:-auto}") + opsec_set_value MAC_INTERFACES "$val" ;; + 2) local val; val=$(ansi_input "Vendor OUI prefix (e.g. 00:1A:2B) or empty for random" "${MAC_VENDOR_SPOOF:-}") + opsec_set_value MAC_VENDOR_SPOOF "$val" ;; + 0|"") return ;; + esac + done +} + +menu_hostname() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}HOSTNAME SETTINGS${C_CYAN} ━━━${C_RST}" + echo "" + echo -e " ${C_CYAN}1${C_RST}) Pattern: ${C_WHITE}${HOSTNAME_PATTERN:-desktop}${C_RST}" + echo -e " ${C_DIM}desktop = desktop-XXXX | random = XXXXXXXX | custom = PREFIX-XXXX${C_RST}" + echo -e " ${C_CYAN}2${C_RST}) Custom Prefix: ${C_WHITE}${HOSTNAME_CUSTOM_PREFIX:-}${C_RST}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) local val; val=$(ansi_input "Pattern (desktop/random/custom)" "${HOSTNAME_PATTERN:-desktop}") + opsec_set_value HOSTNAME_PATTERN "$val" ;; + 2) local val; val=$(ansi_input "Custom prefix" "${HOSTNAME_CUSTOM_PREFIX:-}") + opsec_set_value HOSTNAME_CUSTOM_PREFIX "$val" ;; + 0|"") return ;; + esac + done +} + +menu_hardening() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}SYSTEM HARDENING${C_CYAN} ━━━${C_RST}" + echo "" + ansi_toggle "1) Disable IPv6" "${HARDEN_IPV6:-1}" + ansi_toggle "2) Disable Swap" "${HARDEN_SWAP:-1}" + ansi_toggle "3) Disable Core Dumps" "${HARDEN_CORE_DUMPS:-1}" + ansi_toggle "4) Clipboard Auto-Clear" "${HARDEN_CLIPBOARD_CLEAR:-0}" + ansi_toggle "5) Screen Lock" "${HARDEN_SCREEN_LOCK:-1}" + echo -e " ${C_CYAN}6${C_RST}) Screen Lock Timeout: ${C_WHITE}${HARDEN_SCREEN_LOCK_TIMEOUT:-300}s${C_RST}" + ansi_toggle "7) Timezone Spoof" "${HARDEN_TIMEZONE_SPOOF:-0}" + echo -e " ${C_CYAN}8${C_RST}) Timezone Value: ${C_WHITE}${HARDEN_TIMEZONE_VALUE:-UTC}${C_RST}" + ansi_toggle "9) Locale Spoof" "${HARDEN_LOCALE_SPOOF:-0}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) wt_toggle HARDEN_IPV6 "IPv6 Disable" ;; + 2) wt_toggle HARDEN_SWAP "Swap Disable" ;; + 3) wt_toggle HARDEN_CORE_DUMPS "Core Dumps Disable" ;; + 4) wt_toggle HARDEN_CLIPBOARD_CLEAR "Clipboard Auto-Clear" ;; + 5) wt_toggle HARDEN_SCREEN_LOCK "Screen Lock" ;; + 6) local val; val=$(ansi_input "Lock timeout (seconds)" "${HARDEN_SCREEN_LOCK_TIMEOUT:-300}") + opsec_set_value HARDEN_SCREEN_LOCK_TIMEOUT "$val" ;; + 7) wt_toggle HARDEN_TIMEZONE_SPOOF "Timezone Spoof" ;; + 8) local val; val=$(ansi_input "Timezone (e.g. UTC, Europe/London)" "${HARDEN_TIMEZONE_VALUE:-UTC}") + opsec_set_value HARDEN_TIMEZONE_VALUE "$val" ;; + 9) wt_toggle HARDEN_LOCALE_SPOOF "Locale Spoof" ;; + 0|"") return ;; + esac + done +} + +menu_leak_prevention() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}LEAK PREVENTION${C_CYAN} ━━━${C_RST}" + echo "" + ansi_toggle "1) WebRTC Blocking" "${LEAK_WEBRTC_BLOCK:-1}" + ansi_toggle "2) USB Device Blocking" "${LEAK_USB_BLOCK:-0}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) wt_toggle LEAK_WEBRTC_BLOCK "WebRTC Block" ;; + 2) wt_toggle LEAK_USB_BLOCK "USB Block" ;; + 0|"") return ;; + esac + done +} + +menu_monitoring() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}NETWORK MONITORING${C_CYAN} ━━━${C_RST}" + echo "" + ansi_toggle "1) Process Monitor" "${MONITOR_PROCESSES:-0}" + ansi_toggle "2) Traffic Jitter" "${TRAFFIC_JITTER_ENABLED:-0}" + echo -e " ${C_CYAN}3${C_RST}) Jitter Delay: ${C_WHITE}${TRAFFIC_JITTER_MS:-50}ms${C_RST}" + ansi_toggle "4) Log Rotation" "${MONITOR_LOG_ROTATION:-1}" + echo -e " ${C_CYAN}5${C_RST}) Log Rotation Interval: ${C_WHITE}${LOG_ROTATION_HOURS:-4}h${C_RST}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) wt_toggle MONITOR_PROCESSES "Process Monitor" ;; + 2) wt_toggle TRAFFIC_JITTER_ENABLED "Traffic Jitter" ;; + 3) local val; val=$(ansi_input "Jitter delay (ms)" "${TRAFFIC_JITTER_MS:-50}") + opsec_set_value TRAFFIC_JITTER_MS "$val" ;; + 4) wt_toggle MONITOR_LOG_ROTATION "Log Rotation" ;; + 5) local val; val=$(ansi_input "Rotation interval (hours)" "${LOG_ROTATION_HOURS:-4}") + opsec_set_value LOG_ROTATION_HOURS "$val" ;; + 0|"") return ;; + esac + done +} + +menu_boot_mode() { + clear + echo -e "${C_CYAN}━━━ ${C_MAG}BOOT MODE${C_CYAN} ━━━${C_RST}" + echo "" + if [ -f "$BOOT_MARKER" ]; then + echo -e "${C_GREEN}Boot-into-advanced is: ENABLED${C_RST}" + echo -e "${C_DIM}System will boot into advanced OPSEC mode automatically${C_RST}" + echo "" + if ansi_confirm "Disable boot-into-advanced?"; then + rm -f "$BOOT_MARKER" + systemctl disable opsec-boot-advanced.service 2>/dev/null || true + opsec_green "Boot-into-advanced disabled" + fi + else + echo -e "${C_AMBER}Boot-into-advanced is: DISABLED${C_RST}" + echo -e "${C_DIM}System boots in standard mode (manual activation)${C_RST}" + echo "" + if ansi_confirm "Enable boot-into-advanced?"; then + mkdir -p /etc/opsec + touch "$BOOT_MARKER" + systemctl enable opsec-boot-advanced.service 2>/dev/null || true + opsec_green "Boot-into-advanced enabled" + fi + fi + sleep 1 +} + +menu_profiles() { + while true; do + clear + echo -e "${C_CYAN}━━━ ${C_MAG}PROFILE MANAGEMENT${C_CYAN} ━━━${C_RST}" + echo "" + echo -e "${C_DIM}Active profile:${C_RST} ${C_WHITE}${PROFILE_NAME:-default}${C_RST}" + echo "" + + # List existing profiles + local profiles + profiles=$(opsec_profile_list 2>/dev/null) + if [ -n "$profiles" ]; then + echo -e "${C_DIM}Saved profiles:${C_RST}" + echo "$profiles" | while read -r p; do + if [ "$p" = "${PROFILE_NAME:-default}" ]; then + echo -e " ${C_GREEN}▸ ${p} (active)${C_RST}" + else + echo -e " ${C_DIM} ${p}${C_RST}" + fi + done + echo "" + fi + + echo -e " ${C_CYAN}1${C_RST}) Save current config as profile" + echo -e " ${C_CYAN}2${C_RST}) Load profile" + echo -e " ${C_CYAN}3${C_RST}) Delete profile" + echo -e " ${C_CYAN}4${C_RST}) Export profile to file" + echo -e " ${C_CYAN}5${C_RST}) Import profile from file" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) + local name + name=$(ansi_input "Profile name" "") + if [ -n "$name" ]; then + opsec_profile_save "$name" + opsec_set_value PROFILE_NAME "$name" + opsec_green "Profile '${name}' saved" + sleep 1 + fi + ;; + 2) + local name + name=$(ansi_input "Profile name to load" "") + if [ -n "$name" ]; then + if opsec_profile_load "$name"; then + opsec_load_config + opsec_green "Profile '${name}' loaded" + else + opsec_red "Profile '${name}' not found" + fi + sleep 1 + fi + ;; + 3) + local name + name=$(ansi_input "Profile name to delete" "") + if [ -n "$name" ] && ansi_confirm "Delete profile '${name}'?"; then + opsec_profile_delete "$name" + opsec_green "Profile '${name}' deleted" + sleep 1 + fi + ;; + 4) + local name dest + name=$(ansi_input "Profile name to export" "${PROFILE_NAME:-default}") + dest=$(ansi_input "Export path" "/tmp/${name}.conf") + if opsec_profile_export "$name" "$dest" 2>/dev/null || cp "$OPSEC_CONF" "$dest"; then + opsec_green "Exported to ${dest}" + else + opsec_red "Export failed" + fi + sleep 1 + ;; + 5) + local src name + src=$(ansi_input "Import file path" "") + name=$(ansi_input "Profile name" "") + if [ -n "$src" ] && [ -n "$name" ]; then + if opsec_profile_import "$src" "$name"; then + opsec_green "Profile '${name}' imported" + else + opsec_red "Import failed (file not found?)" + fi + sleep 1 + fi + ;; + 0|"") return ;; + esac + done +} + +menu_tor_bridges() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}TOR BRIDGES (PLUGGABLE TRANSPORTS)${C_CYAN} ━━━${C_RST}" + echo "" + echo -e "${C_DIM}Current mode:${C_RST} ${C_WHITE}${TOR_BRIDGE_MODE:-off}${C_RST}" + [ -n "${TOR_BRIDGE_RELAY:-}" ] && echo -e "${C_DIM}Custom relay:${C_RST} ${C_WHITE}${TOR_BRIDGE_RELAY}${C_RST}" + echo "" + echo -e " ${C_CYAN}1${C_RST}) ${C_GREEN}off${C_RST} — Direct Tor connection (no bridge)" + echo -e " ${C_CYAN}2${C_RST}) ${C_AMBER}obfs4${C_RST} — obfs4 obfuscated bridge (recommended)" + echo -e " ${C_CYAN}3${C_RST}) ${C_AMBER}meek-azure${C_RST} — Domain-fronted via Azure CDN" + echo -e " ${C_CYAN}4${C_RST}) ${C_AMBER}snowflake${C_RST} — WebRTC-based (resembles video call)" + echo -e " ${C_CYAN}5${C_RST}) ${C_WHITE}Custom relay${C_RST} — Set bridge relay line manually" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) opsec_set_value TOR_BRIDGE_MODE "off"; opsec_green "Bridges disabled" ; sleep 1 ;; + 2) opsec_set_value TOR_BRIDGE_MODE "obfs4"; opsec_green "Bridge mode: obfs4" ; sleep 1 ;; + 3) opsec_set_value TOR_BRIDGE_MODE "meek-azure"; opsec_green "Bridge mode: meek-azure" ; sleep 1 ;; + 4) opsec_set_value TOR_BRIDGE_MODE "snowflake"; opsec_green "Bridge mode: snowflake" ; sleep 1 ;; + 5) + local relay + relay=$(ansi_input "Bridge relay line (e.g. obfs4 IP:PORT FINGERPRINT ...)" "${TOR_BRIDGE_RELAY:-}") + opsec_set_value TOR_BRIDGE_RELAY "$relay" + ;; + 0|"") return ;; + esac + done +} + +menu_widget_theme() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}WIDGET THEME${C_CYAN} ━━━${C_RST}" + echo "" + echo -e "${C_DIM}Current theme:${C_RST} ${C_WHITE}${WIDGET_THEME:-default}${C_RST}" + echo "" + + # List available themes with numbers + local themes=() + local i=1 + if [ -d /etc/opsec/themes ]; then + while IFS= read -r t; do + [ -z "$t" ] && continue + # Source theme to get label + local label="$t" + if [ -f "/etc/opsec/themes/${t}.theme" ]; then + label=$(grep '^THEME_LABEL=' "/etc/opsec/themes/${t}.theme" 2>/dev/null | cut -d'"' -f2) + [ -z "$label" ] && label="$t" + fi + themes+=("$t") + local marker="" + if [ "$t" = "${WIDGET_THEME:-default}" ]; then + marker=" ${C_GREEN}◀ active${C_RST}" + fi + echo -e " ${C_CYAN}${i}${C_RST}) ${C_WHITE}${label}${C_RST} ${C_DIM}(${t})${C_RST}${marker}" + i=$((i + 1)) + done < <(opsec_theme_list 2>/dev/null) + fi + + if [ ${#themes[@]} -eq 0 ]; then + echo -e "${C_RED}No themes found in /etc/opsec/themes/${C_RST}" + echo -e "${C_DIM}Deploy with playbook first.${C_RST}" + echo "" + echo -e "${C_DIM}Press Enter to go back...${C_RST}" + read -r + return + fi + + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + if [ "$choice" = "0" ] || [ -z "$choice" ]; then + return + fi + + # Validate numeric choice + if [[ "$choice" =~ ^[0-9]+$ ]] && [ "$choice" -ge 1 ] && [ "$choice" -le ${#themes[@]} ]; then + local selected="${themes[$((choice - 1))]}" + opsec_set_value WIDGET_THEME "$selected" + opsec_load_config + opsec_info "Applying theme '${selected}'..." + opsec_generate_conky + opsec_green "Theme '${selected}' applied" + sleep 1 + fi + done +} + +do_apply() { + echo "" + opsec_hdr "APPLYING CONFIGURATION" + echo "" + + opsec_load_config + + # Regenerate torrc + opsec_info "Regenerating torrc..." + opsec_generate_torrc + opsec_green "Torrc regenerated" + + # Regenerate resolv.conf if in advanced mode + if opsec_is_advanced; then + opsec_info "Regenerating resolv.conf..." + opsec_generate_resolv + opsec_green "DNS config regenerated" + + # Restart Tor + opsec_info "Restarting Tor..." + systemctl restart tor 2>/dev/null || true + + # Reapply hardening + if [ -x /usr/local/bin/opsec-harden.sh ]; then + opsec_info "Reapplying hardening..." + /usr/local/bin/opsec-harden.sh apply + fi + + # Reapply kill switch + opsec_info "Reapplying kill switch..." + /usr/local/bin/opsec-killswitch.sh on 2>/dev/null || true + fi + + # Regenerate conky widget with current theme + opsec_info "Regenerating conky widget..." + opsec_generate_conky 2>/dev/null && opsec_green "Widget theme applied" || true + + opsec_green "Configuration applied" + echo "" +} + +menu_deployment_level() { + while true; do + opsec_load_config 2>/dev/null || true + clear + echo -e "${C_CYAN}━━━ ${C_MAG}DEPLOYMENT LEVEL${C_CYAN} ━━━${C_RST}" + echo "" + echo -e "${C_DIM}Current level:${C_RST} ${C_WHITE}${DEPLOYMENT_LEVEL:-bare-metal}${C_RST}" + echo "" + + # List available levels + local levels + levels=$(opsec_level_list 2>/dev/null) + if [ -z "$levels" ]; then + echo -e "${C_RED}No levels found in /etc/opsec/levels/${C_RST}" + echo -e "${C_DIM}Deploy with playbook first.${C_RST}" + echo "" + echo -e "${C_DIM}Press Enter to go back...${C_RST}" + read -r + return + fi + + echo -e " ${C_CYAN}1${C_RST}) ${C_WHITE}bare-metal-standard${C_RST} ${C_DIM}— Physical box, privacy base + ghost toggle${C_RST}" + echo -e " ${C_CYAN}2${C_RST}) ${C_RED}bare-metal-paranoid${C_RST} ${C_DIM}— Physical box, ghost mode always on${C_RST}" + echo -e " ${C_CYAN}3${C_RST}) ${C_AMBER}cloud-normal${C_RST} ${C_DIM}— Cloud VPS, privacy base + ghost toggle${C_RST}" + echo -e " ${C_CYAN}4${C_RST}) ${C_RED}cloud-paranoid${C_RST} ${C_DIM}— Cloud VPS, ghost mode always on${C_RST}" + echo "" + echo -e " ${C_CYAN}5${C_RST}) ${C_BLUE}Banner Mode${C_RST} ${C_DIM}— Terminal banner: ${OPSEC_BANNER:-compact}${C_RST}" + echo -e " ${C_RED}0${C_RST}) ${C_DIM}Back${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) if ansi_confirm "Apply 'bare-metal-standard'? (privacy base + ghost toggle)"; then + opsec_level_apply "bare-metal-standard" && opsec_green "Level 'bare-metal-standard' applied" || opsec_red "Failed to apply level" + sleep 1 + fi ;; + 2) if ansi_confirm "Apply 'bare-metal-paranoid'? (ghost mode always on)"; then + opsec_level_apply "bare-metal-paranoid" && opsec_green "Level 'bare-metal-paranoid' applied" || opsec_red "Failed to apply level" + sleep 1 + fi ;; + 3) if ansi_confirm "Apply 'cloud-normal'? (cloud privacy base + ghost toggle)"; then + opsec_level_apply "cloud-normal" && opsec_green "Level 'cloud-normal' applied" || opsec_red "Failed to apply level" + sleep 1 + fi ;; + 4) if ansi_confirm "Apply 'cloud-paranoid'? (cloud ghost mode always on)"; then + opsec_level_apply "cloud-paranoid" && opsec_green "Level 'cloud-paranoid' applied" || opsec_red "Failed to apply level" + sleep 1 + fi ;; + 5) + echo "" + echo -e " ${C_CYAN}a${C_RST}) compact" + echo -e " ${C_CYAN}b${C_RST}) full" + echo -e " ${C_CYAN}c${C_RST}) off" + echo -ne "${C_BLUE}▸ ${C_RST}" + local bchoice + read -r bchoice + case "$bchoice" in + a) opsec_set_value OPSEC_BANNER "compact"; opsec_green "Banner: compact" ;; + b) opsec_set_value OPSEC_BANNER "full"; opsec_green "Banner: full" ;; + c) opsec_set_value OPSEC_BANNER "off"; opsec_green "Banner: off" ;; + esac + sleep 1 + ;; + 0|"") return ;; + esac + done +} + +menu_apply() { + if ansi_confirm "Regenerate configs and restart affected services?"; then + do_apply + echo "" + echo -e "${C_DIM}Press Enter to continue...${C_RST}" + read -r + fi +} + +# ─── MAIN MENU ───────────────────────────────────────────────────────────────── + +main_menu() { + while true; do + opsec_load_config 2>/dev/null || true + clear + ansi_banner + + local mode_str + if [ -f /var/run/opsec-advanced.enabled ]; then + mode_str="${C_GREEN}ADVANCED${C_RST}" + else + mode_str="${C_AMBER}STANDARD${C_RST}" + fi + + echo -e " ${C_DIM}Mode: ${mode_str} ${C_DIM}Boot: $([ -f "$BOOT_MARKER" ] && echo -e "${C_GREEN}PERSIST" || echo -e "${C_AMBER}MANUAL")${C_RST}" + echo "" + echo -e " ${C_CYAN} 1${C_RST}) ${C_WHITE}Tor Settings${C_RST} ${C_DIM}— circuits, blacklist, isolation, padding${C_RST}" + echo -e " ${C_CYAN} 2${C_RST}) ${C_WHITE}DNS Settings${C_RST} ${C_DIM}— tor/quad9/cloudflare/doh/dot/custom${C_RST}" + echo -e " ${C_CYAN} 3${C_RST}) ${C_WHITE}Kill Switch${C_RST} ${C_DIM}— DHCP/VPN toggles, extra ports${C_RST}" + echo -e " ${C_CYAN} 4${C_RST}) ${C_WHITE}MAC Address${C_RST} ${C_DIM}— interfaces, vendor spoof${C_RST}" + echo -e " ${C_CYAN} 5${C_RST}) ${C_WHITE}Hostname${C_RST} ${C_DIM}— pattern, custom prefix${C_RST}" + echo -e " ${C_CYAN} 6${C_RST}) ${C_WHITE}System Hardening${C_RST} ${C_DIM}— IPv6, swap, core dumps, clipboard, etc.${C_RST}" + echo -e " ${C_CYAN} 7${C_RST}) ${C_WHITE}Leak Prevention${C_RST} ${C_DIM}— WebRTC, USB blocking${C_RST}" + echo -e " ${C_CYAN} 8${C_RST}) ${C_WHITE}Network Monitoring${C_RST} ${C_DIM}— process monitor, traffic jitter${C_RST}" + echo -e " ${C_CYAN} 9${C_RST}) ${C_WHITE}Boot Mode${C_RST} ${C_DIM}— toggle boot-into-advanced${C_RST}" + echo -e " ${C_CYAN}10${C_RST}) ${C_WHITE}Profiles${C_RST} ${C_DIM}— save/load/list/delete/export/import${C_RST}" + echo -e " ${C_CYAN}11${C_RST}) ${C_WHITE}Deployment Level${C_RST} ${C_DIM}— ${DEPLOYMENT_LEVEL:-bare-metal} | banner: ${OPSEC_BANNER:-compact}${C_RST}" + echo -e " ${C_CYAN}12${C_RST}) ${C_WHITE}Tor Bridges${C_RST} ${C_DIM}— pluggable transports: ${TOR_BRIDGE_MODE:-off}${C_RST}" + echo -e " ${C_CYAN}13${C_RST}) ${C_WHITE}Widget Theme${C_RST} ${C_DIM}— conky color theme: ${WIDGET_THEME:-default}${C_RST}" + echo -e " ${C_MAG}14${C_RST}) ${C_MAG}Apply & Restart${C_RST} ${C_DIM}— regenerate configs, restart services${C_RST}" + echo -e " ${C_RED} 0${C_RST}) ${C_DIM}Exit${C_RST}" + echo "" + echo -ne "${C_BLUE}▸ ${C_RST}" + local choice + read -r choice + + case "$choice" in + 1) menu_tor ;; + 2) menu_dns ;; + 3) menu_killswitch ;; + 4) menu_mac ;; + 5) menu_hostname ;; + 6) menu_hardening ;; + 7) menu_leak_prevention ;; + 8) menu_monitoring ;; + 9) menu_boot_mode ;; + 10) menu_profiles ;; + 11) menu_deployment_level ;; + 12) menu_tor_bridges ;; + 13) menu_widget_theme ;; + 14) menu_apply ;; + 0|"") + clear + exit 0 + ;; + esac + done +} + +# ─── CLI MODE ────────────────────────────────────────────────────────────────── + +cli_boot() { + case "${1:-}" in + on) + mkdir -p /etc/opsec + touch "$BOOT_MARKER" + systemctl enable opsec-boot-advanced.service 2>/dev/null || true + opsec_green "Boot-into-advanced: ENABLED" + ;; + off) + rm -f "$BOOT_MARKER" + systemctl disable opsec-boot-advanced.service 2>/dev/null || true + opsec_green "Boot-into-advanced: DISABLED" + ;; + *) + if [ -f "$BOOT_MARKER" ]; then + echo "enabled" + else + echo "disabled" + fi + ;; + esac +} + +cli_profile() { + local action="${1:-}" name="${2:-}" + case "$action" in + save) + [ -z "$name" ] && { echo "Usage: --profile save NAME"; exit 1; } + opsec_profile_save "$name" + opsec_set_value PROFILE_NAME "$name" + opsec_green "Profile '${name}' saved" + ;; + load) + [ -z "$name" ] && { echo "Usage: --profile load NAME"; exit 1; } + if opsec_profile_load "$name"; then + opsec_green "Profile '${name}' loaded" + else + opsec_red "Profile '${name}' not found" + exit 1 + fi + ;; + list) + opsec_profile_list + ;; + delete) + [ -z "$name" ] && { echo "Usage: --profile delete NAME"; exit 1; } + opsec_profile_delete "$name" + opsec_green "Profile '${name}' deleted" + ;; + *) + echo "Usage: --profile save|load|list|delete [NAME]" + exit 1 + ;; + esac +} + +# ─── ARGUMENT PARSING ───────────────────────────────────────────────────────── + +case "${1:-}" in + --boot) + cli_boot "${2:-}" + ;; + --profile) + cli_profile "${2:-}" "${3:-}" + ;; + --level) + case "${2:-}" in + apply) + [ -z "${3:-}" ] && { echo "Usage: --level apply NAME"; echo "Available: $(opsec_level_list 2>/dev/null | tr '\n' ' ')"; exit 1; } + opsec_level_apply "$3" && opsec_green "Level '${3}' applied" + ;; + list) + opsec_level_list + ;; + *) + echo "Usage: --level apply|list [NAME]" + echo "Available levels: $(opsec_level_list 2>/dev/null | tr '\n' ' ')" + exit 1 + ;; + esac + ;; + --banner) + case "${2:-}" in + compact|full|off) + opsec_set_value OPSEC_BANNER "$2" + opsec_green "Banner mode: ${2}" + ;; + *) + echo "Usage: --banner compact|full|off" + echo "Current: $(opsec_get_value OPSEC_BANNER)" + exit 1 + ;; + esac + ;; + --theme) + case "${2:-}" in + list) + _themes=$(opsec_theme_list 2>/dev/null) + if [ -z "$_themes" ]; then + echo "No themes found in /etc/opsec/themes/" + exit 1 + fi + _current=$(opsec_get_value WIDGET_THEME) + echo "$_themes" | while IFS= read -r t; do + if [ "$t" = "$_current" ]; then + echo "* ${t} (active)" + else + echo " ${t}" + fi + done + ;; + apply) + [ -z "${3:-}" ] && { echo "Usage: --theme apply NAME"; echo "Available: $(opsec_theme_list 2>/dev/null | tr '\n' ' ')"; exit 1; } + if [ ! -f "/etc/opsec/themes/${3}.theme" ]; then + opsec_red "Theme '${3}' not found" + echo "Available: $(opsec_theme_list 2>/dev/null | tr '\n' ' ')" + exit 1 + fi + opsec_set_value WIDGET_THEME "$3" + opsec_load_config + opsec_generate_conky && opsec_green "Theme '${3}' applied" + ;; + *) + echo "Usage: --theme list|apply NAME" + echo "Current: $(opsec_get_value WIDGET_THEME)" + exit 1 + ;; + esac + ;; + --set) + [ -z "${2:-}" ] && { echo "Usage: --set KEY VALUE"; exit 1; } + opsec_set_value "$2" "${3:-}" + opsec_green "${2}=${3:-}" + ;; + --get) + [ -z "${2:-}" ] && { echo "Usage: --get KEY"; exit 1; } + opsec_get_value "$2" + ;; + --apply) + do_apply + ;; + --help|-h) + echo "OPSEC Configuration Manager" + echo "" + echo "Usage:" + echo " sudo opsec-config.sh Interactive TUI" + echo " sudo opsec-config.sh --boot on|off Toggle boot-into-advanced" + echo " sudo opsec-config.sh --profile CMD NAME Profile management" + echo " sudo opsec-config.sh --level apply NAME Apply deployment level" + echo " sudo opsec-config.sh --level list List available levels" + echo " sudo opsec-config.sh --banner compact|full|off Set terminal banner mode" + echo " sudo opsec-config.sh --theme list List widget themes" + echo " sudo opsec-config.sh --theme apply NAME Apply widget theme" + echo " sudo opsec-config.sh --set KEY VALUE Set config value" + echo " sudo opsec-config.sh --get KEY Get config value" + echo " sudo opsec-config.sh --apply Apply config changes" + ;; + "") + main_menu + ;; + *) + echo "Unknown option: $1" + echo "Try: sudo opsec-config.sh --help" + exit 1 + ;; +esac diff --git a/opsec/scripts/opsec-harden.sh b/opsec/scripts/opsec-harden.sh new file mode 100755 index 0000000..8b702df --- /dev/null +++ b/opsec/scripts/opsec-harden.sh @@ -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 diff --git a/opsec/scripts/opsec-hostname-randomize.sh b/opsec/scripts/opsec-hostname-randomize.sh new file mode 100755 index 0000000..e5b12f2 --- /dev/null +++ b/opsec/scripts/opsec-hostname-randomize.sh @@ -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}" diff --git a/opsec/scripts/opsec-killswitch.sh b/opsec/scripts/opsec-killswitch.sh new file mode 100755 index 0000000..f04abb6 --- /dev/null +++ b/opsec/scripts/opsec-killswitch.sh @@ -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 diff --git a/opsec/scripts/opsec-log-rotate.sh b/opsec/scripts/opsec-log-rotate.sh new file mode 100755 index 0000000..0c1a2d2 --- /dev/null +++ b/opsec/scripts/opsec-log-rotate.sh @@ -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" diff --git a/opsec/scripts/opsec-mode-toggle.sh b/opsec/scripts/opsec-mode-toggle.sh new file mode 100755 index 0000000..2e5acb1 --- /dev/null +++ b/opsec/scripts/opsec-mode-toggle.sh @@ -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 diff --git a/opsec/scripts/opsec-mode.sh b/opsec/scripts/opsec-mode.sh new file mode 100755 index 0000000..0a6b2bb --- /dev/null +++ b/opsec/scripts/opsec-mode.sh @@ -0,0 +1,1210 @@ +#!/bin/bash +# opsec-mode — Master toggle for Advanced OPSEC Mode +# Usage: sudo opsec-mode on|off|status|breakglass|breakglass-off +# +# ON: Activates all subsystems (torrc, DNS, kill switch, MAC, hostname, hardening) +# OFF: Reverses everything back to standard mode (blocked on paranoid levels) +# STATUS: Shows current state of all subsystems with config values +# BREAKGLASS: Emergency override — drops kill switch temporarily +# BREAKGLASS-OFF: End break-glass early + +## NOTE: Do NOT use "set -euo pipefail" — it causes silent crashes +## throughout this script when commands like grep, systemctl, etc. +## return non-zero. Use explicit error handling instead. + +if [ "$EUID" -ne 0 ]; then + echo "[!] Please run as root: sudo opsec-mode on|off|status" + exit 1 +fi + +# ─── EARLY ARG PARSE (needed before lock decision) ─────────────────────────── +_VERBOSE_EARLY=false +_CMD_EARLY="" +for _a in "$@"; do + case "$_a" in + -v|--verbose) _VERBOSE_EARLY=true ;; + *) [ -z "$_CMD_EARLY" ] && _CMD_EARLY="$_a" ;; + esac +done + +echo "[*] opsec-mode: ${_CMD_EARLY:-} (pid $$)" +mkdir -p /run/opsec 2>/dev/null || true + +LOCK_FILE="/run/opsec/mode.lock" +PID_FILE="/run/opsec/mode.pid" + +# ─── MUTEX: PID-FILE BASED (no inherited fd leaks) ────────────────────────── +# Previous approach used flock(fd 9) but child processes (opsec-harden.sh etc.) +# inherit the fd and hold the lock after the parent exits, causing "Another +# instance running" on the next invocation. Fix: use flock only for the brief +# atomic check, then immediately close the fd. The PID file guards ongoing +# exclusivity, and stale PIDs are detected automatically. +_acquire_lock() { + exec 9>"$LOCK_FILE" + if flock -n 9; then + # Got it clean + echo $$ > "$PID_FILE" + exec 9>&- # Close fd 9 immediately — children won't inherit it + return 0 + fi + exec 9>&- # Close our attempt either way + + # flock failed — check if the holder is still a real opsec-mode process + local stale_pid + stale_pid=$(cat "$PID_FILE" 2>/dev/null || echo "") + if [ -n "$stale_pid" ] && kill -0 "$stale_pid" 2>/dev/null; then + local stale_cmd + stale_cmd=$(cat "/proc/${stale_pid}/comm" 2>/dev/null || echo "") + if echo "$stale_cmd" | grep -q "opsec-mode"; then + echo "[!] Another opsec-mode instance is running (PID ${stale_pid})" + return 1 + fi + fi + + # Stale lock: previous opsec-mode exited but a child inherited the fd, + # or the process crashed. Force-reclaim. + echo "[*] Reclaiming stale lock (previous holder gone or inherited by child)" + rm -f "$LOCK_FILE" + exec 9>"$LOCK_FILE" + if flock -n 9; then + echo $$ > "$PID_FILE" + exec 9>&- + return 0 + fi + exec 9>&- + + echo "[!] Cannot acquire lock even after cleanup" + return 1 +} + +# ─── DISPATCH READ-ONLY COMMANDS BEFORE LOCK ───────────────────────────────── +# diag and status are read-only — they must not be blocked by a running mode_on +_OWNS_LOCK=false +case "${_CMD_EARLY:-}" in + diag|status) ;; # handled after full init below, but skip lock + *) _acquire_lock || exit 1; _OWNS_LOCK=true ;; +esac +# Only clean up PID file if WE own the lock (diag/status must not delete mode_on's PID) +trap '[ -n "${TAIL_PID:-}" ] && kill "$TAIL_PID" 2>/dev/null; [ "$_OWNS_LOCK" = "true" ] && rm -f "$PID_FILE"' EXIT + +STATE_FILE="/var/run/opsec-advanced.enabled" +BOOT_MARKER="/etc/opsec/boot-advanced.enabled" +TORRC_DEFAULT="/etc/tor/torrc-default" +RESOLV_BACKUP="/etc/resolv.conf.backup" +OPSEC_LIB="/usr/local/lib/opsec/opsec-lib.sh" +BREAKGLASS_STATE="/var/run/opsec-breakglass.active" +BREAKGLASS_TIMEOUT="${BREAKGLASS_TIMEOUT:-900}" # 15 minutes default +PREFLIGHT_LOG="/run/opsec/preflight.log" +DEBUG_LOG="/run/opsec/debug.log" +BOOTSTRAP_STOP="/var/run/opsec-bootstrap-stop" +NM_MAC_CONF="/etc/NetworkManager/conf.d/opsec-mac-random.conf" + +# Debug logger — appends timestamped entries (prints to terminal in verbose mode) +opsec_debug() { + local msg="[$(date -Is)] $*" + echo "$msg" >> "$DEBUG_LOG" + chmod 600 "$DEBUG_LOG" 2>/dev/null + if [ "$VERBOSE" = "true" ]; then + echo -e "\033[38;5;240m DBG: $*\033[0m" + fi +} + +# ─── SOURCE SHARED LIBRARY ──────────────────────────────────────────────────── +if [ -f "$OPSEC_LIB" ]; then + . "$OPSEC_LIB" + opsec_load_config 2>/dev/null || true + _HAS_LIB=true +else + # Fallback: hardcoded behavior if lib not yet deployed + _HAS_LIB=false + 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_mag() { echo -e "\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"; } +fi + +# ─── HELPERS ─────────────────────────────────────────────────────────────────── + +get_level_type() { + echo "${LEVEL_TYPE:-standard}" +} + +is_paranoid() { + [ "$(get_level_type)" = "paranoid" ] +} + +is_cloud_level() { + local level="${DEPLOYMENT_LEVEL:-bare-metal-standard}" + case "$level" in + cloud-*) return 0 ;; + *) return 1 ;; + esac +} + +# ─── BASE MODE (standard privacy hardening only) ───────────────────────────── +mode_base() { + local profile="${PROFILE_NAME:-default}" + + echo "" + opsec_hdr "APPLYING BASE PRIVACY HARDENING" + opsec_dim "Profile: ${profile}" + echo "" + + # 1. Disable IPv6 + opsec_info "Step 1/5: Disabling IPv6..." + local ipv6_all + ipv6_all=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0") + if [ "$ipv6_all" = "1" ]; then + opsec_green "IPv6 already disabled" + else + 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 + + # 2. Set privacy DNS (not Tor — use BASE_DNS) + opsec_info "Step 2/5: Setting privacy DNS..." + local base_dns="${BASE_DNS:-quad9}" + chattr -i /etc/resolv.conf 2>/dev/null || true + if [ ! -f "$RESOLV_BACKUP" ]; then + cp /etc/resolv.conf "$RESOLV_BACKUP" 2>/dev/null || true + chmod 600 "$RESOLV_BACKUP" 2>/dev/null || true + fi + case "$base_dns" in + quad9) + cat > /etc/resolv.conf << 'EOF' +nameserver 9.9.9.9 +nameserver 149.112.112.112 +EOF + ;; + cloudflare) + cat > /etc/resolv.conf << 'EOF' +nameserver 1.1.1.1 +nameserver 1.0.0.1 +EOF + ;; + *) + cat > /etc/resolv.conf << 'EOF' +nameserver 9.9.9.9 +nameserver 149.112.112.112 +EOF + ;; + esac + opsec_green "DNS set to ${base_dns}" + + # 3. Randomize MAC (bare-metal only) + reconnect network after + if ! is_cloud_level && [ "${BASE_MAC_RANDOMIZE:-1}" = "1" ]; then + opsec_info "Step 3/5: Randomizing MAC addresses..." + if [ -x /usr/local/bin/randomize-mac.sh ]; then + /usr/local/bin/randomize-mac.sh + # Reconnect WiFi to get new DHCP lease with new MAC + local wifi_if + wifi_if=$(nmcli -t -f DEVICE,TYPE device status 2>/dev/null | grep ':wifi$' | head -1 | cut -d: -f1) + if [ -n "$wifi_if" ]; then + opsec_dim "Reconnecting ${wifi_if} for new DHCP lease..." + nmcli device disconnect "$wifi_if" 2>/dev/null || true + sleep 1 + nmcli device connect "$wifi_if" 2>/dev/null || true + sleep 3 + fi + else + opsec_yellow "randomize-mac.sh not found — skipping" + fi + else + opsec_dim "Step 3/5: MAC randomization skipped (cloud or disabled)" + fi + + # 4. Disable swap + core dumps + opsec_info "Step 4/5: Hardening swap and core dumps..." + if [ "${HARDEN_SWAP:-1}" = "1" ]; then + swapoff -a 2>/dev/null || true + opsec_green "Swap disabled" + fi + if [ "${HARDEN_CORE_DUMPS:-1}" = "1" ]; then + sysctl -w kernel.core_pattern='|/bin/false' >/dev/null 2>&1 || true + opsec_green "Core dumps disabled" + fi + + # 5. Apply additional system hardening (base-only: skip ghost-mode features like USB block) + opsec_info "Step 5/5: Applying system hardening..." + if [ -x /usr/local/bin/opsec-harden.sh ]; then + OPSEC_BASE_ONLY=1 /usr/local/bin/opsec-harden.sh apply + else + opsec_yellow "opsec-harden.sh not found — skipping" + fi + + echo "" + opsec_hdr "BASE PRIVACY MODE: ACTIVE" + echo "" + opsec_dim "DNS: ${base_dns}" + opsec_dim "IPv6: disabled" + opsec_dim "Swap: disabled" + opsec_dim "Core dumps: disabled" + if ! is_cloud_level; then + opsec_dim "MAC: randomized" + fi + echo "" + opsec_dim "Ghost mode: sudo opsec-mode on" + echo "" + + # Run base preflight + if [ -x /usr/local/bin/opsec-preflight.sh ]; then + /usr/local/bin/opsec-preflight.sh --base --enforce || { + opsec_red "Base preflight FAILED — check /run/opsec/preflight.log" + } + fi +} + +# ─── ON ────────────────────────────────────────────────────────────────────── +mode_on() { + local rotation="${TOR_CIRCUIT_ROTATION:-30}" + local blacklist="${TOR_BLACKLIST-us,gb,ca,au,nz}" + local socks_port="${TOR_SOCKS_PORT:-9050}" + local dns_port="${TOR_DNS_PORT:-5353}" + local profile="${PROFILE_NAME:-default}" + + echo "" + opsec_hdr "ACTIVATING GHOST MODE" + opsec_dim "Profile: ${profile}" + echo "" + + opsec_debug "==========================================" + opsec_debug "=== MODE ON START ===" + opsec_debug "==========================================" + + # Clear any stale stop signal from previous runs + rm -f "$BOOTSTRAP_STOP" + + # 0. Disarm stale KS, then immediately arm filter-only lockdown + if iptables -L GP_FW >/dev/null 2>&1; then + opsec_debug "Step 0: Disarming stale kill switch" + /usr/local/bin/opsec-killswitch.sh off 2>/dev/null + fi + opsec_debug "Step 0.5: Arming filter-only lockdown (pre-bootstrap)" + /usr/local/bin/opsec-killswitch.sh filter-on + opsec_debug " Filter lockdown active — only Tor/VPN/DHCP allowed" + + # 1. Disable IPv6 + opsec_info "Step 1/10: Disabling IPv6..." + opsec_debug "Step 1: Disabling IPv6" + 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 + opsec_green "IPv6 disabled" + + # 2. Randomize MAC via NetworkManager (prevents NM from reverting macchanger) + opsec_info "Step 2/10: Randomizing MAC addresses..." + opsec_debug "Step 2: MAC randomization via NetworkManager" + if ! is_cloud_level; then + local wifi_if + wifi_if=$(nmcli -t -f DEVICE,TYPE device status 2>/dev/null | grep ':wifi$' | head -1 | cut -d: -f1) + # Configure NetworkManager to use random MACs natively + mkdir -p /etc/NetworkManager/conf.d + cat > "$NM_MAC_CONF" << 'NMEOF' +# OPSEC: random MAC on every connection (managed by opsec-mode) +[device] +wifi.scan-rand-mac-address=yes + +[connection] +wifi.cloned-mac-address=random +ethernet.cloned-mac-address=random +NMEOF + opsec_debug " NM MAC config written to ${NM_MAC_CONF}" + # Reload NM config and reconnect to apply random MAC + nmcli general reload 2>/dev/null || true + if [ -n "$wifi_if" ]; then + opsec_debug " Reconnecting WiFi: ${wifi_if}" + local old_mac + old_mac=$(ip link show "$wifi_if" 2>/dev/null | awk '/ether/ {print $2}') + opsec_debug " MAC before: ${old_mac}" + nmcli device disconnect "$wifi_if" 2>/dev/null || true + sleep 2 + nmcli device connect "$wifi_if" 2>/dev/null || true + # Wait for connection to establish + for w in $(seq 1 15); do + if nmcli -t -f DEVICE,STATE device status 2>/dev/null | grep "^${wifi_if}:connected" >/dev/null; then + opsec_debug " WiFi reconnected at second ${w}" + break + fi + sleep 1 + done + # Wait for actual IP routing (NM "connected" != routed) + opsec_debug " Waiting for network route to establish..." + for r in $(seq 1 10); do + if ip route show default 2>/dev/null | grep -q .; then + opsec_debug " Default route available at second ${r}" + break + fi + sleep 1 + done + sleep 2 # Brief settle for ARP/DHCP + fi + local new_mac + new_mac=$(ip link show "$wifi_if" 2>/dev/null | awk '/ether/ {print $2}') + opsec_debug " MAC after: ${new_mac}" + if [ "$old_mac" != "$new_mac" ] 2>/dev/null; then + opsec_green "MAC randomized: ${new_mac}" + else + opsec_yellow "MAC unchanged — NM may need restart" + opsec_debug " WARNING: MAC did not change (${old_mac} -> ${new_mac})" + fi + else + opsec_dim "Skipped (cloud deployment)" + fi + + # 3. Randomize hostname + opsec_info "Step 3/10: Randomizing hostname..." + opsec_debug "Step 3: Hostname randomization" + if ! is_cloud_level; then + if [ ! -f /run/opsec/hostname.original ]; then + hostname > /run/opsec/hostname.original + chmod 600 /run/opsec/hostname.original + opsec_dim "Saved original hostname: $(cat /run/opsec/hostname.original)" + opsec_debug " Saved original to tmpfs: $(hostname)" + fi + /usr/local/bin/opsec-hostname-randomize.sh 2>&1 | tee -a "$DEBUG_LOG" || true + opsec_debug " Hostname after randomize: $(hostname)" + else + opsec_dim "Skipped (cloud deployment)" + fi + + # 4. Set state marker EARLY — needed for chattr +i in DNS step + touch "$STATE_FILE" + opsec_debug "Step 4: State marker set early (for DNS lock)" + + # 5. Deploy hardened torrc + clear stale Tor state + restart Tor + opsec_info "Step 5/10: Deploying hardened Tor configuration..." + opsec_debug "Step 5: Deploying torrc (HAS_LIB=${_HAS_LIB})" + + # Clear Tor caches only if TOR_CLEAR_CACHE=1 (preserving guard state) + opsec_debug " Cache clear: TOR_CLEAR_CACHE=${TOR_CLEAR_CACHE:-0}" + systemctl stop tor 2>/dev/null || true + if [ "${TOR_CLEAR_CACHE:-0}" = "1" ]; then + opsec_debug " Clearing Tor descriptor caches" + rm -f /var/lib/tor/cached-descriptors /var/lib/tor/cached-descriptors.new \ + /var/lib/tor/cached-microdesc* /var/lib/tor/cached-consensus \ + /var/lib/tor/cached-certs 2>/dev/null || true + else + opsec_debug " Keeping Tor descriptor caches (faster bootstrap)" + fi + + if [ "$_HAS_LIB" = "true" ]; then + opsec_generate_torrc + opsec_green "Torrc generated from config (blacklist: ${blacklist})" + opsec_debug "Torrc generated via lib (blacklist: ${blacklist})" + else + local torrc_opsec="/etc/tor/torrc-opsec" + if [ -f "$torrc_opsec" ]; then + cp "$torrc_opsec" /etc/tor/torrc + fi + fi + + opsec_debug "Current torrc contents:" + cat /etc/tor/torrc >> "$DEBUG_LOG" 2>&1 + opsec_debug "---" + + # Truncate Tor log so bootstrap check doesn't read stale entries + mkdir -p /run/tor && chown debian-tor:debian-tor /run/tor 2>/dev/null || true + : > /run/tor/notices.log + chown debian-tor:debian-tor /run/tor/notices.log 2>/dev/null || true + + opsec_debug "Restarting Tor service..." + systemctl restart tor 2>&1 | tee -a "$DEBUG_LOG" + opsec_debug "systemctl restart tor exit code: $?" + opsec_debug "Tor service status: $(systemctl is-active tor 2>&1)" + + # 6. Configure DNS (lock it before bootstrap wait) + opsec_info "Step 6/10: Configuring DNS..." + opsec_debug "Step 6: Configuring DNS (DNS_MODE=${DNS_MODE:-tor})" + if [ ! -f "$RESOLV_BACKUP" ]; then + chattr -i /etc/resolv.conf 2>/dev/null || true + cp /etc/resolv.conf "$RESOLV_BACKUP" + chmod 600 "$RESOLV_BACKUP" 2>/dev/null || true + opsec_debug "Backed up resolv.conf (mode 600)" + fi + if [ "$_HAS_LIB" = "true" ]; then + opsec_generate_resolv + opsec_green "DNS set to mode: ${DNS_MODE:-tor}" + else + chattr -i /etc/resolv.conf 2>/dev/null || true + echo "nameserver 127.0.0.1" > /etc/resolv.conf + chattr +i /etc/resolv.conf + opsec_green "DNS locked to 127.0.0.1 (Tor DNSPort ${dns_port})" + fi + # Force lock DNS regardless — state file is already set + chattr +i /etc/resolv.conf 2>/dev/null || true + # Verify immutable flag actually took hold + if ! lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then + opsec_red "WARNING: chattr +i failed on resolv.conf — DNS lock may not hold" + opsec_debug " CRITICAL: chattr +i verification FAILED — filesystem may not support extended attributes" + else + opsec_debug " chattr +i verified on resolv.conf" + fi + opsec_debug "resolv.conf now: $(cat /etc/resolv.conf 2>/dev/null | tr '\n' ' ')" + # Lock NetworkManager out of DNS management + mkdir -p /etc/NetworkManager/conf.d + cat > /etc/NetworkManager/conf.d/opsec-dns-lock.conf << 'DNSEOF' +[main] +dns=none +DNSEOF + nmcli general reload 2>/dev/null || true + opsec_debug " NM dns=none lock installed" + + # 7. Wait for Tor bootstrap (up to 180s — first guard may timeout) + opsec_yellow "Waiting for Tor to fully bootstrap (100%)..." + opsec_debug "Step 7: Tor bootstrap wait (180s max)" + TAIL_PID="" + if [ "$VERBOSE" = "true" ]; then + tail -f /run/tor/notices.log 2>/dev/null & + TAIL_PID=$! + fi + local tor_ready=false + for i in $(seq 1 180); do + # Check if mode_off signaled us to stop + if [ -f "$BOOTSTRAP_STOP" ]; then + opsec_yellow "Bootstrap wait aborted (mode_off requested)" + opsec_debug "Bootstrap loop aborted by stop signal at ${i}s" + rm -f "$BOOTSTRAP_STOP" + break + fi + local boot_pct + boot_pct=$(grep "Bootstrapped" /run/tor/notices.log 2>/dev/null | tail -1 | grep -oP '\d+(?=%)' || true) + if [ "$boot_pct" = "100" ] && ss -tln | grep -q ":${socks_port} "; then + opsec_green "Tor fully bootstrapped (100%) at ${i}s" + opsec_debug "Tor bootstrapped 100% at second ${i}" + tor_ready=true + break + fi + if [ $((i % 5)) -eq 0 ] || [ "$i" -eq 1 ]; then + opsec_dim " Bootstrap: ${boot_pct:-0}% (${i}s)" + opsec_debug "Tor wait ${i}s: bootstrap=${boot_pct:-0}%" + fi + sleep 1 + if [ "$i" -eq 180 ]; then + opsec_red "Tor bootstrap timeout after 180s (at ${boot_pct:-0}%)" + opsec_debug "TIMEOUT: Tor at ${boot_pct:-0}% after 180s" + opsec_debug "Last 10 Tor log lines:" + tail -10 /run/tor/notices.log >> "$DEBUG_LOG" 2>&1 + fi + done + [ -n "${TAIL_PID:-}" ] && kill "$TAIL_PID" 2>/dev/null + + # 8. Add NAT transparent proxy AFTER Tor is ready + opsec_info "Step 8/10: Arming NAT transparent proxy..." + opsec_debug "Step 8: NAT proxy (tor_ready=${tor_ready})" + local ks_exit + if [ "$tor_ready" = "true" ]; then + /usr/local/bin/opsec-killswitch.sh nat-on + ks_exit=$? + opsec_debug " NAT proxy armed with Tor at 100% (exit=${ks_exit})" + else + # If mode_off aborted us, don't arm NAT — teardown is already running + if [ ! -f "$STATE_FILE" ]; then + opsec_yellow "Bootstrap aborted by mode_off — skipping NAT arm" + opsec_debug " Skipping NAT — state file gone (mode_off in progress)" + return 0 + fi + opsec_yellow "WARNING: Tor not fully bootstrapped — arming NAT anyway" + /usr/local/bin/opsec-killswitch.sh nat-on + ks_exit=$? + opsec_debug " NAT proxy armed with Tor NOT ready (exit=${ks_exit})" + fi + opsec_debug " Kill switch exit code: ${ks_exit}" + # Flush conntrack so stale entries don't bypass NAT redirect + conntrack -F 2>/dev/null && opsec_debug " conntrack table flushed" || opsec_debug " conntrack flush skipped (not available)" + opsec_debug " Filter rule count: $(iptables -L GP_FW --line-numbers 2>/dev/null | tail -n +3 | wc -l)" + local _nat_count + _nat_count=$(iptables -t nat -L GP_NAT --line-numbers 2>/dev/null | tail -n +3 | wc -l) + opsec_debug " NAT chain (GP_NAT) rule count: ${_nat_count}" + opsec_debug " NAT OUTPUT jump rules: $(iptables -t nat -L OUTPUT --line-numbers 2>/dev/null | tail -n +3 | wc -l)" + # TCP listeners (TransPort 9040, SOCKSPort 9050) + local _tcp_ports + _tcp_ports=$(ss -tlnp 2>/dev/null | grep -E ":(9040|9050|5353)" || echo " NONE") + opsec_debug " Tor TCP ports listening:" + echo "$_tcp_ports" >> "$DEBUG_LOG" 2>&1 + if [ "$VERBOSE" = "true" ]; then echo -e "\033[38;5;240m DBG: TCP: ${_tcp_ports}\033[0m"; fi + # UDP listener (DNSPort 5353) + local _udp_ports + _udp_ports=$(ss -ulnp 2>/dev/null | grep -E ":5353" || echo " NONE") + opsec_debug " Tor UDP ports listening:" + echo "$_udp_ports" >> "$DEBUG_LOG" 2>&1 + if [ "$VERBOSE" = "true" ]; then echo -e "\033[38;5;240m DBG: UDP: ${_udp_ports}\033[0m"; fi + + # 9. Apply system hardening + boot services + opsec_info "Step 9/10: Applying system hardening..." + opsec_debug "Step 9: Hardening + boot services" + if [ -x /usr/local/bin/opsec-harden.sh ]; then + /usr/local/bin/opsec-harden.sh apply 2>&1 | tee -a "$DEBUG_LOG" || true + else + opsec_yellow "opsec-harden.sh not found — skipping" + fi + systemctl enable opsec-mac-randomize.service 2>/dev/null && opsec_green "MAC randomize on boot: enabled" || opsec_yellow "MAC service not found" + systemctl enable opsec-hostname-randomize.service 2>/dev/null && opsec_green "Hostname randomize on boot: enabled" || opsec_yellow "Hostname service not found" + systemctl enable opsec-killswitch.service 2>/dev/null && opsec_green "Kill switch on boot: enabled" || opsec_yellow "Kill switch service not found" + if [ -f "$BOOT_MARKER" ]; then + systemctl enable opsec-boot-advanced.service 2>/dev/null || true + opsec_green "Boot-into-advanced: enabled" + fi + + # 10. Start sentry daemon if available + opsec_info "Step 10/10: Starting sentry..." + opsec_debug "Step 10: Sentry + traffic blend" + if [ -x /usr/local/bin/opsec-sentry.sh ]; then + /usr/local/bin/opsec-sentry.sh start 2>/dev/null && opsec_green "Sentry daemon started" || opsec_yellow "Sentry start failed" + else + opsec_dim "Sentry not installed — skipping" + fi + if [ -x /usr/local/bin/opsec-traffic-blend.sh ]; then + /usr/local/bin/opsec-traffic-blend.sh start 2>/dev/null && opsec_green "Traffic blending started" || opsec_dim "Traffic blend not started" + fi + + # ─── EXIT GATE: Verify all subsystems before declaring active ──────────── + _final_ok=true + if ! systemctl is-active tor >/dev/null 2>&1; then + opsec_red "CRITICAL: Tor is NOT running"; _final_ok=false + fi + if ! iptables -C OUTPUT -j GP_FW 2>/dev/null; then + opsec_red "CRITICAL: Kill switch is NOT armed"; _final_ok=false + fi + # Check NAT rules exist (either in chain or direct OUTPUT fallback) + local _gate_nat_count=0 + if iptables -t nat -L GP_NAT >/dev/null 2>&1; then + _gate_nat_count=$(iptables -t nat -L GP_NAT --line-numbers 2>/dev/null | tail -n +3 | wc -l) + fi + if [ "$_gate_nat_count" -lt 3 ]; then + # Check if fallback direct OUTPUT rules are present + local _gate_output_nat + _gate_output_nat=$(iptables -t nat -L OUTPUT --line-numbers 2>/dev/null | tail -n +3 | wc -l) + if [ "$_gate_output_nat" -lt 3 ]; then + opsec_red "CRITICAL: NAT transparent proxy NOT armed (chain=${_gate_nat_count}, output=${_gate_output_nat} rules)" + _final_ok=false + else + opsec_debug " NAT using direct OUTPUT fallback (${_gate_output_nat} rules)" + fi + fi + if ! ss -tln | grep -q ":${socks_port} "; then + opsec_red "CRITICAL: SOCKS port ${socks_port} NOT listening"; _final_ok=false + fi + # Check TransPort (9040) and DNSPort (5353) are listening + if ! ss -tln | grep -q ":9040 "; then + opsec_red "CRITICAL: TransPort 9040 NOT listening"; _final_ok=false + fi + if ! ss -uln | grep -q ":5353 "; then + opsec_red "CRITICAL: DNSPort 5353 NOT listening (UDP)"; _final_ok=false + fi + # Live connectivity test — actually verify traffic flows through Tor + if [ "$_final_ok" = "true" ]; then + opsec_info "Testing transparent proxy connectivity..." + local _tp_result + _tp_result=$(curl -4 -s --max-time 15 https://check.torproject.org/api/ip 2>&1) + opsec_debug " TransProxy test result: ${_tp_result}" + if echo "$_tp_result" | grep -q '"IsTor":true'; then + opsec_green "Transparent proxy VERIFIED — traffic routed through Tor" + elif echo "$_tp_result" | grep -q '"IsTor":false'; then + opsec_red "CRITICAL: Transparent proxy LEAKING — traffic NOT through Tor" + _final_ok=false + else + opsec_yellow "WARNING: Transparent proxy test inconclusive (timeout or network error)" + opsec_debug " curl output: ${_tp_result}" + # Don't fail — services are running, might just be slow + fi + fi + if [ "$_final_ok" = "false" ]; then + opsec_red "==========================================" + opsec_red " GHOST MODE ACTIVATION FAILED" + opsec_red " YOU ARE NOT PROTECTED" + opsec_red "==========================================" + opsec_red " Tearing down partial activation..." + # Clean teardown — don't leave zombie armed state + /usr/local/bin/opsec-killswitch.sh off 2>/dev/null + rm -f /etc/NetworkManager/conf.d/opsec-dns-lock.conf + nmcli general reload 2>/dev/null || true + chattr -i /etc/resolv.conf 2>/dev/null || true + rm -f "$STATE_FILE" + opsec_red " Recovery: sudo opsec-mode off (full cleanup)" + opsec_red "==========================================" + exit 1 + fi + + echo "" + opsec_hdr "GHOST MODE: ACTIVE" + echo "" + opsec_dim "Profile: ${profile}" + opsec_dim "Kill switch: armed (Tor/VPN only)" + opsec_dim "DNS: ${DNS_MODE:-tor}" + opsec_dim "Exit blacklist: ${blacklist}" + opsec_dim "Circuit rotation: ${rotation}s" + if ! is_cloud_level; then + opsec_dim "MAC addresses: randomized" + opsec_dim "Hostname: randomized" + fi + echo "" + opsec_dim "Test: curl --socks5-hostname 127.0.0.1:${socks_port} https://ifconfig.me" + opsec_dim "Off: sudo opsec-mode off" + echo "" + + # Run full preflight verification + if [ -x /usr/local/bin/opsec-preflight.sh ]; then + opsec_info "Running post-activation preflight..." + /usr/local/bin/opsec-preflight.sh --full --enforce || { + opsec_red "Preflight FAILED — kill switch stays armed (safe default)" + opsec_red "Check failures: opsec-preflight" + echo "[$(date -Is)] PREFLIGHT FAIL after mode_on" >> "$PREFLIGHT_LOG" + } + fi + + # ─── FINAL STATE SUMMARY ───────────────────────────────────────────────── + opsec_debug "==========================================" + opsec_debug "=== MODE ON FINAL STATE ===" + opsec_debug "==========================================" + opsec_debug "Hostname: $(hostname)" + opsec_debug "DNS: $(cat /etc/resolv.conf 2>/dev/null | tr '\n' ' ')" + opsec_debug "IPv6 disabled: $(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null)" + opsec_debug "Tor status: $(systemctl is-active tor 2>&1)" + opsec_debug "Tor bootstrap: $(grep 'Bootstrapped' /run/tor/notices.log 2>/dev/null | tail -1 || echo NONE)" + opsec_debug "Tor TCP ports:" + ss -tlnp 2>/dev/null | grep -E ":(9040|9050|5353)" >> "$DEBUG_LOG" 2>&1 + opsec_debug "Tor UDP ports:" + ss -ulnp 2>/dev/null | grep -E ":5353" >> "$DEBUG_LOG" 2>&1 + opsec_debug "Kill switch chain rule count: $(iptables -L GP_FW --line-numbers 2>/dev/null | tail -n +3 | wc -l)" + opsec_debug "NAT chain (GP_NAT) rule count: $(iptables -t nat -L GP_NAT --line-numbers 2>/dev/null | tail -n +3 | wc -l)" + opsec_debug "State file: $([ -f "$STATE_FILE" ] && echo EXISTS || echo MISSING)" + opsec_debug "Network interfaces:" + ip -br addr 2>/dev/null >> "$DEBUG_LOG" 2>&1 + opsec_debug "Default route: $(ip route show default 2>/dev/null | head -1)" + opsec_debug "==========================================" + opsec_debug "=== MODE ON COMPLETE ===" + opsec_debug "==========================================" + +} + +# ─── OFF ───────────────────────────────────────────────────────────────────── +mode_off() { + # Block mode_off on paranoid levels + if is_paranoid; then + echo "" + opsec_red "BLOCKED: Cannot disable ghost mode on paranoid level." + opsec_red "Current level: ${DEPLOYMENT_LEVEL:-unknown} (LEVEL_TYPE=paranoid)" + opsec_yellow "To lower security, change deployment level first:" + opsec_dim " sudo opsec-config.sh --level apply bare-metal-standard" + opsec_dim " sudo opsec-config.sh --level apply cloud-normal" + echo "" + exit 1 + fi + + echo "" + opsec_hdr "DEACTIVATING GHOST MODE" + opsec_debug "=== MODE OFF START ===" + echo "" + + # Interrupt safety: if mode_off is killed mid-teardown (after kill switch is down), + # re-arm the kill switch to avoid leaving the system exposed + _mode_off_rearm() { + echo "" + opsec_red "INTERRUPTED — re-arming kill switch to prevent exposure" + /usr/local/bin/opsec-killswitch.sh on 2>/dev/null + opsec_debug "mode_off interrupted — kill switch re-armed" + exit 1 + } + trap '_mode_off_rearm' INT TERM + + # Signal any running mode_on bootstrap loop to stop immediately + touch "$BOOTSTRAP_STOP" + opsec_debug " Stop signal sent to bootstrap loop" + + # Stop sentry if running + opsec_debug "Stopping sentry..." + if [ -x /usr/local/bin/opsec-sentry.sh ]; then + opsec_info "Stopping sentry..." + /usr/local/bin/opsec-sentry.sh stop 2>/dev/null || true + fi + + # Stop traffic blending if running + if [ -x /usr/local/bin/opsec-traffic-blend.sh ]; then + /usr/local/bin/opsec-traffic-blend.sh stop 2>/dev/null || true + fi + + # Clear break-glass if active + rm -f "$BREAKGLASS_STATE" + + # 1. Disarm kill switch + opsec_info "Disarming kill switch..." + opsec_debug "Step 1: Disarming kill switch" + /usr/local/bin/opsec-killswitch.sh off + local ks_off_exit=$? + opsec_debug " Kill switch off exit code: ${ks_off_exit}" + opsec_debug " iptables OUTPUT after disarm:" + iptables -L OUTPUT -n --line-numbers >> "$DEBUG_LOG" 2>&1 + opsec_debug " NAT OUTPUT after disarm:" + iptables -t nat -L OUTPUT -n --line-numbers >> "$DEBUG_LOG" 2>&1 + + # 2. Restore DNS FIRST (before NM lock removal to prevent race) + opsec_info "Restoring DNS to base privacy resolver..." + opsec_debug "Step 2: Restoring DNS" + opsec_debug " resolv.conf before: $(cat /etc/resolv.conf 2>/dev/null | tr '\n' ' ')" + chattr -i /etc/resolv.conf 2>/dev/null || true + local base_dns="${BASE_DNS:-quad9}" + case "$base_dns" in + quad9) + cat > /etc/resolv.conf << 'EOF' +nameserver 9.9.9.9 +nameserver 149.112.112.112 +EOF + ;; + cloudflare) + cat > /etc/resolv.conf << 'EOF' +nameserver 1.1.1.1 +nameserver 1.0.0.1 +EOF + ;; + *) + if [ -f "$RESOLV_BACKUP" ]; then + cp "$RESOLV_BACKUP" /etc/resolv.conf + else + cat > /etc/resolv.conf << 'EOF' +nameserver 9.9.9.9 +nameserver 149.112.112.112 +EOF + fi + ;; + esac + # Secure-delete ISP DNS backup (contains pre-opsec resolver config) + if [ -f "$RESOLV_BACKUP" ]; then + shred -fuz -n 1 "$RESOLV_BACKUP" 2>/dev/null || rm -f "$RESOLV_BACKUP" + opsec_debug " resolv.conf.backup securely deleted" + fi + opsec_green "DNS restored to ${base_dns}" + opsec_debug " resolv.conf after: $(cat /etc/resolv.conf 2>/dev/null | tr '\n' ' ')" + + # NOW unlock NM DNS management (after DNS is written and settled) + rm -f /etc/NetworkManager/conf.d/opsec-dns-lock.conf + nmcli general reload 2>/dev/null || true + opsec_debug " NM dns=none lock removed (after DNS restore)" + + # 3. Restore stock torrc and stop Tor + opsec_info "Restoring default Tor configuration..." + opsec_debug "Step 3: Stopping Tor" + opsec_debug " Tor status before: $(systemctl is-active tor 2>&1)" + if [ -f "$TORRC_DEFAULT" ]; then + cp "$TORRC_DEFAULT" /etc/tor/torrc + opsec_debug " Restored torrc from ${TORRC_DEFAULT}" + else + opsec_debug " WARNING: ${TORRC_DEFAULT} not found" + fi + systemctl stop tor 2>/dev/null || true + opsec_debug " Tor status after: $(systemctl is-active tor 2>&1)" + opsec_green "Tor stopped, default torrc restored" + + # 4. Revert system hardening + opsec_info "Reverting system hardening..." + opsec_debug "Step 4: Reverting hardening" + if [ -x /usr/local/bin/opsec-harden.sh ]; then + /usr/local/bin/opsec-harden.sh revert 2>&1 | tee -a "$DEBUG_LOG" || true + else + opsec_debug " opsec-harden.sh not found" + fi + + # 5. Restore hostname + opsec_info "Restoring hostname..." + opsec_debug "Step 5: Restoring hostname" + # Check both tmpfs (new) and disk (legacy) locations + local _hostname_file="" + if [ -f /run/opsec/hostname.original ]; then + _hostname_file="/run/opsec/hostname.original" + elif [ -f /etc/opsec/hostname.original ]; then + _hostname_file="/etc/opsec/hostname.original" + fi + opsec_debug "hostname.original location: ${_hostname_file:-NONE}" + opsec_debug "hostname.original content: $(cat "$_hostname_file" 2>/dev/null || echo MISSING)" + opsec_debug "Current hostname: $(hostname)" + if [ -n "$_hostname_file" ]; then + local orig_hostname + orig_hostname=$(cat "$_hostname_file") + local cur_hostname + cur_hostname=$(hostname) + hostnamectl set-hostname "$orig_hostname" 2>/dev/null || { + echo "$orig_hostname" > /etc/hostname + hostname "$orig_hostname" + } + # Update /etc/hosts + if [ "$cur_hostname" != "$orig_hostname" ]; then + sed -i "s/$cur_hostname/$orig_hostname/g" /etc/hosts 2>/dev/null || true + fi + # Secure-delete from both locations + shred -fuz -n 1 "$_hostname_file" 2>/dev/null || rm -f "$_hostname_file" + rm -f /etc/opsec/hostname.original /run/opsec/hostname.original 2>/dev/null + opsec_green "Hostname restored to: ${orig_hostname}" + opsec_debug "Hostname restored: ${cur_hostname} -> ${orig_hostname}" + else + opsec_dim "No saved hostname — keeping current: $(hostname)" + opsec_debug "WARNING: No hostname.original file found, hostname stays: $(hostname)" + fi + + # 6. Remove NM MAC randomization config + if [ -f "$NM_MAC_CONF" ]; then + rm -f "$NM_MAC_CONF" + nmcli general reload 2>/dev/null || true + opsec_debug "Step 6: NM MAC config removed, NM reloaded" + opsec_green "MAC randomization config removed" + fi + + # 7. Disable boot services + opsec_info "Disabling boot persistence..." + opsec_debug "Step 7: Disabling boot services" + systemctl disable opsec-mac-randomize.service 2>/dev/null || true + systemctl disable opsec-hostname-randomize.service 2>/dev/null || true + systemctl disable opsec-killswitch.service 2>/dev/null || true + systemctl disable opsec-boot-advanced.service 2>/dev/null || true + opsec_green "Boot services disabled" + + # 8. Remove state marker (keep boot marker if user wants it) + rm -f "$STATE_FILE" + opsec_green "State marker removed" + opsec_debug "Step 8: State marker removed" + + # Teardown complete — clear the interrupt trap (no longer dangerous) + trap - INT TERM + + echo "" + opsec_hdr "GHOST MODE: OFF" + echo "" + opsec_dim "Base privacy mode restored" + opsec_dim "Kill switch disarmed" + opsec_dim "DNS using ${base_dns}" + opsec_dim "Tor stopped" + opsec_dim "Hostname restored" + echo "" + + opsec_debug "--- MODE OFF FINAL STATE ---" + opsec_debug " Hostname: $(hostname)" + opsec_debug " DNS: $(cat /etc/resolv.conf 2>/dev/null | tr '\n' ' ')" + opsec_debug " Tor: $(systemctl is-active tor 2>&1)" + opsec_debug " iptables OUTPUT rules: $(iptables -L OUTPUT --line-numbers 2>/dev/null | tail -n +3 | wc -l)" + opsec_debug " State file exists: $([ -f "$STATE_FILE" ] && echo YES || echo NO)" + opsec_debug "=== MODE OFF COMPLETE ===" + + # Re-apply base hardening (skip MAC re-randomization — already handled) + opsec_debug "Re-applying base hardening..." + BASE_MAC_RANDOMIZE=0 mode_base +} + +# ─── BREAK-GLASS ────────────────────────────────────────────────────────────── +mode_breakglass() { + if [ ! -f "$STATE_FILE" ]; then + opsec_red "Break-glass requires ghost mode to be active" + exit 1 + fi + + local expiry + expiry=$(( $(date +%s) + BREAKGLASS_TIMEOUT )) + + echo "" + opsec_hdr "BREAK-GLASS EMERGENCY OVERRIDE" + echo "" + opsec_red "WARNING: Temporarily disabling kill switch" + opsec_red "Direct internet access will be available" + opsec_dim "Auto-expires in $((BREAKGLASS_TIMEOUT / 60)) minutes" + echo "" + + # Drop kill switch only + /usr/local/bin/opsec-killswitch.sh off + + # Write state file with expiry timestamp + echo "$expiry" > "$BREAKGLASS_STATE" + + # Log the event + echo "[$(date -Is)] BREAKGLASS ACTIVATED — expires $(date -d @${expiry} -Is) — timeout ${BREAKGLASS_TIMEOUT}s" >> "$PREFLIGHT_LOG" + + opsec_yellow "Kill switch DISARMED — all other OPSEC settings preserved" + opsec_dim "DNS, MAC, hostname remain randomized" + echo "" + opsec_dim "End early: sudo opsec-mode breakglass-off" + opsec_dim "Auto-rearm: $(date -d @${expiry} '+%H:%M:%S')" + echo "" + + # Schedule auto-rearm via at or background process + ( + exec 9>&- # Release lock fd — don't hold it during sleep + sleep "$BREAKGLASS_TIMEOUT" + if [ -f "$BREAKGLASS_STATE" ]; then + /usr/local/bin/opsec-killswitch.sh on + rm -f "$BREAKGLASS_STATE" + echo "[$(date -Is)] BREAKGLASS EXPIRED — kill switch re-armed" >> "$PREFLIGHT_LOG" + # Re-run preflight + if [ -x /usr/local/bin/opsec-preflight.sh ]; then + /usr/local/bin/opsec-preflight.sh --full --enforce >> "$PREFLIGHT_LOG" 2>&1 || true + fi + fi + ) & + disown +} + +mode_breakglass_off() { + if [ ! -f "$BREAKGLASS_STATE" ]; then + opsec_yellow "Break-glass is not active" + exit 0 + fi + + echo "" + opsec_hdr "ENDING BREAK-GLASS" + echo "" + + # Re-arm kill switch + /usr/local/bin/opsec-killswitch.sh on + rm -f "$BREAKGLASS_STATE" + + echo "[$(date -Is)] BREAKGLASS ENDED MANUALLY — kill switch re-armed" >> "$PREFLIGHT_LOG" + + opsec_green "Kill switch re-armed" + opsec_green "Break-glass ended" + echo "" + + # Re-run preflight + if [ -x /usr/local/bin/opsec-preflight.sh ]; then + opsec_info "Running preflight verification..." + /usr/local/bin/opsec-preflight.sh --full --enforce || { + opsec_red "Preflight FAILED — check: opsec-preflight" + } + fi +} + +# ─── STATUS ────────────────────────────────────────────────────────────────── +mode_status() { + local profile="${PROFILE_NAME:-default}" + local level="${DEPLOYMENT_LEVEL:-bare-metal-standard}" + local level_type="$(get_level_type)" + + echo "" + opsec_hdr "OPSEC STATUS" + echo "" + + # Mode state + if [ -f "$STATE_FILE" ]; then + opsec_green "Mode: GHOST (active)" + else + opsec_yellow "Mode: BASE PRIVACY" + fi + + # Level info + opsec_info "Level: ${level} (${level_type})" + opsec_info "Profile: ${profile}" + + # Break-glass warning + if [ -f "$BREAKGLASS_STATE" ]; then + local bg_expiry bg_remaining + bg_expiry=$(cat "$BREAKGLASS_STATE" 2>/dev/null) + bg_remaining=$(( bg_expiry - $(date +%s) )) + if [ "$bg_remaining" -gt 0 ]; then + opsec_red "BREAK-GLASS ACTIVE — kill switch DOWN — ${bg_remaining}s remaining" + fi + fi + echo "" + + # Boot-into-advanced + if [ -f "$BOOT_MARKER" ]; then + opsec_green "Boot Mode: PERSISTENT (will activate on reboot)" + else + opsec_dim "Boot Mode: standard (manual activation only)" + fi + echo "" + + # ─── Tor ─── + opsec_cyan "Tor Configuration" + if systemctl is-active tor >/dev/null 2>&1; then + opsec_green " Status: Running" + if grep -q "ExcludeExitNodes" /etc/tor/torrc 2>/dev/null; then + local blacklist_display + blacklist_display=$(grep "ExcludeExitNodes" /etc/tor/torrc | sed 's/ExcludeExitNodes //' | tr -d '{}') + opsec_green " Torrc: Hardened" + opsec_dim " Blacklist: ${blacklist_display}" + else + opsec_yellow " Torrc: Default" + fi + local rotation_val + rotation_val=$(grep "MaxCircuitDirtiness" /etc/tor/torrc 2>/dev/null | awk '{print $2}') + [ -n "$rotation_val" ] && opsec_dim " Circuit rotation: ${rotation_val}s" + + local isolation_val + isolation_val=$(grep "IsolateDestAddr" /etc/tor/torrc 2>/dev/null | head -1) + [ -n "$isolation_val" ] && opsec_green " Stream isolation: ON" || opsec_dim " Stream isolation: off" + + local padding_val + padding_val=$(grep "ConnectionPadding" /etc/tor/torrc 2>/dev/null | awk '{print $2}') + [ "$padding_val" = "1" ] && opsec_green " Traffic padding: ON" || opsec_dim " Traffic padding: off" + else + opsec_yellow " Status: Stopped" + fi + echo "" + + # ─── IPv6 ─── + local ipv6_all + ipv6_all=$(sysctl -n net.ipv6.conf.all.disable_ipv6 2>/dev/null || echo "0") + [ "$ipv6_all" = "1" ] && opsec_green "IPv6: Disabled" || opsec_red "IPv6: ENABLED (risk)" + + # ─── DNS ─── + local dns_server + dns_server=$(awk '/^nameserver/ {print $2; exit}' /etc/resolv.conf 2>/dev/null) + if [ "$dns_server" = "127.0.0.1" ]; then + opsec_green "DNS: Tor DNS (127.0.0.1)" + if lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i'; then + opsec_green "DNS Lock: Immutable (chattr +i)" + else + opsec_yellow "DNS Lock: Not locked" + fi + elif echo "$dns_server" | grep -qE '^(9\.9\.9\.9|149\.112|1\.1\.1\.1|1\.0\.0\.1)$'; then + opsec_green "DNS: Privacy resolver (${dns_server})" + else + opsec_yellow "DNS: ${dns_server}" + fi + + # ─── Kill switch ─── + if [ -f "$BREAKGLASS_STATE" ]; then + opsec_red "Kill Switch: DISARMED (BREAK-GLASS)" + elif iptables -L GP_FW >/dev/null 2>&1; then + opsec_green "Kill Switch: ARMED" + else + opsec_yellow "Kill Switch: Off" + fi + + # ─── VPN ─── + local vpn_if + vpn_if=$(ip -o link show 2>/dev/null | awk -F': ' '{print $2}' | grep -E '^(tun|wg)' | head -1) + if [ -n "$vpn_if" ]; then + local vpn_ip + vpn_ip=$(ip -4 addr show "$vpn_if" 2>/dev/null | awk '/inet / {print $2}' | cut -d/ -f1) + opsec_green "VPN: Active (${vpn_if}: ${vpn_ip})" + else + opsec_yellow "VPN: Not connected" + fi + + # ─── MAC ─── + if ! is_cloud_level; then + local primary_if + 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 + local cur_mac first_octet first_dec + 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 + opsec_green "MAC: Randomized (${primary_if}: ${cur_mac})" + else + opsec_yellow "MAC: Hardware (${primary_if}: ${cur_mac})" + fi + fi + fi + echo "" + + # ─── Hardening status ─── + opsec_cyan "System Hardening" + # Core dumps + local core_pattern + core_pattern=$(sysctl -n kernel.core_pattern 2>/dev/null) + [[ "$core_pattern" == *"/bin/false"* ]] && opsec_green " Core dumps: Disabled" || opsec_dim " Core dumps: enabled" + # Swap + local swap_total + swap_total=$(swapon --show=SIZE --noheadings 2>/dev/null | head -1) + [ -z "$swap_total" ] && opsec_green " Swap: Disabled" || opsec_dim " Swap: active (${swap_total})" + echo "" + + # ─── Hostname ─── + opsec_info "Hostname: $(hostname)" + + # ─── Boot services ─── + echo "" + opsec_cyan "Boot Services" + for svc in opsec-boot-advanced opsec-mac-randomize opsec-hostname-randomize opsec-killswitch; do + if systemctl is-enabled "$svc" 2>/dev/null | grep -q enabled; then + opsec_green " ${svc}: enabled" + else + opsec_dim " ${svc}: disabled" + fi + done + + # ─── Preflight ─── + if [ -x /usr/local/bin/opsec-preflight.sh ]; then + echo "" + opsec_cyan "Preflight Score" + /usr/local/bin/opsec-preflight.sh --score 2>/dev/null || opsec_dim " Preflight not available" + fi + + echo "" +} + +# ─── DIAGNOSTIC ────────────────────────────────────────────────────────────── +mode_diag() { + echo "=== OPSEC DIAGNOSTIC ===" + echo "Time: $(date -Is)" + echo "" + echo "--- State ---" + echo "Ghost mode: $([ -f "$STATE_FILE" ] && echo ACTIVE || echo INACTIVE)" + echo "Breakglass: $([ -f "$BREAKGLASS_STATE" ] && echo ACTIVE || echo INACTIVE)" + echo "" + echo "--- Tor ---" + echo "Service: $(systemctl is-active tor 2>&1)" + echo "TCP ports: $(ss -tlnp 2>/dev/null | grep -E ':(9040|9050|5353)' || echo NONE)" + echo "UDP ports: $(ss -ulnp 2>/dev/null | grep -E ':5353' || echo NONE)" + echo "Bootstrap: $(grep 'Bootstrapped' /run/tor/notices.log 2>/dev/null | tail -1 || echo NONE)" + echo "Last 5 log:" + tail -5 /run/tor/notices.log 2>/dev/null || echo " (no log)" + echo "" + echo "--- Kill Switch ---" + echo "Filter: $(iptables -L GP_FW >/dev/null 2>&1 && echo ARMED || echo OFF)" + local _diag_nat_count=0 + if iptables -t nat -L GP_NAT >/dev/null 2>&1; then + _diag_nat_count=$(iptables -t nat -L GP_NAT --line-numbers 2>/dev/null | tail -n +3 | wc -l) + echo "NAT chain: ARMED (${_diag_nat_count} rules)" + else + echo "NAT chain: OFF" + local _diag_out_nat + _diag_out_nat=$(iptables -t nat -L OUTPUT --line-numbers 2>/dev/null | tail -n +3 | wc -l) + [ "$_diag_out_nat" -gt 0 ] && echo "NAT OUTPUT fallback: ${_diag_out_nat} rules" + fi + echo "" + echo "--- DNS ---" + echo "Resolver: $(grep nameserver /etc/resolv.conf 2>/dev/null | tr '\n' ' ')" + echo "Locked: $(lsattr /etc/resolv.conf 2>/dev/null | grep -q 'i' && echo YES || echo NO)" + echo "NM dns=none: $([ -f /etc/NetworkManager/conf.d/opsec-dns-lock.conf ] && echo YES || echo NO)" + echo "" + echo "--- Network ---" + echo "Route: $(ip route show default 2>/dev/null | head -1)" + echo "" + echo "--- Quick Test ---" + echo -n "SOCKS: " + curl -s --max-time 5 --socks5-hostname 127.0.0.1:9050 https://check.torproject.org/api/ip 2>/dev/null || echo "FAILED" + echo "" + echo -n "TransPort: " + curl -s --max-time 10 https://check.torproject.org/api/ip 2>/dev/null || echo "FAILED (transparent proxy not working)" +} + +# ─── APPLY FLAGS ───────────────────────────────────────────────────────────── +VERBOSE="$_VERBOSE_EARLY" +if [ "$VERBOSE" = "true" ]; then + echo -e "\033[38;5;196m[!] VERBOSE: debug output may contain sensitive identifiers\033[0m" +fi + +# ─── MAIN ──────────────────────────────────────────────────────────────────── +case "${_CMD_EARLY:-}" in + on) mode_on ;; + off) mode_off ;; + status) mode_status ;; + diag) mode_diag ;; + breakglass) mode_breakglass ;; + breakglass-off) mode_breakglass_off ;; + *) + echo "Usage: sudo opsec-mode [--verbose] on|off|status|diag|breakglass|breakglass-off" + echo "" + echo " on — Activate ghost mode (Tor, kill switch, full lockdown)" + echo " off — Deactivate ghost mode (returns to base privacy)" + echo " status — Show current OPSEC subsystem status" + echo " diag — Quick diagnostic of all subsystems" + echo " breakglass — Emergency: temporarily drop kill switch" + echo " breakglass-off — End break-glass, re-arm kill switch" + echo "" + echo " --verbose, -v — Show debug output in real-time" + exit 1 + ;; +esac diff --git a/opsec/scripts/opsec-monitor.sh b/opsec/scripts/opsec-monitor.sh new file mode 100755 index 0000000..da31fe9 --- /dev/null +++ b/opsec/scripts/opsec-monitor.sh @@ -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 diff --git a/opsec/scripts/opsec-preflight.sh b/opsec/scripts/opsec-preflight.sh new file mode 100755 index 0000000..673d475 --- /dev/null +++ b/opsec/scripts/opsec-preflight.sh @@ -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 diff --git a/opsec/scripts/opsec-sentry.sh b/opsec/scripts/opsec-sentry.sh new file mode 100755 index 0000000..de104d0 --- /dev/null +++ b/opsec/scripts/opsec-sentry.sh @@ -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 diff --git a/opsec/scripts/opsec-ssh-check.sh b/opsec/scripts/opsec-ssh-check.sh new file mode 100755 index 0000000..da6a3f7 --- /dev/null +++ b/opsec/scripts/opsec-ssh-check.sh @@ -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 [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") [port]" + echo " ssh-safe [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 "" diff --git a/opsec/scripts/opsec-toggle.sh b/opsec/scripts/opsec-toggle.sh new file mode 100755 index 0000000..4a6b68a --- /dev/null +++ b/opsec/scripts/opsec-toggle.sh @@ -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 diff --git a/opsec/scripts/opsec-traffic-blend.sh b/opsec/scripts/opsec-traffic-blend.sh new file mode 100755 index 0000000..f8b6a3a --- /dev/null +++ b/opsec/scripts/opsec-traffic-blend.sh @@ -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 diff --git a/opsec/scripts/opsec-widget-launch.sh b/opsec/scripts/opsec-widget-launch.sh new file mode 100755 index 0000000..646c0d6 --- /dev/null +++ b/opsec/scripts/opsec-widget-launch.sh @@ -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)" diff --git a/opsec/scripts/opsec-wifi-check.sh b/opsec/scripts/opsec-wifi-check.sh new file mode 100755 index 0000000..fbc291e --- /dev/null +++ b/opsec/scripts/opsec-wifi-check.sh @@ -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 diff --git a/phantom/README.md b/phantom/README.md new file mode 100644 index 0000000..666f3fa --- /dev/null +++ b/phantom/README.md @@ -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 diff --git a/phantom/ansible.cfg b/phantom/ansible.cfg new file mode 100644 index 0000000..f4dfefd --- /dev/null +++ b/phantom/ansible.cfg @@ -0,0 +1,8 @@ +[defaults] +host_key_checking = False +retry_files_enabled = False +stdout_callback = yaml +timeout = 30 + +[ssh_connection] +pipelining = True diff --git a/phantom/modules/__init__.py b/phantom/modules/__init__.py new file mode 100644 index 0000000..8a5f2a3 --- /dev/null +++ b/phantom/modules/__init__.py @@ -0,0 +1 @@ +# phantom modules diff --git a/phantom/modules/all_in_one.py b/phantom/modules/all_in_one.py new file mode 100644 index 0000000..6d11ace --- /dev/null +++ b/phantom/modules/all_in_one.py @@ -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 diff --git a/phantom/modules/cloud.py b/phantom/modules/cloud.py new file mode 100644 index 0000000..8b47f3b --- /dev/null +++ b/phantom/modules/cloud.py @@ -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 diff --git a/phantom/modules/dns.py b/phantom/modules/dns.py new file mode 100644 index 0000000..cd97082 --- /dev/null +++ b/phantom/modules/dns.py @@ -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 diff --git a/phantom/modules/email.py b/phantom/modules/email.py new file mode 100644 index 0000000..0e06b44 --- /dev/null +++ b/phantom/modules/email.py @@ -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 diff --git a/phantom/modules/matrix.py b/phantom/modules/matrix.py new file mode 100644 index 0000000..0302df4 --- /dev/null +++ b/phantom/modules/matrix.py @@ -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 diff --git a/phantom/modules/media.py b/phantom/modules/media.py new file mode 100644 index 0000000..53057b1 --- /dev/null +++ b/phantom/modules/media.py @@ -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 diff --git a/phantom/modules/vault.py b/phantom/modules/vault.py new file mode 100644 index 0000000..41c8738 --- /dev/null +++ b/phantom/modules/vault.py @@ -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 diff --git a/phantom/modules/vpn.py b/phantom/modules/vpn.py new file mode 100644 index 0000000..2e90393 --- /dev/null +++ b/phantom/modules/vpn.py @@ -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 diff --git a/phantom/phantom.py b/phantom/phantom.py new file mode 100755 index 0000000..1e3431c --- /dev/null +++ b/phantom/phantom.py @@ -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") diff --git a/phantom/playbooks/all_in_one/main.yml b/phantom/playbooks/all_in_one/main.yml new file mode 100644 index 0000000..8d4f862 --- /dev/null +++ b/phantom/playbooks/all_in_one/main.yml @@ -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 diff --git a/phantom/playbooks/cloud/main.yml b/phantom/playbooks/cloud/main.yml new file mode 100644 index 0000000..2b9b462 --- /dev/null +++ b/phantom/playbooks/cloud/main.yml @@ -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." diff --git a/phantom/playbooks/common/base_hardening.yml b/phantom/playbooks/common/base_hardening.yml new file mode 100644 index 0000000..4459d71 --- /dev/null +++ b/phantom/playbooks/common/base_hardening.yml @@ -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 diff --git a/phantom/playbooks/dns/main.yml b/phantom/playbooks/dns/main.yml new file mode 100644 index 0000000..d012c6d --- /dev/null +++ b/phantom/playbooks/dns/main.yml @@ -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 diff --git a/phantom/playbooks/dns/tasks/configure.yml b/phantom/playbooks/dns/tasks/configure.yml new file mode 100644 index 0000000..a573ce8 --- /dev/null +++ b/phantom/playbooks/dns/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)') }}" diff --git a/phantom/playbooks/dns/tasks/install.yml b/phantom/playbooks/dns/tasks/install.yml new file mode 100644 index 0000000..07a1c61 --- /dev/null +++ b/phantom/playbooks/dns/tasks/install.yml @@ -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" } diff --git a/phantom/playbooks/email/main.yml b/phantom/playbooks/email/main.yml new file mode 100644 index 0000000..e9eab6c --- /dev/null +++ b/phantom/playbooks/email/main.yml @@ -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." diff --git a/phantom/playbooks/matrix/main.yml b/phantom/playbooks/matrix/main.yml new file mode 100644 index 0000000..bb2ab5b --- /dev/null +++ b/phantom/playbooks/matrix/main.yml @@ -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 diff --git a/phantom/playbooks/matrix/tasks/element.yml b/phantom/playbooks/matrix/tasks/element.yml new file mode 100644 index 0000000..112bdd5 --- /dev/null +++ b/phantom/playbooks/matrix/tasks/element.yml @@ -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" diff --git a/phantom/playbooks/matrix/tasks/nginx.yml b/phantom/playbooks/matrix/tasks/nginx.yml new file mode 100644 index 0000000..4b3e6a6 --- /dev/null +++ b/phantom/playbooks/matrix/tasks/nginx.yml @@ -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 diff --git a/phantom/playbooks/matrix/tasks/synapse.yml b/phantom/playbooks/matrix/tasks/synapse.yml new file mode 100644 index 0000000..6bc1c72 --- /dev/null +++ b/phantom/playbooks/matrix/tasks/synapse.yml @@ -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 diff --git a/phantom/playbooks/matrix/templates/homeserver.yaml.j2 b/phantom/playbooks/matrix/templates/homeserver.yaml.j2 new file mode 100644 index 0000000..d364b23 --- /dev/null +++ b/phantom/playbooks/matrix/templates/homeserver.yaml.j2 @@ -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' diff --git a/phantom/playbooks/media/main.yml b/phantom/playbooks/media/main.yml new file mode 100644 index 0000000..e57bbd4 --- /dev/null +++ b/phantom/playbooks/media/main.yml @@ -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." diff --git a/phantom/playbooks/vault/main.yml b/phantom/playbooks/vault/main.yml new file mode 100644 index 0000000..1f3727b --- /dev/null +++ b/phantom/playbooks/vault/main.yml @@ -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." diff --git a/phantom/playbooks/vpn/main.yml b/phantom/playbooks/vpn/main.yml new file mode 100644 index 0000000..91d9cdb --- /dev/null +++ b/phantom/playbooks/vpn/main.yml @@ -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 diff --git a/phantom/playbooks/vpn/tasks/configure.yml b/phantom/playbooks/vpn/tasks/configure.yml new file mode 100644 index 0000000..8fb9774 --- /dev/null +++ b/phantom/playbooks/vpn/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 diff --git a/phantom/playbooks/vpn/tasks/install.yml b/phantom/playbooks/vpn/tasks/install.yml new file mode 100644 index 0000000..b5220c3 --- /dev/null +++ b/phantom/playbooks/vpn/tasks/install.yml @@ -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 diff --git a/phantom/providers/aws.yml b/phantom/providers/aws.yml new file mode 100644 index 0000000..9fd6f65 --- /dev/null +++ b/phantom/providers/aws.yml @@ -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 diff --git a/phantom/providers/flokinet.yml b/phantom/providers/flokinet.yml new file mode 100644 index 0000000..3ee7bce --- /dev/null +++ b/phantom/providers/flokinet.yml @@ -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 }} diff --git a/phantom/providers/linode.yml b/phantom/providers/linode.yml new file mode 100644 index 0000000..e545281 --- /dev/null +++ b/phantom/providers/linode.yml @@ -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