policy_module(workbench, 2026.09.0)

########################################
#
# Feature Detection
#

# If building from source, comment or uncomment the defines below based on which
# interfaces your base policy has defined. The defaults included in the sample
# policy are based on the Red Hat Enterprise Linux 9 base policy.
#
# If your base policy is significantly customized away from the RHEL base policy
# or the SELinux Reference Policy, you may need to make additional changes in
# this file and/or in workbench.if to accommodate the customizations.

# These interfaces are unique to the RHEL policy.
define(`has_rootfs');
define(`has_hostnamed_config');
define(`has_userdom_admin');
define(`has_gconf');
define(`has_gnome_cache');
# define(`has_gnome_map_home_config_file');
define(`has_manage_usr');
define(`has_auth_read_passwd');
define(`has_read_proc');
define(`has_exec_systemctl');
define(`has_systemd_config_generic_services');
define(`has_init_reload_services');
define(`has_systemd_start_systemd_services');
# define(`has_read_resolved_runtime_files');
define(`has_refpolicy_syslog');
# define(`has_net_sysctl');
define(`has_map_generic_tmp');
# These interfaces were introduced to the RHEL policy in RHEL9.
# define(`has_jboss');
# define(`has_watch');
define(`has_watch_corecmd');
# define(`has_anon_inode_perms');

# These interfaces are available in the Debian policy.
# define(`has_xdg_config');
# define(`has_xdg_data');
# define(`has_search_sysctls');
# define(`has_systemd_journal');
# define(`has_use_passwd_agent');
# define(`has_map_user_tmp');
# define(`has_runtime');
# define(`has_selinux_status');
# define(`has_list_unit_dirs');
# define(`has_run_passwd_agent');
# define(`has_all_session_bus_client');
# define(`has_watch_shared_libs');
# define(`has_systemd_run_passwd_agent')

########################################
#
# Declarations
#

## <desc>
## <p>
## Allow Workbench sessions to connect to all TCP ports
## </p>
## </desc>
gen_bool(workbench_connect_all_ports, true)

## <desc>
## <p>
## Allow Workbench sessions to bind to any TCP port
## </p>
## </desc>
gen_bool(workbench_bind_all_ports, true)

## <desc>
## <p>
## Allow Chronicle to bind any unreserved TCP port instead of only the
## labeled OTLP port (default 4319/tcp, posit_chronicle_otlp_port_t).
## Enable this when the operator has reconfigured Chronicle to use a
## different port and prefers not to relabel via `semanage port`.
## </p>
## </desc>
gen_bool(posit_chronicle_bind_all_ports, false)

# External types imported into scope from other policies
require {
  type lib_t;
  type var_run_t;
  type var_lib_t;
  type var_log_t;
  type var_lock_t;
  type proc_t;
  type proc_net_t;
  type root_t;
  type semanage_t;
  type shell_exec_t;
  type sshd_t;
  type sysadm_t;
  type system_dbusd_t;
  type unconfined_t;
  role sysadm_r;
  role unconfined_r;
  class passwd rootok;
}

## workbench_domain
# This attribute is applied to runtime domains for Workbench processes:
# - workbench_t
# - workbench_worker_t
# - workbench_server_t
attribute workbench_domain;

## workbench_file
# This attribute is applied to all non-executable file types defined
# for Workbench:
# - workbench_file_t
# - workbench_lock_t
# - workbench_runtime_t
# - workbench_exec_t
# - workbench_server_exec_t
# - workbench_worker_exec_t
# - workbench_initrc_t
attribute workbench_file;

## workbench_server_t
# This domain type is for the main services. These processes are
# launched as daemons and have the broadest permissions.
## workbench_server_exec_t
# Executing a file of this type can transition a process into the
# workbench_server_t domain.
workbench_domain_type(workbench_server)

## workbench_t
# This domain type is for sessions launched by the main services.
# These processes are the middleman between workbench_server_t and
# workbench_worker_t.
## workbench_exec_t
# Executing a file of this type can transition a process into the
# workbench_t domain.
workbench_domain_type(workbench)

## workbench_worker_t
# This domain type is intended to be the most restrictive context,
# where code can run as an unprivileged user. Because it has fewer
# permissions, it can be allowed to run generated code out of RAM.
# However, it is currently broader than intended: The domain
# transitions into workbench_worker_t before changing users and
# dropping permissions.
## workbench_worker_exec_t
# Executing a file of this type can transition a process into the
# workbench_t domain.
workbench_domain_type(workbench_worker)

## workbench_session_exec_t
# Executing a file of this type can transition a process into the
# unconfined_t domain. Only Workbench domains have this permission.
workbench_file_type(workbench_session_exec_t)
corecmd_executable_file(workbench_session_exec_t)

## workbench_initrc_t
# A file type that is the primary entrypoint into the Workbench domains.
# Only these files can be directly run by init or from a shell prompt.
# The created process transitions into the workbench_server_t domain.
workbench_shared_file_type(workbench_initrc_t)
files_type(workbench_initrc_t)

## workbench_file_t
# A general-purpose file type that all Workbench domains have
# permission to manage.
workbench_shared_file_type(workbench_file_t)
files_type(workbench_file_t)

## workbench_runtime_t
# Runtime files such as sockets and pid files that can only be
# managed by workbench_server_t but can be accessed by other
# Workbench domains as well as init.
workbench_file_type(workbench_runtime_t)
ifdef(`has_runtime',
  `files_runtime_file(workbench_runtime_t)',
  `files_pid_file(workbench_runtime_t)'
)

## workbench_lock_t
# Lock files that can only be managed by workbench_server_t,
# but init is permitted to clean them up if necessary.
workbench_file_type(workbench_lock_t)
files_lock_file(workbench_lock_t)

## posit_chronicle_t
# This domain supervises other Workbench domains.
## posit_chronicle_exec_t
# Executing a file of this type can transition a process into the
# posit_chronicle_t domain.
workbench_domain_type(posit_chronicle)

## posit_chronicle_var_lib_t
# Chronicle's persistent data directory under
# <server-shared-storage-path>/chronicle. Chronicle manages this tree;
# no other Workbench domain reads or writes it.
type posit_chronicle_var_lib_t;
files_type(posit_chronicle_var_lib_t)

## posit_chronicle_otlp_port_t
# Port type for Chronicle's OTLP receiver. The number-to-type mapping
# (default 4319/tcp) is wired by the package postinst via `semanage port -a`
# so operators can change it with `semanage port -m` without a policy rebuild.
type posit_chronicle_otlp_port_t;
corenet_port(posit_chronicle_otlp_port_t)

########################################
#
# local policies
#

# Apply common features to domains
workbench_common_read(workbench_t)
workbench_common(workbench_server_t)
workbench_named_filetrans(unconfined_t)

# The rstudio-server script is an entry point; allow init and unconstrained users to use it
domain_entry_file(workbench_server_t, workbench_initrc_t)
workbench_server_domtrans_from(init_t)
workbench_domtrans_from(unconfined_t)
unconfined_domtrans_to(workbench_server_t, workbench_initrc_t)
role_transition unconfined_r workbench_file system_r;

# Allow unconstrained users to run worker processes
unconfined_domtrans_to(workbench_worker_t, workbench_worker_exec_t)

# Apply a more restrictive set to worker processes but let them JIT
workbench_common_read(workbench_worker_t)
workbench_user_home(workbench_worker_t)
ifdef(`has_gconf',`gnome_manage_cache_home_dir(workbench_worker_t)')
ifdef(`has_gnome_cache',`gnome_map_generic_cache_files(workbench_worker_t)')
workbench_map_home_config_files(workbench_worker_t)
allow workbench_worker_t self:capability { chown setgid fowner fsetid dac_override dac_read_search net_admin }; # TODO: how can we avoid needing this in the worker?
allow workbench_worker_t self:process { execmem getcap getsched setsched signal getpgid setpgid };
allow workbench_worker_t workbench_domain:process { sigkill signal };
allow workbench_worker_t workbench_file_t:file { mmap_exec_file_perms execute_no_trans };
allow workbench_worker_t workbench_worker_exec_t:file { execute execute_no_trans getattr open read map unlink };
ifdef(`has_watch_corecmd',`corecmd_watch_bin_dirs(workbench_worker_t)')
ifdef(`has_anon_inode_perms',`allow workbench_worker_t workbench_worker_t:anon_inode { create getattr ioctl read write map };')
term_create_pty(workbench_worker_t, user_devpts_t)
term_use_unallocated_ttys(workbench_worker_t)
files_manage_usr_files(workbench_worker_t) # too broad, do not like this

# shells run by workbench_t processes run as worker
corecmd_shell_domtrans(workbench_t, workbench_worker_t)
# allow worker processes to open unconstrained shells / rsession
corecmd_shell_domtrans(workbench_worker_t, unconfined_t)

# sessions can fork and manage signals
allow workbench_t self:process { fork signal_perms };

# server can drop perms, nice itself, kill workers, and invoke systemctl
allow workbench_server_t self:capability { sys_admin net_admin };
selinux_get_fs_mount(workbench_server_t)
seutil_read_config(workbench_server_t)
ifdef(`has_selinux_status',`selinux_use_status_page(workbench_domain)')
allow workbench_server_t self:process { setcap setsched };
allow workbench_server_t workbench_domain:capability { kill sys_ptrace sys_resource };
allow workbench_server_t workbench_domain:process { sigkill signal };
init_stream_connect(workbench_server_t)
init_dbus_chat(workbench_server_t)
optional_policy(`
  require {
    type systemd_unit_t;
    class service all_service_perms;
  }
  allow workbench_server_t systemd_unit_t:service { all_service_perms };
')
# XXX: this is overly permissive and we should research alternatives (D-BUS?)
ifdef(`has_exec_systemctl',`
  systemd_exec_systemctl(workbench_server_t)
  ifdef(`has_systemd_config_generic_services',`systemd_config_generic_services(workbench_server_t)')
  ifdef(`has_init_reload_services',`init_reload_services(workbench_server_t)')
  optional_policy(`
    gen_require(`
      type systemd_systemctl_exec_t;
    ')
    domtrans_pattern(workbench_server_t, systemd_systemctl_exec_t, sysadm_t)
  ')
',`
  ifdef(`has_list_unit_dirs',`
    init_list_unit_dirs(workbench_server_t)
  ',`
    init_search_units(workbench_server_t)
  ')
')
# XXX: unclear why the two agent interfaces aren't sufficient
ifdef(`has_systemd_run_passwd_agent',`
  systemd_run_passwd_agent(workbench_server_t, system_r)
  systemd_use_passwd_agent(workbench_server_t)
  allow workbench_server_t systemd_passwd_agent_t:dir search;
  allow workbench_server_t systemd_passwd_agent_t:file { getattr open read };
  allow workbench_server_t systemd_passwd_agent_t:process signal;
')

# server can replace worker binaries
allow workbench_server_t workbench_worker_exec_t:dir manage_dir_perms;
allow workbench_server_t workbench_worker_exec_t:file manage_file_perms;

# server can manage link files for other domains
allow workbench_server_t workbench_domain:lnk_file { manage_lnk_file_perms };

# server can manage cgroups and resource limits
fs_manage_cgroup_dirs(workbench_server_t)
fs_manage_cgroup_files(workbench_server_t)
allow workbench_server_t workbench_domain:process { setrlimit };

# worker can inherit rlimits
allow workbench_worker_t workbench_worker_t:process { rlimitinh };

# sessions and worker can talk to init; init can launch server processes
workbench_sock_file(workbench_t, init_t)
workbench_sock_file(workbench_worker_t, init_t)
kernel_stream_connect(workbench_worker_t)
ifdef(`has_systemd_start_systemd_services',`
  systemd_start_systemd_services(workbench_server_t)
')

# if enabled, worker can connect to arbitrary TCP ports
if ( workbench_connect_all_ports ) {
  allow workbench_worker_t self:tcp_socket create_stream_socket_perms;
  corenet_tcp_sendrecv_generic_if(workbench_worker_t)
  corenet_tcp_sendrecv_generic_node(workbench_worker_t)
  corenet_tcp_sendrecv_all_ports(workbench_worker_t)
  corenet_tcp_connect_all_ports(workbench_worker_t)
  # corenet_all_recvfrom_unlabeled(workbench_worker_t)
}

# if enabled, worker can connect to arbitrary TCP ports
if ( workbench_bind_all_ports ) {
  allow workbench_worker_t self:tcp_socket create_stream_socket_perms;
  corenet_tcp_sendrecv_generic_if(workbench_worker_t)
  corenet_tcp_sendrecv_generic_node(workbench_worker_t)
  corenet_tcp_sendrecv_all_ports(workbench_worker_t)
  corenet_tcp_bind_all_ports(workbench_worker_t)
  # corenet_all_recvfrom_unlabeled(workbench_worker_t)
}

# server (and workers) can validate passwords (TODO: delegate so workers do not need this)
ifdef(`has_auth_read_passwd',`
  auth_read_passwd(workbench_server_t)
  auth_read_passwd(workbench_worker_t)
')
auth_read_shadow(workbench_server_t)
auth_read_shadow(workbench_worker_t)
auth_domtrans_chk_passwd(workbench_server_t)
auth_domtrans_chk_passwd(workbench_worker_t)
ifdef(`has_use_passwd_agent',`systemd_use_passwd_agent(workbench_server_t)')
ifdef(`has_run_passwd_agent',`systemd_run_passwd_agent(workbench_server_t, sysadm_r)')

# both server and session can signal unconfined (i.e., user) processes
allow workbench_server_t unconfined_t:process { signal signull sigkill };
allow workbench_worker_t unconfined_t:process { signal signull sigkill };

# server can modify files in /usr and /var
files_manage_usr_files(workbench_server_t)
ifdef(`has_manage_usr',
  `files_manage_usr_dirs(workbench_server_t)',
  `files_rw_usr_dirs(workbench_server_t)'
)
ifdef(`has_runtime',`
  files_runtime_filetrans(workbench_server_t, workbench_runtime_t, file, "rserver-http.pid")
  files_runtime_filetrans(workbench_server_t, workbench_runtime_t, file, "rstudio-server.pid")
',`
  files_pid_filetrans(workbench_server_t, workbench_runtime_t, file, "rserver-http.pid")
  files_pid_filetrans(workbench_server_t, workbench_runtime_t, file, "rstudio-server.pid")
')
allow workbench_server_t workbench_runtime_t:file manage_file_perms;
allow workbench_server_t workbench_runtime_t:dir manage_dir_perms;
allow workbench_server_t workbench_lock_t:file manage_file_perms;
allow workbench_server_t workbench_lock_t:dir manage_dir_perms;
allow workbench_server_t { var_run_t var_lock_t }:dir { setattr }; # TODO: refine filetrans to avoid this need
allow workbench_server_t root_t:dir { create };

# server can bind port HTTP/debug ports and look at networking info
allow workbench_server_t self:capability { net_admin };
corenet_tcp_bind_http_port(workbench_server_t)
ifdef(`has_jboss',`corenet_tcp_bind_jboss_debug_port(workbench_server_t)')
allow workbench_server_t proc_net_t:{ file lnk_file } { open read getattr };

# server can use the audit log (should it also be workbench_t?)
logging_send_audit_msgs(workbench_server_t)
ifdef(`has_refpolicy_syslog',`
  logging_systemctl_audit(workbench_server_t)
  logging_systemctl_syslogd(workbench_server_t)
')

# allow system-level tools to manage Workbench files/sockets
allow unconfined_t workbench_session_exec_t:file { entrypoint };
allow unconfined_t workbench_initrc_t:file { execute execute_no_trans };
allow unconfined_t workbench_initrc_t:lnk_file { read };
allow unconfined_t workbench_file:sock_file { create setattr };
allow semanage_t workbench_file:dir search_dir_perms;
allow system_dbusd_t workbench_file:dir search_dir_perms;
allow system_dbusd_t workbench_file:file read_file_perms;
dbus_system_bus_client(workbench_t)
dbus_system_bus_client(workbench_worker_t)

# sshd needs to be able to read admin/user tokens owned by workbench
# workbench needs to be able to connect over ssh-forwarded sockets
allow sshd_t { workbench_file_t workbench_exec_t }:file { open read getattr map execute };
allow sshd_t workbench_file_t:dir search_dir_perms;
allow workbench_domain sshd_t:fifo_file { rw_fifo_file_perms };

# In RHEL8 the netlink_selinux_socket permission is required.
# It is not clear why.
allow workbench_server_t self:netlink_selinux_socket { create bind };

# worker wants to be able to map memory using tmpfs
allow workbench_worker_t tmpfs_t:file mmap_rw_file_perms;

# worker invokes publisher, which uses session dbus
ifdef(`has_all_session_bus_client',`
  dbus_all_session_bus_client(workbench_worker_t)
',`
  dbus_session_bus_client(workbench_worker_t)
')

# worker watches for filesystem changes
ifdef(`has_watch',`
  corecmd_watch_bin_dirs(workbench_worker_t)
  files_watch_usr_dirs(workbench_worker_t)
  ifdef(`has_watch_shared_libs',`
    libs_watch_shared_lib_dirs(workbench_worker_t)
  ',`
    libs_watch_lib_dirs(workbench_worker_t)
  ')
  optional_policy(`
    require {
      type locale_t;
    }
    allow workbench_worker_t locale_t:dir watch;
  ')
')

# allow dpkg to update file contexts
optional_policy(`
  require {
    type dpkg_t;
    type dpkg_script_t;
  }
  allow dpkg_t workbench_file:fifo_file { manage_fifo_file_perms relabelfrom relabelto };
  allow dpkg_t workbench_file:sock_file { manage_sock_file_perms relabelfrom relabelto };
  allow dpkg_t workbench_file:lnk_file { manage_lnk_file_perms relabelfrom relabelto };
  allow dpkg_t workbench_file:file { manage_file_perms relabelfrom relabelto };
  allow dpkg_t workbench_file:dir { manage_dir_perms relabelfrom relabelto };
  allow dpkg_script_t workbench_initrc_t:file { entrypoint };
  workbench_server_domtrans_from(dpkg_script_t)
')

# allow rpm to start the server
optional_policy(`
  require {
    type rpm_script_t;
  }
  workbench_server_domtrans_from(rpm_script_t)
')

# Posit Chronicle policy
#
# rserver (workbench_server_t) execs posit-chronicle and the resulting
# process transitions into posit_chronicle_t. Chronicle reads its generated
# config under workbench_file_t paths, owns its data dir under
# posit_chronicle_var_lib_t, binds an OTLP receiver on 127.0.0.1:4319,
# scrapes Workbench's metrics + admin API over loopback, and inherits its
# log fd from rserver. workbench_file_t access (search/read/manage on dirs,
# files, and sock_files) flows from posit_chronicle_t carrying the
# workbench_domain attribute and workbench_file_t being declared via
# workbench_shared_file_type (see workbench.if); no explicit rules needed.
domtrans_pattern(workbench_server_t, posit_chronicle_exec_t, posit_chronicle_t)
allow posit_chronicle_t posit_chronicle_var_lib_t:file manage_file_perms;
allow posit_chronicle_t posit_chronicle_var_lib_t:dir manage_dir_perms;

# OTLP receiver bind. The number-to-type mapping for posit_chronicle_otlp_port_t
# (default 4319/tcp) is set by the package postinst via `semanage port -a`;
# operators who relocate Chronicle to a different port can either relabel
# (`semanage port -m -t posit_chronicle_otlp_port_t -p tcp <port>`) or flip
# the posit_chronicle_bind_all_ports boolean for a broader fallback.
allow posit_chronicle_t self:tcp_socket create_stream_socket_perms;
corenet_tcp_bind_generic_node(posit_chronicle_t)
allow posit_chronicle_t posit_chronicle_otlp_port_t:tcp_socket name_bind;
if ( posit_chronicle_bind_all_ports ) {
  corenet_tcp_bind_all_unreserved_ports(posit_chronicle_t)
}

# Outbound TCP connects (loopback only in practice, but the rules are
# port-type-scoped — they don't constrain destination address). Covers
# Workbench's admin API (typically http_port_t) and the metrics port +
# Chronicle's own ephemeral gRPC back-connect (both unreserved).
# all_unreserved_ports is the portable replacement for the RHEL-only
# corenet_tcp_connect_ephemeral_ports and is tighter than _all_ports.
corenet_tcp_connect_http_port(posit_chronicle_t)
corenet_tcp_connect_all_unreserved_ports(posit_chronicle_t)

allow posit_chronicle_t self:udp_socket { connect create getattr read setopt write };
auth_use_nsswitch(posit_chronicle_t)
sysnet_read_config(posit_chronicle_t)
# Prefer the semantic interface where available (RHEL, trixie); fall back to
# a guarded raw allow on bookworm where the interface is absent.
ifdef(`has_read_resolved_runtime_files',`
  systemd_read_resolved_runtime_files(posit_chronicle_t)
',`
  optional_policy(`
    gen_require(`
      type systemd_resolved_runtime_t;
    ')
    allow posit_chronicle_t systemd_resolved_runtime_t:dir search;
    allow posit_chronicle_t systemd_resolved_runtime_t:file { getattr open read };
  ')
')

# Go runtime introspection: /sys, cgroup quotas, /proc, /proc/1/environ.
# kernel_read_proc_files is absent on both Debians; kernel_read_system_state
# covers the Go runtime's /proc reads (cpuinfo, meminfo, modules, …) without
# touching /proc/<pid>/* — init_read_state handles /proc/1/environ.
dev_read_sysfs(posit_chronicle_t)
fs_read_cgroup_files(posit_chronicle_t)
fs_search_cgroup_dirs(posit_chronicle_t)
kernel_read_net_sysctls(posit_chronicle_t)
ifdef(`has_read_proc',`
  kernel_read_proc_files(posit_chronicle_t)
',`
  kernel_read_system_state(posit_chronicle_t)
')
init_read_state(posit_chronicle_t)

# Management UDS (server.sock / pwb-chronicle.sock) under /var/lib/rstudio-server.
workbench_sock_file(posit_chronicle_t, workbench_file_t)

# Debian execs lsb_release via a shell for OS detection; RHEL resolves the
# same lookup in-process. Granting unconditionally is forward-safe.
corecmd_exec_bin(posit_chronicle_t)
corecmd_exec_shell(posit_chronicle_t)
allow posit_chronicle_t self:capability setgid;
allow posit_chronicle_t self:process { getsched signal signull };
allow posit_chronicle_t self:fifo_file rw_fifo_file_perms;
# miscfiles_read_locale is absent on both Debians; use miscfiles_read_localization.
miscfiles_read_localization(posit_chronicle_t)
files_read_etc_files(posit_chronicle_t)
files_read_etc_runtime_files(posit_chronicle_t)

# Silence audit messages about things we do not need permission for
dontaudit workbench_server_t { daemon -workbench_domain }:file { getattr open };
dontaudit workbench_server_t { daemon -workbench_domain }:dir { search };
dontaudit workbench_server_t workbench_domain:process { noatsecure }; # do not allow environment sanitation to be disabled
dontaudit workbench_domain domain:process { noatsecure rlimitinh siginh };
dontaudit workbench_domain domain:dir { getattr search };
dontaudit workbench_domain domain:file { getattr open read };

optional_policy(`
  require {
    type locale_t;
  }
  # The Python zoneinfo package gets mislabeled in RHEL, do not complain
  dontaudit workbench_domain locale_t:file { create rename write };
  dontaudit workbench_domain locale_t:dir { rename add_name remove_name };
')
