1818
1919(* Global constants *)
2020let op_def_file = " /usr/share/vyos/op_cache.json"
21+ let permissions_file = " /etc/vyos/operators.json"
22+ let vyos_admin_group_name = " vyattacfg"
23+
24+ (* List of commands that operators are unconditionally denied to execute. *)
25+ let admin_only_commands = [
26+ (* Configuration mode operations *)
27+ [" configure" ];
28+ [" commit" ];
29+ [" commit-confirm" ];
30+ [" confirm" ];
31+ (* XXX: executing a shell through a wrapper that does setuid 0
32+ provides a ready shell escape and defeats the purpose.
33+ We cannot allow operator-level users to execute shells
34+ in VRFs and network namespaces
35+ at least until we find a way to drop privileges
36+ after attaching to the VRF/netns but before executing the commands.
37+ *)
38+ [" execute" ; " shell" ];
39+ ]
2140
2241(* Execution options *)
2342type options = {
@@ -50,6 +69,9 @@ let internal_error msg = raise (Internal_error msg)
5069exception Command_error of string
5170let command_error msg = raise (Command_error msg)
5271
72+ exception Permission_error
73+ let permission_error () = raise Permission_error
74+
5375exception Incomplete_command
5476
5577(* Logging setup routines *)
@@ -83,6 +105,12 @@ let read_command_definitions () =
83105 let () = close_in ic in
84106 data
85107
108+ let read_permissions () =
109+ let ic = open_in permissions_file in
110+ let data = Yojson.Safe. from_channel ic in
111+ let () = close_in ic in
112+ data
113+
86114let find_child_node op_node word =
87115 let open Yojson.Safe.Util in
88116 let res = member word op_node in
@@ -137,6 +165,148 @@ let get_virtual_tag_node node =
137165 | `Null -> None
138166 | _ -> Some res
139167
168+ (* Command permission checks *)
169+
170+ let rec permission_matches perm cmd =
171+ match perm, cmd with
172+ | [] , _ ->
173+ (* If all terms of the permission matched
174+ all words of the command, the command is allowed --
175+ we follow the implicit approach
176+ "every permission includes all sub-commands"
177+ *)
178+ true
179+ | _ , [] ->
180+ (* If the command is shorter than the permission spec,
181+ it means the permission is more specific.
182+ E.g., 'show interfaces ethernet' permission
183+ should reject attempts to run 'show interfaces',
184+ since its intent is to allow access only to Ethernet. *)
185+ false
186+ | (p :: ps ), (c :: cs ) ->
187+ (* Permission term can be either a command word
188+ or a special token '*' that matches any command. *)
189+ if (p = c) || (p = " *" ) then permission_matches ps cs
190+ else false
191+
192+ let group_perms_match perms group cmd =
193+ let get_group_perms perms g =
194+ let perms = Yojson.Safe.Util. path
195+ [" groups" ; g; " command_policy" ; " allow" ] perms
196+ in
197+ match perms with
198+ | Some v ->
199+ (try
200+ v |>
201+ Yojson.Safe.Util. to_list |>
202+ List. map (fun j -> Yojson.Safe.Util. to_list j |> List. map Yojson.Safe.Util. to_string)
203+ with _ ->
204+ Printf. ksprintf internal_error
205+ " Command policy for group %s is not a list of string lists" g)
206+ | None -> Printf. ksprintf internal_error
207+ " Configuration does not define command policy for group %s" g
208+ in
209+ let rec perm_list_matches ps cmd =
210+ match ps with
211+ | [] -> false
212+ | p :: ps ->
213+ if permission_matches p cmd then true
214+ else perm_list_matches ps cmd
215+ in
216+ let group_perms = get_group_perms perms group in
217+ perm_list_matches group_perms cmd
218+
219+ let is_admin () =
220+ let admin_group = Unix. getgrnam vyos_admin_group_name in
221+ let user_groups = Unix. getgroups () in
222+ match (Array. find_opt ((= ) admin_group.gr_gid) user_groups) with
223+ | Some _ -> true
224+ | None -> false
225+
226+ let has_unsafe_characters cmd =
227+ (* XXX: this function is highly restrictive now,
228+ until we are completely certain that shell escape
229+ cannot happen down the line inside VyOS op mode scripts.
230+ Alphanumeric characters, hyphens, dots, and whitespace
231+ should allow operator users to use most commands
232+ that take interface names, FQDNs, and config entities
233+ like IPsec peer names.
234+ Notable exceptions are:
235+ - 'show bgp regexp': regexes naturally require '$' and other
236+ patently shell-unsafe characters.
237+ - 'monitor traffic interface eth0 filter':
238+ PCAP filters use '!', '&&' and '||',
239+ although people can use 'and', 'or', 'not'
240+ to get around the restriction.
241+ - 'add system image': requires non-alphanumeric characters
242+ for URLs.
243+ *)
244+ try
245+ let _ = Pcre2. exec ~pat: {| [^ a- zA- Z0 -9_ \- \.\s]| } cmd in
246+ let () =
247+ Printf. fprintf stderr " Command [%s] contains special characters \
248+ that operator-level users are not allowed to use\n " cmd
249+ in
250+ true
251+ with Not_found -> false
252+
253+ let is_admin_only_command cmd =
254+ let rec prefix_matches prefix target =
255+ match prefix, target with
256+ | [] , _ ->
257+ (* The target matched every word of the prefix,
258+ so it's a match.
259+ *)
260+ true
261+ | _ , [] ->
262+ (* The target is shorter than the prefix,
263+ so it's not a match.
264+ *)
265+ false
266+ | (p :: ps ), (t :: ts ) ->
267+ if p = t then prefix_matches ps ts
268+ else false
269+ in
270+ let res = List. find_opt (fun p -> prefix_matches p cmd) admin_only_commands in
271+ match res with
272+ | None -> false
273+ | Some _ -> true
274+
275+ let check_command_permissions perms cmd =
276+ let rec aux perms groups cmd =
277+ match groups with
278+ | [] -> permission_error ()
279+ | g :: gs ->
280+ if group_perms_match perms g cmd then ()
281+ else aux perms gs cmd
282+ in
283+ (* VyOS admins can execute any commands without restrictions *)
284+ if is_admin () then () else
285+ (* Operators are not allowed to execute commands
286+ with potentially unsafe characters in them *)
287+ if has_unsafe_characters (String. concat " " cmd) then permission_error () else
288+ (* Some commands are unconditionally denied to operators *)
289+ if is_admin_only_command cmd then permission_error () else
290+ (* Operator level users must always be in groups
291+ with defined command policies
292+ *)
293+ let username = Unix. getlogin () in
294+ let groups = Yojson.Safe.Util. path [" users" ; username] perms in
295+ match groups with
296+ | None | Some (`List [] ) ->
297+ Printf. ksprintf internal_error " User %s is not assigned to any operator group" username
298+ | Some gs ->
299+ let group_list =
300+ (try
301+ gs |>
302+ Yojson.Safe.Util. to_list |>
303+ List. map Yojson.Safe.Util. to_string
304+ with _ ->
305+ Printf. ksprintf internal_error " The groups field for user %s is not a list of strings"
306+ username)
307+ in
308+ aux perms group_list cmd
309+
140310(* Command rendering and execution *)
141311let render_command opts env command_tmpl =
142312 let () = Logs. debug @@ fun m -> m " Command template: %s" command_tmpl in
@@ -145,25 +315,30 @@ let render_command opts env command_tmpl =
145315 let vyos_command = opts.vyos_command in
146316 Pcre2. replace ~pat: {| \$ [@* ]| } ~templ: vyos_command command
147317
148- let run_command opts env command_tmpl =
318+ let run_external_command opts env command_tmpl =
149319 let cmd = render_command opts env command_tmpl in
150320 if opts.dry_run then Printf. printf " %s\n %!" cmd else
151321 let () = Logs. debug @@ fun m -> m " Command to be executed %s" cmd in
152322 let res = Unix. system cmd in
153323 match res with
154324 | Unix. WEXITED 0 -> ()
155- | _ -> Printf. ksprintf command_error " Execution of command '%s' failed" cmd
325+ | _ ->
326+ (* Many op mode commands return non-zero exit codes on benign errors
327+ such as an unconfigured subsystem,
328+ so we shouldn't show this to the user by default.
329+ *)
330+ Logs. debug @@ fun m -> m " Execution of command '%s' failed" cmd
156331
157332(* Command lookup *)
158- let rec find_command opts ?(env =[] ) ?(parent =" " ) node cmd_words =
333+ let rec run_vyos_command opts ?(env =[] ) ?(parent =" " ) node cmd_words =
159334 match cmd_words with
160335 | w :: ws ->
161336 let () = Logs. debug @@ fun m -> m " Looking up node '%s'" w in
162337 let res = find_child_node node w in
163338 begin match res with
164339 | Some child_node ->
165340 (* It's a normal, fixed command word *)
166- find_command opts ~env: env ~parent: w child_node ws
341+ run_vyos_command opts ~env: env ~parent: w child_node ws
167342 | None ->
168343 (* It's either an argument of a tag node
169344 or an incorrect command word *)
@@ -177,9 +352,9 @@ let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
177352 begin match ws with
178353 | [] ->
179354 let command = get_command node_data in
180- run_command opts env command
355+ run_external_command opts env command
181356 | _ as ws ->
182- find_command opts ~env: env ~parent: w node ws
357+ run_vyos_command opts ~env: env ~parent: w node ws
183358 end
184359 | "node" , Some vtn ->
185360 (* 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 =
188363 | [] ->
189364 let vtn_data = get_node_data vtn in
190365 let command = get_command vtn_data in
191- run_command opts env command
366+ run_external_command opts env command
192367 | _ ->
193368 (* In the case of a virtual tag node, we take the parent (for variable substitution purposes)
194369 from the upper level.
195370 *)
196- find_command opts ~env: env ~parent: parent vtn ws
371+ run_vyos_command opts ~env: env ~parent: parent vtn ws
197372 end
198373 | "node" , None | "leafNode" , None ->
199374 let path = get_path node_data in
@@ -223,7 +398,7 @@ let rec find_command opts ?(env=[]) ?(parent="") node cmd_words =
223398 in
224399 begin match command with
225400 | Some command ->
226- run_command opts env command
401+ run_external_command opts env command
227402 | None ->
228403 raise Incomplete_command
229404 end
@@ -276,19 +451,26 @@ let () =
276451 let debug = if debug then true else options .debug in
277452 let () = setup_logging debug in
278453 let op_defs = read_command_definitions () in
454+ let permissions = read_permissions () in
279455 let () = Unix .setuid 0 in
456+ let () = Logs .debug @@ fun m -> m " Executing VyOS command [%s]" (String. concat " " args) in
280457 try
281- find_command options ~env: [] ~parent: "" op_defs args
282- with
458+ check_command_permissions permissions args;
459+ run_vyos_command options ~env: [] ~parent: " " op_defs args
460+ with
461+ | Permission_error ->
462+ Printf. fprintf stderr " You do not have a permission to execute VyOS command [%s]\n "
463+ options.vyos_command;
464+ exit 1
283465 | Invalid_command msg ->
284- Printf .fprintf stderr "Invalid command [%s]: %s" options .vyos_command msg ;
466+ Printf. fprintf stderr " Invalid command [%s]: %s\n " options.vyos_command msg;
285467 exit 1
286468 | Command_error msg ->
287- Printf. fprintf stderr " %s" msg;
469+ Printf. fprintf stderr " %s\n " msg;
288470 | Incomplete_command ->
289- Printf. fprintf stderr " Incomplete command: %s" options.vyos_command;
471+ Printf. fprintf stderr " Incomplete command: %s\n " options.vyos_command;
290472 exit 2
291473 | Internal_error msg ->
292- Printf. fprintf stderr " Internal error: %s" msg;
474+ Printf. fprintf stderr " Internal error: %s\n " msg;
293475 exit 255
294476
0 commit comments