407 lines
18 KiB
Python
407 lines
18 KiB
Python
#!/usr/bin/env python
|
|
|
|
"""
|
|
Harden FreeBSD system perms, settings.
|
|
Set and reset rc, sysctl, login, confs; set file perms, run shell commands
|
|
Uses ini file in the same directory.
|
|
|
|
Usage:
|
|
./harden-freebsd.py
|
|
./harden-freebsd.py <ini file>
|
|
./harden-freebsd.py restore
|
|
"""
|
|
|
|
__author__ = "Elias Christopher Griffin"
|
|
__url__ = "https://www.quadhelion.engineering"
|
|
__license__ = "QHELP-OME-NC-ND-HI"
|
|
__copyright__ = "https://www.quadhelion.engineering/qhelp.html"
|
|
__version__ = "3.1"
|
|
__date__ = "01/20/2024"
|
|
__email__ = "elias@quadhelion.engineering"
|
|
__status__ = "Production"
|
|
|
|
|
|
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
import os, re, subprocess, syslog, configparser, shutil, sys
|
|
|
|
_date = datetime.now()
|
|
date_time = _date.strftime("%m/%d/%Y, %H:%M")
|
|
|
|
|
|
# Setup for script argument processing
|
|
sysargs = sys.argv
|
|
config = configparser.ConfigParser()
|
|
|
|
|
|
# Setup to manipulate all the system files
|
|
harden_freebsd_log = Path("/var/log/harden-freebsd.log")
|
|
rc_conf = Path("/etc/rc.conf")
|
|
sysctl_conf = Path("/etc/sysctl.conf")
|
|
loader_conf = Path("/boot/loader.conf")
|
|
login_conf = Path("/etc/login.conf")
|
|
cron_access = Path("/var/cron/allow")
|
|
at_access = Path("/var/at/at.allow")
|
|
|
|
|
|
# Set the backup file names
|
|
backup_suffix = ".original"
|
|
rc_backup = rc_conf.with_name(rc_conf.name + backup_suffix)
|
|
sysctl_backup = sysctl_conf.with_name(sysctl_conf.name + backup_suffix)
|
|
loader_backup = loader_conf.with_name(loader_conf.name + backup_suffix)
|
|
login_backup = login_conf.with_name(login_conf.name + backup_suffix)
|
|
|
|
|
|
# Handle script arguments
|
|
sysargs = sys.argv
|
|
config = configparser.ConfigParser()
|
|
|
|
if len(sys.argv) == 2:
|
|
script_argument = sys.argv[1]
|
|
script_argument_path = Path(script_argument)
|
|
if script_argument_path.exists and script_argument_path.suffix == ".ini":
|
|
config.read(script_argument)
|
|
elif sys.argv[1] == "restore":
|
|
rc_conf.write_bytes(rc_backup.read_bytes())
|
|
sysctl_conf.write_bytes(sysctl_backup.read_bytes())
|
|
loader_conf.write_bytes(loader_backup.read_bytes())
|
|
print(f"\n*********************\033[38;5;76m Success \033[0;0m*************************")
|
|
print("Original rc.conf, sysctl.conf, loader.conf restored")
|
|
print(f"*******************************************************\n")
|
|
exit(0)
|
|
else:
|
|
pass
|
|
else:
|
|
config.read('settings.ini')
|
|
|
|
|
|
# Handle Errors
|
|
def exception_handler(func):
|
|
def intake(*args, **kwargs):
|
|
try:
|
|
func(*args, **kwargs)
|
|
except PermissionError as e:
|
|
print(f"\n******************\033[38;5;1m Permissions \033[0;0m************************\n")
|
|
print(f"Insufficient permissions {e}")
|
|
print(f"*******************************************************\n")
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
return intake
|
|
|
|
|
|
|
|
# Write to either "syslog" (/var/log/messages) or "script" (/var/log/harden-freebsd.log)
|
|
@exception_handler
|
|
def writeLog(log_type, content):
|
|
harden_freebsd_logwriter = open(harden_freebsd_log, "a")
|
|
syslog.openlog("LOG_INFO")
|
|
if log_type == "script":
|
|
harden_freebsd_logwriter.writelines(content + os.linesep)
|
|
elif log_type == "syslog":
|
|
syslog.syslog(1, content)
|
|
else:
|
|
print(f"*******************************************************\n")
|
|
print(f"\033[38;5;63m LOG: \033[0;0m {content}")
|
|
print(f"*******************************************************\n")
|
|
|
|
|
|
|
|
# Make *.original backups of all files only once
|
|
if config['SCRIPT']['first_run'] == "True":
|
|
try:
|
|
harden_freebsd_log.touch()
|
|
cron_access.touch()
|
|
at_access.touch()
|
|
rc_backup.write_bytes(rc_conf.read_bytes())
|
|
sysctl_backup.write_bytes(sysctl_conf.read_bytes())
|
|
loader_backup.write_bytes(loader_conf.read_bytes())
|
|
login_backup.write_bytes(login_conf.read_bytes())
|
|
except FileNotFoundError as e:
|
|
error_path = Path(e.filename)
|
|
print(f"\n********************\033[38;5;1m File Not Found \033[0;0m*******************")
|
|
print(f"Filename: {error_path.parts}")
|
|
print("*******************************************************\n")
|
|
except PermissionError as e:
|
|
print(f"\n******************\033[38;5;1m Permissions \033[0;0m************************\n")
|
|
print(f"Insufficient permissions {e}")
|
|
print(f"*******************************************************\n")
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
else:
|
|
writeLog("syslog", "System file backups complete")
|
|
with open('settings.ini', 'w') as configfile:
|
|
config.set('SCRIPT', 'first_run', 'False')
|
|
config.write(configfile)
|
|
print(f"\n*********************\033[38;5;76m Success \033[0;0m*************************")
|
|
print(f"\033[38;5;75mCreated: \033[0;0m")
|
|
print(f" {cron_access.name}, {at_access.name} \n")
|
|
print(f"\033[38;5;75mBackups Made: \033[0;0m")
|
|
print(f" {rc_backup.name}, {sysctl_backup.name}")
|
|
print(f" {login_backup.name}, {loader_backup.name} \n")
|
|
print(f"*******************************************************\n")
|
|
|
|
|
|
|
|
# Read the system file content
|
|
try:
|
|
rc_content = rc_conf.read_text(encoding="utf-8")
|
|
sysctl_content = sysctl_conf.read_text(encoding="utf-8")
|
|
login_content = login_conf.read_text(encoding="utf-8")
|
|
loader_content = loader_conf.read_text(encoding="utf-8")
|
|
except FileNotFoundError as e:
|
|
error_path = Path(e.filename)
|
|
writeLog("script", "Error finding file " + error_path)
|
|
print(f"\n*******************\033[38;5;1m File Not Found \033[0;0m********************")
|
|
print(f"Filename: {error_path.name}")
|
|
print(f"Directories used: {error_path.parts}\n")
|
|
print("*******************************************************\n")
|
|
except PermissionError as e:
|
|
print(f"\n******************\033[38;5;1m Permissions \033[0;0m************************\n")
|
|
print(f"Permission to read/append {e}")
|
|
print(f"{os.stat(rc_conf)}{os.linesep}")
|
|
print(f"{os.stat(sysctl_conf)}{os.linesep}")
|
|
print(f"{os.stat(loader_conf)}{os.linesep}")
|
|
print(f"{os.stat(cron_access)}{os.linesep}")
|
|
print(f"{os.stat(at_access)}{os.linesep}")
|
|
print(f"{os.stat(login_conf)}{os.linesep}")
|
|
print(f"*******************************************************\n")
|
|
else:
|
|
print(
|
|
f"{os.linesep}*********************\033[38;5;75m Running \033[0;0m*************************{os.linesep}"
|
|
f"Loaded system files for read/append\n"
|
|
f"*******************************************************{os.linesep}"
|
|
)
|
|
finally:
|
|
writeLog("syslog", "Hardening in progress")
|
|
print(f"\n********************\033[38;5;75m Info Panel \033[0;0m***********************")
|
|
print(f"Executing {__file__}")
|
|
print(f"Executing {date_time}")
|
|
print(f"*******************************************************\n")
|
|
|
|
|
|
# Main working class dealing with rc.conf and sysctl.conf
|
|
class Conf:
|
|
def __init__(self, file, setting, flag):
|
|
self.file = file
|
|
self.setting = setting
|
|
self.flag = flag
|
|
|
|
# Changes the flag from whatever it is currently to flag in settings.ini
|
|
def setConf(self):
|
|
try:
|
|
with open(self.file, 'r+', encoding="us-ascii") as file_content:
|
|
lines = file_content.readlines()
|
|
for i, line in enumerate(lines):
|
|
if line.startswith(self.setting):
|
|
lines[i] = self.setting + "=" + self.flag + os.linesep
|
|
file_content.seek(0)
|
|
for line in lines:
|
|
file_content.write(line)
|
|
file_content.truncate()
|
|
writeLog("script", self.setting + " was set to " + self.flag)
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
else:
|
|
print(f"\033[38;5;208m {self.setting} \033[0;0m changed to\033[38;5;208m {self.flag}\033[0;0m in\033[38;5;75m {self.file}\033[0;0m ")
|
|
|
|
# Appends at the end of a file a directive that was not present previously
|
|
def addConf(self):
|
|
try:
|
|
with open(self.file, 'a') as file_content:
|
|
file_content.write(self.setting + "=" + self.flag + os.linesep)
|
|
writeLog("script", self.setting + "=" + self.flag + " added")
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
else:
|
|
print(f"\033[38;5;63m {self.setting} \033[0;0m added to \033[38;5;75m{self.file}\033[0;0m ")
|
|
|
|
# Checks to see if the directive is already in the conf, returns True if present.
|
|
def checkConf(self) -> bool:
|
|
self.found = False
|
|
try:
|
|
with open(self.file, 'r') as file_content:
|
|
lines = file_content.readlines()
|
|
for i, line in enumerate(lines):
|
|
if line.startswith(self.setting):
|
|
self.found = True
|
|
else:
|
|
pass
|
|
return self.found
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
|
|
# Checks proper flag and equality syntax in rc.conf and sysctl.conf with first-boot 13.2 directives
|
|
# May not work with advanced flags added later
|
|
def verifyConf(self):
|
|
global conf_directives
|
|
conf_directives = []
|
|
sysctl_conf_verify = re.compile(r'[^\"]') # No quotes
|
|
loader_rc_conf_verify = re.compile(r'^[\"].+[$\"]') # Pair of quotes
|
|
try:
|
|
with open(self.file, 'r+') as file_content:
|
|
lines = file_content.readlines()
|
|
for i, line in enumerate(lines):
|
|
partitioned_line = line.partition("=")
|
|
if line.isspace():
|
|
pass
|
|
elif line.startswith("#"):
|
|
pass
|
|
elif partitioned_line[1] != "=":
|
|
print(f"\n*******************************************************")
|
|
print(f"Error at {lines[i]}: No equality operator. Restored original.")
|
|
print(f"*******************************************************\n")
|
|
writeLog("script", "No equality operator at line " + lines[i].rstrip() + " in " + self.file.name)
|
|
self.restoreConf()
|
|
sys.exit()
|
|
elif self.file == rc_conf and re.match(loader_rc_conf_verify, partitioned_line[2]) == None:
|
|
print(f"\n*******************************************************")
|
|
print(f"Error: {self.flag} not allowed in {lines[i]} in {self.file}. Restored original.")
|
|
print(f"*******************************************************\n")
|
|
writeLog("script", "Quote matching error at line " + lines[i].rstrip())
|
|
self.restoreConf()
|
|
sys.exit()
|
|
elif self.file == loader_conf and re.match(loader_rc_conf_verify, partitioned_line[2]) == None:
|
|
print(f"\n*******************************************************")
|
|
print(f"Error: {self.flag} not allowed in {lines[i]} in {self.file}. Restored original.")
|
|
print(f"*******************************************************\n")
|
|
writeLog("script", "Quote matching error at line " + lines[i].rstrip())
|
|
self.restoreConf()
|
|
sys.exit()
|
|
elif self.file == sysctl_conf and re.match(sysctl_conf_verify, partitioned_line[2]) == None:
|
|
print(f"\n*******************************************************")
|
|
print(f"Error: {self.flag} not allowed in {lines[i].rstrip()} in {self.file}. Restored original.")
|
|
print(f"*******************************************************\n")
|
|
writeLog("script", "Quote in sysctl.conf at line " + lines[i])
|
|
self.restoreConf()
|
|
sys.exit()
|
|
else:
|
|
conf_directives.append(line.rstrip())
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
|
|
# If syntax verification fails, restore *.originals to prevent boot failure
|
|
# If in single user read-only mode use commands:
|
|
# zfs set readonly=false zroot
|
|
# zfs mount -a
|
|
def restoreConf(self):
|
|
try:
|
|
if self.file == rc_conf:
|
|
shutil.copy(rc_backup, rc_conf)
|
|
elif self.file == sysctl_conf:
|
|
shutil.copy(sysctl_backup, sysctl_conf)
|
|
elif self.file == loader_conf:
|
|
shutil.copy(loader_backup, loader_conf)
|
|
except FileNotFoundError as e:
|
|
error_path = Path(e.filename)
|
|
print(f"\n*******************\033[38;5;1m File Not Found \033[0;0m********************")
|
|
print(f"Filename: {error_path.name}")
|
|
print(f"Directories used: {error_path.parts}\n")
|
|
print("*******************************************************\n")
|
|
else:
|
|
print(f"\n*********************\033[38;5;76m Success \033[0;0m*************************")
|
|
print(f"Files restored")
|
|
print("*******************************************************\n")
|
|
|
|
|
|
|
|
# Hardcoded sections as only t e contain flags we can dynamically set and re-set.
|
|
# Loops through all directives and sets each
|
|
class SetOpts:
|
|
def __init__(self, section):
|
|
self.section = section
|
|
if self.section == "STARTUP":
|
|
file = rc_conf
|
|
elif self.section == "SYSTEM":
|
|
file = sysctl_conf
|
|
elif self.section == "KERNEL":
|
|
file = loader_conf
|
|
else:
|
|
pass
|
|
|
|
for opt in config[self.section]:
|
|
value = config[self.section][opt]
|
|
conf_runner = Conf(file, opt, value)
|
|
setting_present = conf_runner.checkConf()
|
|
if setting_present:
|
|
conf_runner.setConf()
|
|
conf_runner.verifyConf()
|
|
else:
|
|
conf_runner.addConf()
|
|
conf_runner.verifyConf()
|
|
|
|
|
|
|
|
# Run shell commands for named section. Will error if sent setting.ini sections that have no shell commands.
|
|
def shellCommand(section):
|
|
try:
|
|
for opt in config[section]:
|
|
value = config[section][opt]
|
|
command_result = subprocess.run([value], shell=True, timeout=1.7)
|
|
except subprocess.CalledProcessError as e:
|
|
syslog.syslog(syslog.LOG_ERR, "Failure: Shell Command")
|
|
print(f"\n*********************\033[38;5;1m Shell Error \033[0;0m*********************")
|
|
print(f"Command {e.args[1]} failed")
|
|
print(f"Terminated by {command_result.returncode}")
|
|
print(f"{command_result.stderr}")
|
|
print("*******************************************************\n")
|
|
except OSError as e:
|
|
print(f"\n********************\033[38;5;1m Locked \033[0;0m**************************\n")
|
|
print(f"Perhaps file is busy, locked, process blocked, or raced:\n")
|
|
print(f"{e}")
|
|
print(f"*******************************************************\n")
|
|
else:
|
|
print(f"\n*********************\033[38;5;76m Success \033[0;0m*************************")
|
|
print(f"\033[38;5;208mShell Error: {opt}\033[0;0m {command_result.stdout}")
|
|
print(f"*******************************************************\n")
|
|
|
|
|
|
|
|
# Set chmod for added convienence of the adminstrator with error handling and logging
|
|
@exception_handler
|
|
def setChmod(file, setting):
|
|
os.fchmod(file, setting)
|
|
writeLog("script", file + "was set to " + setting )
|
|
|
|
|
|
|
|
|
|
# Main
|
|
writeLog("script", date_time)
|
|
SetOpts("STARTUP")
|
|
SetOpts("SYSTEM")
|
|
SetOpts("KERNEL")
|
|
shellCommand("FILESEC")
|
|
shellCommand("USERSEC")
|
|
|
|
|
|
# Write succesfull completion to console and syslog
|
|
writeLog("script", "************ SUCCESS ************")
|
|
writeLog("script", "All files and directives validate")
|
|
writeLog("script", "*********************************")
|
|
writeLog("syslog", "SUCCESS: Hardening completed")
|
|
|
|
print(f"\n*********************\033[38;5;76m Success \033[0;0m*************************")
|
|
print("All files and directives validate")
|
|
print("Package Security Report generated; pkg-audit-report")
|
|
print(f"*******************************************************\n")
|
|
|
|
# EOF
|