mirror of
https://github.com/joshuaboniface/rffmpeg.git
synced 2026-01-22 18:17:36 +00:00
feat: Added python version of the wrapper
- GPT-OSS:120b did the converson of the script from bash to python3 - Updated rsyslog definition to work with either bash or python -file - updated SSH config to use the python version
This commit is contained in:
parent
5994d0e7a0
commit
f5681397a7
4 changed files with 160 additions and 8 deletions
|
|
@ -19,14 +19,14 @@ These were tested and validated on Ubuntu 24.04 LTS, 2025-11-03
|
|||
SSH server configuration is formed out of two files
|
||||
|
||||
1. `10-jellyfin-limits.conf` - SSH config
|
||||
2. `limited-wrapper.sh` - a script to limit what commands can be run
|
||||
2. `limited-wrapper.sh` or `limited-wrapper.py` - a script to limit what commands can be run
|
||||
|
||||
### 10-jellyfin-limits.conf
|
||||
|
||||
This config file does few things
|
||||
- allows only jellyfin user to SSH from jellyfin server
|
||||
- limits jellyfin user login options to be only from jellyfin server
|
||||
- limits the commands jellyfin user can run to `limited-wrapper.sh`
|
||||
- limits the commands jellyfin user can run to `limited-wrapper.py`
|
||||
|
||||
1. Copy `10-jellyfin-limits.conf` to `/etc/ssh/sshd_config.d`
|
||||
2. Update the IP of the jellyfin server to the file
|
||||
|
|
@ -35,16 +35,16 @@ This config file does few things
|
|||
sudo systemctl restart ssh
|
||||
```
|
||||
|
||||
### limited-wrapper.sh
|
||||
### limited-wrapper.sh and limited-wrapper.py
|
||||
|
||||
This file analyses what commands are being run over SSH and limits them
|
||||
to the ones we defined.
|
||||
|
||||
1. Update the ALLOWED list to match your `ffmpeg` file locations in the script
|
||||
2. Copy the script to `/usr/local/bin/limited-wrapper.sh` and allow only root to modify it
|
||||
2. Copy the script to `/usr/local/bin/limited-wrapper.py` and allow only root to modify it
|
||||
```bash
|
||||
sudo chwon root:root /usr/local/bin/limited-wrapper.sh &&\
|
||||
sudo chmod 755 /usr/local/bin/limited-wrapper.sh
|
||||
sudo chwon root:root /usr/local/bin/limited-wrapper.py &&\
|
||||
sudo chmod 755 /usr/local/bin/limited-wrapper.py
|
||||
```
|
||||
### Test configuration
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ Match Address IPJELLYFIN
|
|||
|
||||
Match User jellyfin, Address IPJELLYFIN
|
||||
AllowUsers jellyfin@IPJELLYFIN
|
||||
ForceCommand /usr/local/bin/limited-wrapper.sh
|
||||
ForceCommand /usr/local/bin/limited-wrapper.py
|
||||
PermitTTY no
|
||||
X11Forwarding no
|
||||
AllowAgentForwarding no
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
# Match the tag *including* the trailing colon
|
||||
:syslogtag, startswith, "limited-wrapper.sh" /var/log/jellyfin_commands.log
|
||||
:syslogtag, startswith, "limited-wrapper" /var/log/jellyfin_commands.log
|
||||
& stop
|
||||
152
hardening/limited-wrapper.py
Normal file
152
hardening/limited-wrapper.py
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
#!/usr/bin/env python3
|
||||
"""limited-wrapper.py
|
||||
|
||||
Author: GPT-OSS:120b
|
||||
Version: 1.1.0
|
||||
Date: 2025-11-03
|
||||
|
||||
Python 3 implementation of the limited-wrapper.sh script.
|
||||
It restricts SSH command execution to a whitelist of allowed binaries
|
||||
and logs activity either to the console (interactive) or to syslog.
|
||||
|
||||
History
|
||||
1.0.0 - 2025-11-03, initial version
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import shlex
|
||||
import logging
|
||||
import logging.handlers
|
||||
from typing import List
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Logging utilities
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _setup_logger() -> logging.Logger:
|
||||
logger = logging.getLogger("limited-wrapper.py")
|
||||
logger.setLevel(logging.DEBUG) # Capture all levels; handlers will filter
|
||||
# Ensure no duplicate handlers if the module is reloaded
|
||||
logger.handlers.clear()
|
||||
|
||||
if sys.stdout.isatty():
|
||||
# Interactive TTY – simple console output without timestamp or level prefix
|
||||
console = logging.StreamHandler(sys.stdout)
|
||||
console.setLevel(logging.INFO)
|
||||
console.setFormatter(logging.Formatter("%(message)s"))
|
||||
logger.addHandler(console)
|
||||
else:
|
||||
# Non‑interactive – forward to syslog. Let syslog generate its own timestamp,
|
||||
# hostname, and program identifier (the logger name). No extra formatter is
|
||||
# needed to avoid adding the PID or duplicate timestamps.
|
||||
try:
|
||||
syslog = logging.handlers.SysLogHandler(address="/dev/log")
|
||||
except OSError:
|
||||
# Fallback for systems without /dev/log (e.g., macOS)
|
||||
syslog = logging.handlers.SysLogHandler(address=("localhost", 514))
|
||||
syslog.setLevel(logging.DEBUG)
|
||||
# Prefix with logger name (script tag) to match original format
|
||||
syslog.setFormatter(logging.Formatter("%(name)s: %(message)s"))
|
||||
logger.addHandler(syslog)
|
||||
return logger
|
||||
|
||||
_logger = _setup_logger()
|
||||
|
||||
|
||||
def log_msg(level: str, *msg: str) -> None:
|
||||
"""Log a message with an explicit level prefix.
|
||||
|
||||
The original Bash implementation prefixed the log line with the level
|
||||
(e.g. ``DEBUG`` or ``INFO``) before sending it to syslog. To preserve that
|
||||
format we construct ``full_msg = f"{level.upper()} {text}"`` and log the
|
||||
resulting string. This ensures syslog entries look like:
|
||||
``limited-wrapper.sh: DEBUG <message>`` while interactive console output
|
||||
remains readable.
|
||||
"""
|
||||
text = " ".join(msg)
|
||||
level = level.upper()
|
||||
full_msg = f"{level} {text}"
|
||||
if level == "DEBUG":
|
||||
_logger.debug(full_msg)
|
||||
elif level == "INFO":
|
||||
_logger.info(full_msg)
|
||||
elif level in ("WARN", "WARNING"):
|
||||
_logger.warning(full_msg)
|
||||
elif level == "ERROR":
|
||||
_logger.error(full_msg)
|
||||
else:
|
||||
_logger.info(full_msg)
|
||||
|
||||
|
||||
def log_debug(*msg: str) -> None:
|
||||
log_msg("DEBUG", *msg)
|
||||
|
||||
|
||||
def log_info(*msg: str) -> None:
|
||||
log_msg("INFO", *msg)
|
||||
|
||||
|
||||
def log_warn(*msg: str) -> None:
|
||||
log_msg("WARN", *msg)
|
||||
|
||||
|
||||
def log_error(*msg: str) -> None:
|
||||
log_msg("ERROR", *msg)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Whitelist of absolute paths to allowed binaries
|
||||
# ---------------------------------------------------------------------------
|
||||
ALLOWED: List[str] = [
|
||||
"/usr/bin/ffmpeg",
|
||||
"/usr/bin/ffprobe",
|
||||
"/usr/local/bin/ffmpeg",
|
||||
"/usr/local/bin/ffprobe",
|
||||
"/usr/lib/jellyfin-ffmpeg/ffmpeg",
|
||||
"/usr/lib/jellyfin-ffmpeg/ffprobe",
|
||||
]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
req_cmd = os.getenv("SSH_ORIGINAL_COMMAND", "")
|
||||
if not req_cmd:
|
||||
# No command supplied – show the whitelist and exit successfully
|
||||
print("You may run only: " + " ".join(ALLOWED))
|
||||
sys.exit(0)
|
||||
|
||||
# Parse the command string respecting shell quoting (handles spaces in arguments)
|
||||
# Using shlex.split provides proper handling of quoted arguments, unlike the
|
||||
# original bash script which split on whitespace only.
|
||||
try:
|
||||
args = shlex.split(req_cmd, posix=True)
|
||||
except ValueError as e:
|
||||
log_error(f"Failed to parse SSH_ORIGINAL_COMMAND: {e}")
|
||||
print("ERROR: could not parse command.")
|
||||
sys.exit(1)
|
||||
|
||||
if not args:
|
||||
log_error("Empty command after parsing.")
|
||||
print("ERROR: empty command.")
|
||||
sys.exit(1)
|
||||
|
||||
bin_path = os.path.realpath(args[0])
|
||||
log_debug(f"Checking for bin {bin_path}")
|
||||
|
||||
if bin_path in ALLOWED:
|
||||
log_info(f"Running command {req_cmd}")
|
||||
# Ensure the argument list uses the resolved binary path as argv[0]
|
||||
args[0] = bin_path
|
||||
# Replace the current process with the requested command without PATH lookup
|
||||
os.execv(bin_path, args)
|
||||
# execv only returns on failure
|
||||
log_error(f"Failed to exec {req_cmd}")
|
||||
sys.exit(1)
|
||||
else:
|
||||
log_error(f"Not allowed {req_cmd}")
|
||||
print("ERROR: command not allowed.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Add table
Add a link
Reference in a new issue