summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniil Baturin <daniil@baturin.org>2025-08-21 13:46:01 +0100
committerDaniil Baturin <daniil@baturin.org>2025-08-28 13:30:51 +0100
commitdd7ca05d33174c5fe2e35c5d0b7610973a02039c (patch)
tree7c1068ac1ac166342352e8214c2e1e5400993b14 /src
parent9285b34718203a62a9620c34b0bc50ba85d839d6 (diff)
downloadvyos-1x-dd7ca05d33174c5fe2e35c5d0b7610973a02039c.tar.gz
vyos-1x-dd7ca05d33174c5fe2e35c5d0b7610973a02039c.zip
vyos-op-run: T7745: add a command permission checking mechanism
Diffstat (limited to 'src')
-rw-r--r--src/vyos_op_run.ml212
1 files changed, 197 insertions, 15 deletions
diff --git a/src/vyos_op_run.ml b/src/vyos_op_run.ml
index 0a8f35134..0767ba298 100644
--- a/src/vyos_op_run.ml
+++ b/src/vyos_op_run.ml
@@ -18,6 +18,25 @@
(* Global constants *)
let op_def_file = "/usr/share/vyos/op_cache.json"
+let permissions_file = "/etc/vyos/operators.json"
+let vyos_admin_group_name = "vyattacfg"
+
+(* List of commands that operators are unconditionally denied to execute. *)
+let admin_only_commands = [
+ (* Configuration mode operations *)
+ ["configure"];
+ ["commit"];
+ ["commit-confirm"];
+ ["confirm"];
+ (* XXX: executing a shell through a wrapper that does setuid 0
+ provides a ready shell escape and defeats the purpose.
+ We cannot allow operator-level users to execute shells
+ in VRFs and network namespaces
+ at least until we find a way to drop privileges
+ after attaching to the VRF/netns but before executing the commands.
+ *)
+ ["execute"; "shell"];
+]
(* Execution options *)
type options = {
@@ -50,6 +69,9 @@ let internal_error msg = raise (Internal_error msg)
exception Command_error of string
let command_error msg = raise (Command_error msg)
+exception Permission_error
+let permission_error () = raise Permission_error
+
exception Incomplete_command
(* Logging setup routines *)
@@ -83,6 +105,12 @@ let read_command_definitions () =
let () = close_in ic in
data
+let read_permissions () =
+ let ic = open_in permissions_file in
+ let data = Yojson.Safe.from_channel ic in
+ let () = close_in ic in
+ data
+
let find_child_node op_node word =
let open Yojson.Safe.Util in
let res = member word op_node in
@@ -137,6 +165,148 @@ let get_virtual_tag_node node =
| `Null -> None
| _ -> Some res
+(* Command permission checks *)
+
+let rec permission_matches perm cmd =
+ match perm, cmd with
+ | [], _ ->
+ (* If all terms of the permission matched
+ all words of the command, the command is allowed --
+ we follow the implicit approach
+ "every permission includes all sub-commands"
+ *)
+ true
+ | _, [] ->
+ (* If the command is shorter than the permission spec,
+ it means the permission is more specific.
+ E.g., 'show interfaces ethernet' permission
+ should reject attempts to run 'show interfaces',
+ since its intent is to allow access only to Ethernet. *)
+ false
+ | (p :: ps), (c :: cs) ->
+ (* Permission term can be either a command word
+ or a special token '*' that matches any command. *)
+ if (p = c) || (p = "*") then permission_matches ps cs
+ else false
+
+let group_perms_match perms group cmd =
+ let get_group_perms perms g =
+ let perms = Yojson.Safe.Util.path
+ ["groups"; g; "command_policy"; "allow"] perms
+ in
+ match perms with
+ | Some v ->
+ (try
+ v |>
+ Yojson.Safe.Util.to_list |>
+ List.map (fun j -> Yojson.Safe.Util.to_list j |> List.map Yojson.Safe.Util.to_string)
+ with _ ->
+ Printf.ksprintf internal_error
+ "Command policy for group %s is not a list of string lists" g)
+ | None -> Printf.ksprintf internal_error
+ "Configuration does not define command policy for group %s" g
+ in
+ let rec perm_list_matches ps cmd =
+ match ps with
+ | [] -> false
+ | p :: ps ->
+ if permission_matches p cmd then true
+ else perm_list_matches ps cmd
+ in
+ let group_perms = get_group_perms perms group in
+ perm_list_matches group_perms cmd
+
+let is_admin () =
+ let admin_group = Unix.getgrnam vyos_admin_group_name in
+ let user_groups = Unix.getgroups () in
+ match (Array.find_opt ((=) admin_group.gr_gid) user_groups) with
+ | Some _ -> true
+ | None -> false
+
+let has_unsafe_characters cmd =
+ (* XXX: this function is highly restrictive now,
+ until we are completely certain that shell escape
+ cannot happen down the line inside VyOS op mode scripts.
+ Alphanumeric characters, hyphens, dots, and whitespace
+ should allow operator users to use most commands
+ that take interface names, FQDNs, and config entities
+ like IPsec peer names.
+ Notable exceptions are:
+ - 'show bgp regexp': regexes naturally require '$' and other
+ patently shell-unsafe characters.
+ - 'monitor traffic interface eth0 filter':
+ PCAP filters use '!', '&&' and '||',
+ although people can use 'and', 'or', 'not'
+ to get around the restriction.
+ - 'add system image': requires non-alphanumeric characters
+ for URLs.
+ *)
+ try
+ let _ = Pcre2.exec ~pat:{|[^a-zA-Z0-9_\-\.\s]|} cmd in
+ let () =
+ Printf.fprintf stderr "Command [%s] contains special characters \
+ that operator-level users are not allowed to use\n" cmd
+ in
+ true
+ with Not_found -> false
+
+let is_admin_only_command cmd =
+ let rec prefix_matches prefix target =
+ match prefix, target with
+ | [], _ ->
+ (* The target matched every word of the prefix,
+ so it's a match.
+ *)
+ true
+ | _, [] ->
+ (* The target is shorter than the prefix,
+ so it's not a match.
+ *)
+ false
+ | (p :: ps), (t :: ts) ->
+ if p = t then prefix_matches ps ts
+ else false
+ in
+ let res = List.find_opt (fun p -> prefix_matches p cmd) admin_only_commands in
+ match res with
+ | None -> false
+ | Some _ -> true
+
+let check_command_permissions perms cmd =
+ let rec aux perms groups cmd =
+ match groups with
+ | [] -> permission_error ()
+ | g :: gs ->
+ if group_perms_match perms g cmd then ()
+ else aux perms gs cmd
+ in
+ (* VyOS admins can execute any commands without restrictions *)
+ if is_admin () then () else
+ (* Operators are not allowed to execute commands
+ with potentially unsafe characters in them *)
+ if has_unsafe_characters (String.concat " " cmd) then permission_error () else
+ (* Some commands are unconditionally denied to operators *)
+ if is_admin_only_command cmd then permission_error () else
+ (* Operator level users must always be in groups
+ with defined command policies
+ *)
+ let username = Unix.getlogin () in
+ let groups = Yojson.Safe.Util.path ["users"; username] perms in
+ match groups with
+ | None | Some (`List []) ->
+ Printf.ksprintf internal_error "User %s is not assigned to any operator group" username
+ | Some gs ->
+ let group_list =
+ (try
+ gs |>
+ Yojson.Safe.Util.to_list |>
+ List.map Yojson.Safe.Util.to_string
+ with _ ->
+ Printf.ksprintf internal_error "The groups field for user %s is not a list of strings"
+ username)
+ in
+ aux perms group_list cmd
+
(* Command rendering and execution *)
let render_command opts env command_tmpl =
let () = Logs.debug @@ fun m -> m "Command template: %s" command_tmpl in
@@ -145,17 +315,22 @@ let render_command opts env command_tmpl =
let vyos_command = opts.vyos_command in
Pcre2.replace ~pat:{|\$[@*]|} ~templ:vyos_command command
-let run_command opts env command_tmpl =
+let run_external_command opts env command_tmpl =
let cmd = render_command opts env command_tmpl in
if opts.dry_run then Printf.printf "%s\n%!" cmd else
let () = Logs.debug @@ fun m -> m "Command to be executed %s" cmd in
let res = Unix.system cmd in
match res with
| Unix.WEXITED 0 -> ()
- | _ -> Printf.ksprintf command_error "Execution of command '%s' failed" cmd
+ | _ ->
+ (* Many op mode commands return non-zero exit codes on benign errors
+ such as an unconfigured subsystem,
+ so we shouldn't show this to the user by default.
+ *)
+ Logs.debug @@ fun m -> m "Execution of command '%s' failed" cmd
(* Command lookup *)
-let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
+let rec run_vyos_command opts ?(env=[]) ?(parent="") node cmd_words =
match cmd_words with
| w :: ws ->
let () = Logs.debug @@ fun m -> m "Looking up node '%s'" w in
@@ -163,7 +338,7 @@ let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
begin match res with
| Some child_node ->
(* It's a normal, fixed command word *)
- find_command opts ~env:env ~parent:w child_node ws
+ run_vyos_command opts ~env:env ~parent:w child_node ws
| None ->
(* It's either an argument of a tag node
or an incorrect command word *)
@@ -177,9 +352,9 @@ let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
begin match ws with
| [] ->
let command = get_command node_data in
- run_command opts env command
+ run_external_command opts env command
| _ as ws ->
- find_command opts ~env:env ~parent:w node ws
+ run_vyos_command opts ~env:env ~parent:w node ws
end
| "node", Some vtn ->
(* It's a command that can be used either by itself or with an argument. *)
@@ -188,12 +363,12 @@ let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
| [] ->
let vtn_data = get_node_data vtn in
let command = get_command vtn_data in
- run_command opts env command
+ run_external_command opts env command
| _ ->
(* In the case of a virtual tag node, we take the parent (for variable substitution purposes)
from the upper level.
*)
- find_command opts ~env:env ~parent:parent vtn ws
+ run_vyos_command opts ~env:env ~parent:parent vtn ws
end
| "node", None | "leafNode", None ->
let path = get_path node_data in
@@ -223,7 +398,7 @@ let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
in
begin match command with
| Some command ->
- run_command opts env command
+ run_external_command opts env command
| None ->
raise Incomplete_command
end
@@ -276,19 +451,26 @@ let () =
let debug = if debug then true else options.debug in
let () = setup_logging debug in
let op_defs = read_command_definitions () in
+ let permissions = read_permissions () in
let () = Unix.setuid 0 in
+ let () = Logs.debug @@ fun m -> m "Executing VyOS command [%s]" (String.concat " " args) in
try
- find_command options ~env:[] ~parent:"" op_defs args
- with
+ check_command_permissions permissions args;
+ run_vyos_command options ~env:[] ~parent:"" op_defs args
+ with
+ | Permission_error ->
+ Printf.fprintf stderr "You do not have a permission to execute VyOS command [%s]\n"
+ options.vyos_command;
+ exit 1
| Invalid_command msg ->
- Printf.fprintf stderr "Invalid command [%s]: %s" options.vyos_command msg;
+ Printf.fprintf stderr "Invalid command [%s]: %s\n" options.vyos_command msg;
exit 1
| Command_error msg ->
- Printf.fprintf stderr "%s" msg;
+ Printf.fprintf stderr "%s\n" msg;
| Incomplete_command ->
- Printf.fprintf stderr "Incomplete command: %s" options.vyos_command;
+ Printf.fprintf stderr "Incomplete command: %s\n" options.vyos_command;
exit 2
| Internal_error msg ->
- Printf.fprintf stderr "Internal error: %s" msg;
+ Printf.fprintf stderr "Internal error: %s\n" msg;
exit 255