From 9b0b8cfa9976893a5ddcf0b422f1b4095a85bbe1 Mon Sep 17 00:00:00 2001 From: guzzijones12 Date: Thu, 21 May 2026 19:43:15 -0400 Subject: [PATCH 1/4] configgen fix --- conf/st2.conf.sample | 7 ++ .../runners/local_runner/local_runner/base.py | 40 +++++++ packaging/common/scripts/st2-setup-sudo.sh | 110 ++++++++++++++++++ packaging/deb/scripts/post-install.sh | 13 +++ .../deb/systemd/st2actionrunner@.service | 2 +- packaging/rpm/scripts/post-install.sh | 13 +++ .../rpm/systemd/st2actionrunner@.service | 2 +- st2common/st2common/config.py | 23 ++++ 8 files changed, 208 insertions(+), 2 deletions(-) create mode 100755 packaging/common/scripts/st2-setup-sudo.sh diff --git a/conf/st2.conf.sample b/conf/st2.conf.sample index 27a2eb0a86..cd9abfc9ad 100644 --- a/conf/st2.conf.sample +++ b/conf/st2.conf.sample @@ -361,6 +361,13 @@ validate_trigger_parameters = True # True to validate payload for non-system trigger types when dispatching a trigger inside the sensor. By default, only payload for system triggers is validated. validate_trigger_payload = True +[system_security] +# List of users that st2 service can run commands as. Only applies in restricted mode. +allowed_run_as_users = stanley,root # comma separated list allowed here. +# Security mode for action execution. legacy: Full sudo access (backward compatible). restricted: Limited to /opt/stackstorm paths only. +# Valid values: legacy, restricted +security_mode = legacy + [system_user] # SSH private key for the system user. ssh_key_file = /home/stanley/.ssh/stanley_rsa diff --git a/contrib/runners/local_runner/local_runner/base.py b/contrib/runners/local_runner/local_runner/base.py index 436d7dbf6b..51695b6b59 100644 --- a/contrib/runners/local_runner/local_runner/base.py +++ b/contrib/runners/local_runner/local_runner/base.py @@ -104,6 +104,46 @@ def pre_run(self): RUNNER_TIMEOUT, runner_constants.LOCAL_RUNNER_DEFAULT_ACTION_TIMEOUT ) + # Validate security restrictions in restricted mode + self._validate_security_restrictions() + + def _validate_security_restrictions(self): + """ + Validate action execution against security mode restrictions. + Only enforced in restricted mode. + """ + security_mode = cfg.CONF.system_security.security_mode + + if security_mode != "restricted": + # Legacy mode - no restrictions + return + + # Restricted mode validations + allowed_users = cfg.CONF.system_security.allowed_run_as_users + + # Check if target user is allowed + if self._user and self._user not in allowed_users: + raise ValueError( + f"Security violation: User '{self._user}' is not in allowed_run_as_users. " + f"Allowed users: {', '.join(allowed_users)}. " + f"Update [system_security] allowed_run_as_users in st2.conf to allow this user." + ) + + # In restricted mode, validate that entry_point is under /opt/stackstorm + if self.entry_point: + base_path = cfg.CONF.system.base_path + if not self.entry_point.startswith(base_path + "/"): + raise ValueError( + f"Security violation: Script '{self.entry_point}' is outside {base_path}. " + f"In restricted mode, only scripts under {base_path} are allowed. " + f"Switch to legacy mode in st2.conf if you need to run external scripts." + ) + + LOG.debug( + f"Security validation passed: mode={security_mode}, user={self._user}, " + f"entry_point={self.entry_point}" + ) + def _run(self, action): env_vars = self._env diff --git a/packaging/common/scripts/st2-setup-sudo.sh b/packaging/common/scripts/st2-setup-sudo.sh new file mode 100755 index 0000000000..76f55dd9e4 --- /dev/null +++ b/packaging/common/scripts/st2-setup-sudo.sh @@ -0,0 +1,110 @@ +#!/bin/bash +# Generates sudo configuration for ST2 based on security mode +# Copyright 2020 The StackStorm Authors. + +set -e + +ST2_CONF="${ST2_CONF:-/etc/st2/st2.conf}" +SUDOERS_FILE="/etc/sudoers.d/st2" + +# Function to read config value from st2.conf +get_config_value() { + local section="$1" + local key="$2" + local default="$3" + + # Try to read from config file + if [ -f "$ST2_CONF" ]; then + # Look for the section and key + value=$(awk -v section="$section" -v key="$key" ' + /^\[/ { in_section=0 } + $0 == "["section"]" { in_section=1; next } + in_section && $0 ~ "^"key" *= *" { + sub("^"key" *= *", ""); + gsub(/^[ \t]+|[ \t]+$/, ""); + print; + exit + } + ' "$ST2_CONF") + + if [ -n "$value" ]; then + echo "$value" + return + fi + fi + + # Return default if not found + echo "$default" +} + +# Read security mode from config (defaults to legacy) +SECURITY_MODE=$(get_config_value "system_security" "security_mode" "legacy") + +echo "Configuring ST2 sudo access in ${SECURITY_MODE} mode..." + +case "$SECURITY_MODE" in + restricted) + cat > "$SUDOERS_FILE" << 'EOF' +# ST2 Restricted Security Mode +# ST2 service can only execute commands from /opt/stackstorm +# This provides enhanced security by limiting the scope of sudo access + +# Allow execution of pack actions, sensors, and ST2 scripts +Cmnd_Alias ST2_COMMANDS = /opt/stackstorm/packs/*/actions/*, \ + /opt/stackstorm/packs/*/sensors/*, \ + /opt/stackstorm/st2/bin/*, \ + /usr/bin/bash -c /opt/stackstorm/*, \ + /bin/bash -c /opt/stackstorm/* + +# Allow st2 to run commands as stanley and root users +# Only commands from ST2_COMMANDS alias are allowed +st2 ALL=(stanley) NOPASSWD: ST2_COMMANDS +st2 ALL=(root) NOPASSWD: ST2_COMMANDS + +# Explicitly deny dangerous security-related commands +Cmnd_Alias DANGEROUS = /usr/bin/passwd, \ + /usr/sbin/visudo, \ + /bin/su, \ + /usr/bin/sudo + +st2 ALL=(ALL) !DANGEROUS +EOF + ;; + + legacy|*) + cat > "$SUDOERS_FILE" << 'EOF' +# ST2 Legacy Security Mode (backward compatible) +# ST2 service has broad sudo access to maintain compatibility with existing actions +# This is the default mode to ensure smooth upgrades + +# Allow st2 to run as stanley and root users with full sudo access +st2 ALL=(stanley,root) NOPASSWD: ALL + +# Still explicitly deny changing security settings to prevent privilege escalation +Cmnd_Alias DANGEROUS = /usr/bin/passwd root, \ + /usr/sbin/visudo + +st2 ALL=(ALL) !DANGEROUS +EOF + ;; +esac + +# Set correct permissions on sudoers file +chmod 0440 "$SUDOERS_FILE" + +# Validate the sudoers file +if command -v visudo >/dev/null 2>&1; then + if visudo -c -f "$SUDOERS_FILE" >/dev/null 2>&1; then + echo "✓ Sudo configuration validated and updated: $SUDOERS_FILE" + else + echo "✗ ERROR: Generated sudoers file has syntax errors!" + echo " Removing invalid file to prevent system issues..." + rm -f "$SUDOERS_FILE" + exit 1 + fi +else + echo "⚠ Warning: visudo not found, skipping validation" + echo " Sudo configuration updated: $SUDOERS_FILE" +fi + +exit 0 \ No newline at end of file diff --git a/packaging/deb/scripts/post-install.sh b/packaging/deb/scripts/post-install.sh index df4959e3f0..7f91911364 100644 --- a/packaging/deb/scripts/post-install.sh +++ b/packaging/deb/scripts/post-install.sh @@ -132,6 +132,19 @@ case "$1" in done extract_st2_pack examples --target /usr/share/doc/st2/examples || : + # Setup sudo configuration based on security mode + if [ -x /opt/stackstorm/st2/bin/st2-setup-sudo.sh ]; then + /opt/stackstorm/st2/bin/st2-setup-sudo.sh || : + fi + + # Fix file permissions for st2 user + chown -R st2:st2packs /opt/stackstorm/packs 2>/dev/null || : + chown -R st2:st2packs /opt/stackstorm/virtualenvs 2>/dev/null || : + chown -R st2:st2 /var/log/st2 2>/dev/null || : + chown -R st2:st2 /etc/st2 2>/dev/null || : + chmod 2775 /opt/stackstorm/packs 2>/dev/null || : + chmod 2775 /opt/stackstorm/virtualenvs 2>/dev/null || : + # shellcheck disable=SC2086 systemd_enable_and_restart ${_ST2_SERVICES} ;; diff --git a/packaging/deb/systemd/st2actionrunner@.service b/packaging/deb/systemd/st2actionrunner@.service index f77a8707d4..fd7419f6e0 100644 --- a/packaging/deb/systemd/st2actionrunner@.service +++ b/packaging/deb/systemd/st2actionrunner@.service @@ -5,7 +5,7 @@ JoinsNamespaceOf=st2actionrunner.service [Service] Type=simple -User=root +User=st2 Group=st2packs UMask=002 Environment="DAEMON_ARGS=--config-file /etc/st2/st2.conf" diff --git a/packaging/rpm/scripts/post-install.sh b/packaging/rpm/scripts/post-install.sh index 0736e0db61..6529b72d60 100644 --- a/packaging/rpm/scripts/post-install.sh +++ b/packaging/rpm/scripts/post-install.sh @@ -68,6 +68,19 @@ for pack in ${_ST2_PACKS}; do done extract_st2_pack examples --target /usr/share/doc/st2/examples || : +# Setup sudo configuration based on security mode +if [ -x /opt/stackstorm/st2/bin/st2-setup-sudo.sh ]; then + /opt/stackstorm/st2/bin/st2-setup-sudo.sh || : +fi + +# Fix file permissions for st2 user +chown -R st2:st2packs /opt/stackstorm/packs 2>/dev/null || : +chown -R st2:st2packs /opt/stackstorm/virtualenvs 2>/dev/null || : +chown -R st2:st2 /var/log/st2 2>/dev/null || : +chown -R st2:st2 /etc/st2 2>/dev/null || : +chmod 2775 /opt/stackstorm/packs 2>/dev/null || : +chmod 2775 /opt/stackstorm/virtualenvs 2>/dev/null || : + # Native .rpm specs use macros that get expanded into shell snippets. # We are using nfpm, so we inline the macro expansion here. # %systemd_post diff --git a/packaging/rpm/systemd/st2actionrunner@.service b/packaging/rpm/systemd/st2actionrunner@.service index 2e1e971f41..a6411e2fd6 100644 --- a/packaging/rpm/systemd/st2actionrunner@.service +++ b/packaging/rpm/systemd/st2actionrunner@.service @@ -5,7 +5,7 @@ JoinsNamespaceOf=st2actionrunner.service [Service] Type=simple -User=root +User=st2 Group=st2packs UMask=002 Environment="DAEMON_ARGS=--config-file /etc/st2/st2.conf" diff --git a/st2common/st2common/config.py b/st2common/st2common/config.py index 28b0c062ec..00e071dfe7 100644 --- a/st2common/st2common/config.py +++ b/st2common/st2common/config.py @@ -89,6 +89,29 @@ def register_opts(ignore_errors=False): do_register_opts(system_user_opts, "system_user", ignore_errors) + system_security_opts = [ + cfg.StrOpt( + "security_mode", + default="legacy", + choices=["legacy", "restricted"], + help=( + "Security mode for action execution. " + "legacy: Full sudo access (backward compatible). " + "restricted: Limited to /opt/stackstorm paths only." + ), + ), + cfg.ListOpt( + "allowed_run_as_users", + default=["stanley", "root"], + help=( + "List of users that st2 service can run commands as. " + "Only applies in restricted mode." + ), + ), + ] + + do_register_opts(system_security_opts, "system_security", ignore_errors) + schema_opts = [ cfg.IntOpt("version", default=4, help="Version of JSON schema to use."), cfg.StrOpt( From b45fc20a8c0fe4fc42aae5437f2929138e1fa81c Mon Sep 17 00:00:00 2001 From: guzzijones12 Date: Fri, 28 Aug 2026 15:49:39 -0400 Subject: [PATCH 2/4] sudo settings --- packaging/deb/scripts/post-install.sh | 4 ++-- packaging/rpm/scripts/post-install.sh | 4 ++-- st2common/BUILD | 1 + st2common/bin/BUILD | 11 +++++++++-- .../st2-setup-sudo.sh => st2common/bin/st2-setup-sudo | 0 st2common/setup.py | 1 + 6 files changed, 15 insertions(+), 6 deletions(-) rename packaging/common/scripts/st2-setup-sudo.sh => st2common/bin/st2-setup-sudo (100%) diff --git a/packaging/deb/scripts/post-install.sh b/packaging/deb/scripts/post-install.sh index 7f91911364..b77f34396f 100644 --- a/packaging/deb/scripts/post-install.sh +++ b/packaging/deb/scripts/post-install.sh @@ -133,8 +133,8 @@ case "$1" in extract_st2_pack examples --target /usr/share/doc/st2/examples || : # Setup sudo configuration based on security mode - if [ -x /opt/stackstorm/st2/bin/st2-setup-sudo.sh ]; then - /opt/stackstorm/st2/bin/st2-setup-sudo.sh || : + if [ -x /opt/stackstorm/st2/bin/st2-setup-sudo ]; then + /opt/stackstorm/st2/bin/st2-setup-sudo || : fi # Fix file permissions for st2 user diff --git a/packaging/rpm/scripts/post-install.sh b/packaging/rpm/scripts/post-install.sh index 6529b72d60..e029faab0d 100644 --- a/packaging/rpm/scripts/post-install.sh +++ b/packaging/rpm/scripts/post-install.sh @@ -69,8 +69,8 @@ done extract_st2_pack examples --target /usr/share/doc/st2/examples || : # Setup sudo configuration based on security mode -if [ -x /opt/stackstorm/st2/bin/st2-setup-sudo.sh ]; then - /opt/stackstorm/st2/bin/st2-setup-sudo.sh || : +if [ -x /opt/stackstorm/st2/bin/st2-setup-sudo ]; then + /opt/stackstorm/st2/bin/st2-setup-sudo || : fi # Fix file permissions for st2 user diff --git a/st2common/BUILD b/st2common/BUILD index 48a0726dfd..a69b89303b 100644 --- a/st2common/BUILD +++ b/st2common/BUILD @@ -25,6 +25,7 @@ st2_component_python_distribution( "bin/st2-run-pack-tests:shell", "bin/st2ctl:shell", "bin/st2-self-check:shell", + "bin/st2-setup-sudo:shell", # dev scripts we might want to include # "bin/st2-generate-schemas", ], diff --git a/st2common/bin/BUILD b/st2common/bin/BUILD index f8626901e5..ccda1d02ba 100644 --- a/st2common/bin/BUILD +++ b/st2common/bin/BUILD @@ -1,5 +1,12 @@ python_sources( - sources=["*.py", "st2*", "!st2ctl", "!st2-self-check", "!st2-run-pack-tests"], + sources=[ + "*.py", + "st2*", + "!st2ctl", + "!st2-self-check", + "!st2-run-pack-tests", + "!st2-setup-sudo", + ], skip_flake8=True, # skip until resolved: https://github.com/PyCQA/pylint/issues/2095 skip_pylint=True, @@ -13,7 +20,7 @@ python_sources( st2_shell_sources_and_resources( name="shell", - sources=["st2ctl", "st2-self-check", "st2-run-pack-tests"], + sources=["st2ctl", "st2-self-check", "st2-run-pack-tests", "st2-setup-sudo"], skip_shellcheck=True, skip_shfmt=True, overrides={ diff --git a/packaging/common/scripts/st2-setup-sudo.sh b/st2common/bin/st2-setup-sudo similarity index 100% rename from packaging/common/scripts/st2-setup-sudo.sh rename to st2common/bin/st2-setup-sudo diff --git a/st2common/setup.py b/st2common/setup.py index 5e2764286d..13f0cf29a1 100644 --- a/st2common/setup.py +++ b/st2common/setup.py @@ -62,6 +62,7 @@ "bin/st2ctl", "bin/st2-generate-symmetric-crypto-key", "bin/st2-self-check", + "bin/st2-setup-sudo", "bin/st2-track-result", "bin/st2-validate-pack", "bin/st2-validate-pack-config", From 6280834c04f30d04e931eb5b671bae73ced98b69 Mon Sep 17 00:00:00 2001 From: guzzijones12 Date: Fri, 28 Aug 2026 16:11:39 -0400 Subject: [PATCH 3/4] add changelog --- CHANGELOG.rst | 70 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5719ee2d16..59ab6b7a8a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -36,6 +36,76 @@ Changed Added ~~~~~ * added raw_string type to allow template strings to pass through variable processing (by @guzzijones12@gmail.com) #6351 +* Optional unrooting of ``st2actionrunner`` and ``st2workflowengine`` with a configurable security mode + (by @guzzijones12@gmail.com). + + **What changed.** The ``st2actionrunner@`` and ``st2workflowengine`` systemd units now run as the + unprivileged ``st2`` user (group ``st2packs``) instead of ``root``. The non-templated + ``st2actionrunner.service`` remains a ``root`` ``oneshot`` because it only calls + ``systemctl start/stop st2actionrunner@N`` (via ``runners.sh``); it executes no action code. A new + ``[system_security]`` config section is added with two options: ``security_mode`` (``legacy`` or + ``restricted``, default ``legacy``) and ``allowed_run_as_users`` (default ``stanley,root``). + + **Does not break consensus / backward compatible.** The default is ``legacy``, which preserves the + historical behavior: broad ``NOPASSWD: ALL`` sudo for the ``st2`` user (to ``stanley`` and ``root``). + Existing installs and packs continue to work unchanged after upgrade. Unrooting and the tighter + ``restricted`` mode are strictly opt-in, so this is not a breaking consensus change -- operators who + want the hardening choose it, everyone else keeps today's behavior. + + **Two enforcement layers (they are NOT redundant).** + + 1. *Application-level pre-check* -- ``local_runner`` reads ``cfg.CONF.system_security`` at runtime on + every action. In ``restricted`` mode it rejects a target user not in ``allowed_run_as_users`` and a + script ``entry_point`` outside ``base_path``. This is enforced inside the ``st2`` process, so it is a + first-line convenience/early-failure guard with helpful errors -- NOT a boundary against a + compromised or buggy runner, and it does not path-restrict arbitrary local commands (``cmd=...``). + + 2. *OS-level sudoers* -- ``/etc/sudoers.d/st2`` is the real security boundary, enforced by ``sudo`` + (setuid-root), and it holds even if the ``st2`` process is compromised. It does two things the app + check cannot: (a) it *grants* the privilege in the first place -- once unrooted, the unprivileged + ``st2`` user can only ``sudo`` to ``stanley``/``root`` because this file allows it, so the file is + required for local ``sudo``/run-as-user actions to work at all; and (b) in ``restricted`` mode it is + the hard ceiling -- sudo execution is scoped to commands under ``/opt/stackstorm`` and dangerous + commands (``passwd``, ``su``, ``visudo``, ``sudo``) are denied, so a tricked runner still cannot + ``sudo`` arbitrary binaries as root. + + **How the sudoers file is generated.** A new ``st2-setup-sudo`` script (packaged into + ``/opt/stackstorm/st2/bin``) reads ``security_mode`` from ``st2.conf`` and writes ``/etc/sudoers.d/st2`` + accordingly, validating it with ``visudo -c`` (and removing the file on syntax error). It is invoked by + the package post-install as ``root``. + + **How to enable restricted mode.** + + 1. Set ``allowed_run_as_users`` to the exact set of users your actions actually run as. For most + deployments this is just the default system user, ``stanley``. Keep ``root`` in the list only if you + genuinely have actions that run as ``root``; dropping it tightens the surface. Example:: + + [system_security] + security_mode = restricted + allowed_run_as_users = stanley + + 2. Ensure any local script actions live under ``/opt/stackstorm`` (packs already install there), since + ``restricted`` mode only permits sudo execution of commands under that path. Actions that call + external scripts outside ``/opt/stackstorm`` must be moved into a pack or will be rejected. + 3. Regenerate the sudoers file as ``root`` so the OS-level boundary matches the new mode:: + + sudo /opt/stackstorm/st2/bin/st2-setup-sudo + + 4. Restart the st2 services (e.g. ``sudo st2ctl restart``) and exercise a representative action. If an + action fails with a "Security violation" error, add the missing user to ``allowed_run_as_users`` or + move the script under ``/opt/stackstorm`` -- do not fall back to ``legacy`` unless you must. + + A safe transition path is: upgrade on ``legacy`` (no behavior change), then flip a single node to + ``restricted``, validate your packs, and roll it out. + + **Containers/Kubernetes.** Because the sudoers file is generated at package post-install (image-build) + time, a container image that wants ``restricted`` mode must be built with an ``st2.conf`` that already + sets ``security_mode = restricted`` (so ``st2-setup-sudo`` writes the scoped sudoers into the image). +* Run ``st2actionrunner`` and ``st2workflowengine`` as the unprivileged ``st2`` user instead of ``root``, and + added an optional ``[system_security] security_mode`` (``legacy``/``restricted``) with ``allowed_run_as_users``. + In ``restricted`` mode the local runner limits execution to scripts under ``base_path`` and to allowed run-as + users. A new ``st2-setup-sudo`` script generates a scoped ``/etc/sudoers.d/st2`` from the configured mode and is + invoked from the package post-install. Defaults to ``legacy`` for backward compatibility. (by @guzzijones12@gmail.com) 3.9.0 - October 10, 2025 ------------------------ From f3f1a451914ec09d27905d3782bb9d462422b1be Mon Sep 17 00:00:00 2001 From: guzzijones12 Date: Thu, 27 Aug 2026 15:59:21 -0400 Subject: [PATCH 4/4] fix orquesta hash --- lockfiles/st2.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lockfiles/st2.lock b/lockfiles/st2.lock index 546f12698e..7a9f94672c 100644 --- a/lockfiles/st2.lock +++ b/lockfiles/st2.lock @@ -3085,7 +3085,7 @@ "artifacts": [ { "algorithm": "sha256", - "hash": "491767e81c1bb11a54fb68d1a24119bdeede593a2beccca5bc09bfed36fdb35c", + "hash": "b9feb1769b48102061fe4fc59b2f5ad600bc2ac0b55cf12ef5fe49464ac0d230", "url": "git+https://github.com/StackStorm/orquesta.git" } ],