diff --git a/debian/control b/debian/control index 890100fd8..d1d1602ae 100644 --- a/debian/control +++ b/debian/control @@ -1,381 +1,384 @@ Source: vyos-1x Section: contrib/net Priority: extra Maintainer: VyOS Package Maintainers <maintainers@vyos.net> Build-Depends: debhelper (>= 9), dh-python, fakeroot, gcc, iproute2, libvyosconfig0 (>= 0.0.7), libzmq3-dev, python3 (>= 3.10), # For QA pylint, # For generating command definitions python3-lxml, python3-xmltodict, # For running tests python3-coverage, python3-hurry.filesize, python3-netaddr, python3-netifaces, python3-nose, python3-jinja2, python3-paramiko, python3-passlib, python3-psutil, python3-requests, python3-setuptools, python3-tabulate, python3-zmq, quilt, whois Standards-Version: 3.9.6 Package: vyos-1x Architecture: amd64 arm64 Pre-Depends: libpam-runtime [amd64], libnss-tacplus [amd64], libpam-tacplus [amd64], libpam-radius-auth [amd64] Depends: ## Fundamentals ${python3:Depends} (>= 3.10), dialog, libvyosconfig0, libpam-cap, bash-completion, ipvsadm, udev, less, at, rsync, vyatta-bash, vyatta-biosdevname, vyatta-cfg, vyos-http-api-tools, vyos-utils, ## End of Fundamentals ## Python libraries used in multiple modules and scripts python3, python3-cryptography, python3-hurry.filesize, python3-inotify, python3-jinja2, python3-jmespath, python3-netaddr, python3-netifaces, python3-paramiko, python3-passlib, python3-pyroute2, python3-psutil, python3-pyhumps, python3-pystache, python3-pyudev, python3-six, python3-tabulate, python3-voluptuous, python3-xmltodict, python3-zmq, ## End of Python libraries ## Basic System services and utilities coreutils, sudo, systemd, bsdmainutils, openssl, curl, dbus, file, iproute2 (>= 6.0.0), linux-cpupower, # ipaddrcheck is widely used in IP value validators ipaddrcheck, ethtool, lm-sensors, procps, netplug, sed, ssl-cert, tuned, beep, wide-dhcpv6-client, # Generic colorizer grc, ## End of System services and utilities ## For the installer fdisk, gdisk, mdadm, efibootmgr, libefivar1, dosfstools, - grub-efi-amd64-bin [amd64], + grub-efi-amd64-signed [amd64], grub-efi-arm64-bin [arm64], + mokutil [amd64], + shim-signed [amd64], + sbsigntool [amd64], # Image signature verification tool minisign, # Live filesystem tools squashfs-tools, fuse-overlayfs, ## End installer auditd, iputils-arping, iputils-ping, isc-dhcp-client, # For "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server" accel-ppp, # End "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server" avahi-daemon, conntrack, conntrackd, ## Conf mode features # For "interfaces wireless" hostapd, hsflowd, iw, wireless-regdb, wpasupplicant (>= 0.6.7), # End "interfaces wireless" # For "interfaces wwan" modemmanager, usb-modeswitch, libqmi-utils, # End "interfaces wwan" # For "interfaces openvpn" openvpn, openvpn-auth-ldap, openvpn-auth-radius, openvpn-otp, openvpn-dco, libpam-google-authenticator, # End "interfaces openvpn" # For "interfaces wireguard" wireguard-tools, qrencode, # End "interfaces wireguard" # For "interfaces pppoe" pppoe, # End "interfaces pppoe" # For "interfaces sstpc" sstp-client, # End "interfaces sstpc" # For "protocols *" frr (>= 9.1), frr-pythontools, frr-rpki-rtrlib, frr-snmp, # End "protocols *" # For "protocols nhrp" (part of DMVPN) opennhrp, # End "protocols nhrp" # For "protocols igmp-proxy" igmpproxy, # End "protocols igmp-proxy" # For "pki" certbot, # End "pki" # For "service console-server" conserver-client, conserver-server, console-data, dropbear, # End "service console-server" # For "service aws glb" aws-gwlbtun, # For "service dns dynamic" ddclient (>= 3.11.1), # End "service dns dynamic" # # For "service ids" fastnetmon [amd64], suricata, suricata-update, # End "service ids" # # For "service ndp-proxy" ndppd, # End "service ndp-proxy" # For "service router-advert" radvd, # End "service route-advert" # For "load-balancing reverse-proxy" haproxy, # End "load-balancing reverse-proxy" # For "load-balancing wan" vyatta-wanloadbalance, # End "load-balancing wan" # For "service dhcp-relay" isc-dhcp-relay, # For "service dhcp-server" kea, # End "service dhcp-server" # For "service lldp" lldpd, # End "service lldp" # For "service https" nginx-light, # End "service https" # For "service ssh" openssh-server, sshguard, # End "service ssh" # For "service salt-minion" salt-minion, # End "service salt-minion" # For "service snmp" snmp, snmpd, # End "service snmp" # For "service webproxy" squid, squidclient, squidguard, # End "service webproxy" # For "service monitoring telegraf" telegraf (>= 1.20), # End "service monitoring telegraf" # For "service monitoring zabbix-agent" zabbix-agent2, # End "service monitoring zabbix-agent" # For "service tftp-server" tftpd-hpa, # End "service tftp-server" # For "service dns forwarding" pdns-recursor, # End "service dns forwarding" # For "service sla owamp" owamp-client, owamp-server, # End "service sla owamp" # For "service sla twamp" twamp-client, twamp-server, # End "service sla twamp" # For "service broadcast-relay" udp-broadcast-relay, # End "service broadcast-relay" # For "high-availability vrrp" keepalived (>=2.0.5), # End "high-availability-vrrp" # For "system console" util-linux, # End "system console" # For "system task-scheduler" cron, # End "system task-scheduler" # For "system lcd" lcdproc, lcdproc-extra-drivers, # End "system lcd" # For "system config-management commit-archive" git, # End "system config-management commit-archive" # For firewall libndp-tools, libnetfilter-conntrack3, libnfnetlink0, nfct, nftables (>= 0.9.3), # For "vpn ipsec" strongswan (>= 5.9), strongswan-swanctl (>= 5.9), charon-systemd, libcharon-extra-plugins (>=5.9), libcharon-extauth-plugins (>=5.9), libstrongswan-extra-plugins (>=5.9), libstrongswan-standard-plugins (>=5.9), python3-vici (>= 5.7.2), # End "vpn ipsec" # For "nat64" jool, # End "nat64" # For "system conntrack modules rtsp" nat-rtsp, # End "system conntrack modules rtsp" # For "service ntp" chrony, # End "system ntp" # For "vpn openconnect" ocserv, # End "vpn openconnect" # For "system flow-accounting" pmacct (>= 1.6.0), # End "system flow-accounting" # For "system syslog" rsyslog, # End "system syslog" # For "system option keyboard-layout" kbd, # End "system option keyboard-layout" # For "container" podman (>=4.9.5), netavark, aardvark-dns, # iptables is only used for containers now, not the the firewall CLI iptables, # End container ## End Configuration mode ## Operational mode # Used for hypervisor model in "run show version" hvinfo, # For "run traceroute" traceroute, # For "run monitor traffic" tcpdump, # End "run monitor traffic" # For "show hardware dmi" dmidecode, # For "run show hardware storage smart" smartmontools, # For "run show hardware scsi" lsscsi, # For "run show hardware pci" pciutils, # For "show hardware usb" usbutils, # For "run show hardware storage nvme" nvme-cli, # For "run monitor bandwidth-test" iperf, iperf3, # End "run monitor bandwidth-test" # For "run wake-on-lan" etherwake, # For "run force ipv6-nd" ndisc6, # For "run monitor bandwidth" bmon, # For "run format disk" parted, # End Operational mode ## TPM tools cryptsetup, tpm2-tools, ## End TPM tools ## Optional utilities easy-rsa, tcptraceroute, mtr-tiny, telnet, stunnel4, uidmap ## End optional utilities Description: VyOS configuration scripts and data VyOS configuration scripts, interface definitions, and everything Package: vyos-1x-vmware Architecture: amd64 Depends: vyos-1x, open-vm-tools Description: VyOS configuration scripts and data for VMware Adds configuration files required for VyOS running on VMware hosts. Package: vyos-1x-smoketest Architecture: all Depends: skopeo, snmp, vyos-1x Description: VyOS build sanity checking toolkit diff --git a/op-mode-definitions/install-mok.xml.in b/op-mode-definitions/install-mok.xml.in new file mode 100644 index 000000000..18526a354 --- /dev/null +++ b/op-mode-definitions/install-mok.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="install"> + <children> + <leafNode name="mok"> + <properties> + <help>Install Secure Boot MOK (Machine Owner Key)</help> + </properties> + <command>if test -f /var/lib/shim-signed/mok/MOK.der; then sudo mokutil --ignore-keyring --import /var/lib/shim-signed/mok/MOK.der; else echo "Secure Boot Machine Owner Key not found"; fi</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/show-secure-boot.xml.in b/op-mode-definitions/show-secure-boot.xml.in new file mode 100644 index 000000000..ff731bac9 --- /dev/null +++ b/op-mode-definitions/show-secure-boot.xml.in @@ -0,0 +1,21 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="secure-boot"> + <properties> + <help>Show Secure Boot state</help> + </properties> + <command>${vyos_op_scripts_dir}/secure_boot.py show</command> + <children> + <leafNode name="keys"> + <properties> + <help>Show enrolled certificates</help> + </properties> + <command>mokutil --list-enrolled</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index daddb799a..de8303ee2 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -1,464 +1,464 @@ # Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import platform from pathlib import Path from re import MULTILINE, compile as re_compile from shutil import copy2 from uuid import uuid5, NAMESPACE_URL, UUID from vyos.template import render from vyos.utils.process import cmd, rc_cmd from vyos.system import disk # Define variables GRUB_DIR_MAIN: str = '/boot/grub' GRUB_CFG_MAIN: str = f'{GRUB_DIR_MAIN}/grub.cfg' GRUB_DIR_VYOS: str = f'{GRUB_DIR_MAIN}/grub.cfg.d' CFG_VYOS_HEADER: str = f'{GRUB_DIR_VYOS}/00-vyos-header.cfg' CFG_VYOS_MODULES: str = f'{GRUB_DIR_VYOS}/10-vyos-modules-autoload.cfg' CFG_VYOS_VARS: str = f'{GRUB_DIR_VYOS}/20-vyos-defaults-autoload.cfg' CFG_VYOS_COMMON: str = f'{GRUB_DIR_VYOS}/25-vyos-common-autoload.cfg' CFG_VYOS_PLATFORM: str = f'{GRUB_DIR_VYOS}/30-vyos-platform-autoload.cfg' CFG_VYOS_MENU: str = f'{GRUB_DIR_VYOS}/40-vyos-menu-autoload.cfg' CFG_VYOS_OPTIONS: str = f'{GRUB_DIR_VYOS}/50-vyos-options.cfg' GRUB_DIR_VYOS_VERS: str = f'{GRUB_DIR_VYOS}/vyos-versions' TMPL_VYOS_VERSION: str = 'grub/grub_vyos_version.j2' TMPL_GRUB_VARS: str = 'grub/grub_vars.j2' TMPL_GRUB_MAIN: str = 'grub/grub_main.j2' TMPL_GRUB_MENU: str = 'grub/grub_menu.j2' TMPL_GRUB_MODULES: str = 'grub/grub_modules.j2' TMPL_GRUB_OPTS: str = 'grub/grub_options.j2' TMPL_GRUB_COMMON: str = 'grub/grub_common.j2' # default boot options BOOT_OPTS_STEM: str = 'boot=live rootdelay=5 noautologin net.ifnames=0 biosdevname=0 vyos-union=/boot/' # prepare regexes REGEX_GRUB_VARS: str = r'^set (?P<variable_name>\w+)=[\'"]?(?P<variable_value>.*)(?<![\'"])[\'"]?$' REGEX_GRUB_MODULES: str = r'^insmod (?P<module_name>.+)$' REGEX_KERNEL_CMDLINE: str = r'^BOOT_IMAGE=/(?P<boot_type>boot|live)/((?P<image_version>.+)/)?vmlinuz.*$' REGEX_GRUB_BOOT_OPTS: str = r'^\s*set boot_opts="(?P<boot_opts>[^$]+)"$' def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS', chroot : str = "") -> None: """Install GRUB for both BIOS and EFI modes (hybrid boot) Args: drive_path (str): path to a drive where GRUB must be installed boot_dir (str): a path to '/boot' directory efi_dir (str): a path to '/boot/efi' directory """ if chroot: chroot_cmd = f"chroot {chroot}" else: chroot_cmd = "" efi_installation_arch = "x86_64" if platform.machine() == "aarch64": efi_installation_arch = "arm64" elif platform.machine() == "x86_64": cmd( f'{chroot_cmd} grub-install --no-floppy --target=i386-pc \ --boot-directory={boot_dir} {drive_path} --force' ) cmd( f'{chroot_cmd} grub-install --no-floppy --recheck --target={efi_installation_arch}-efi \ --force-extra-removable --boot-directory={boot_dir} \ --efi-directory={efi_dir} --bootloader-id="{id}" \ - --no-uefi-secure-boot' + --uefi-secure-boot' ) def gen_version_uuid(version_name: str) -> str: """Generate unique ID from version name Use UUID5 / NAMESPACE_URL with prefix `uuid5-` Args: version_name (str): version name Returns: str: generated unique ID """ ver_uuid: UUID = uuid5(NAMESPACE_URL, version_name) ver_id: str = f'uuid5-{ver_uuid}' return ver_id def version_add(version_name: str, root_dir: str = '', boot_opts: str = '', boot_opts_config = None) -> None: """Add a new VyOS version to GRUB loader configuration Args: vyos_version (str): VyOS version name root_dir (str): an optional path to the root directory. Defaults to empty. boot_opts (str): an optional boot options for Linux kernel. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() version_config: str = f'{root_dir}/{GRUB_DIR_VYOS_VERS}/{version_name}.cfg' render( version_config, TMPL_VYOS_VERSION, { 'version_name': version_name, 'version_uuid': gen_version_uuid(version_name), 'boot_opts_default': BOOT_OPTS_STEM + version_name, 'boot_opts': boot_opts, 'boot_opts_config': boot_opts_config }) def version_del(vyos_version: str, root_dir: str = '') -> None: """Delete a VyOS version from GRUB loader configuration Args: vyos_version (str): VyOS version name root_dir (str): an optional path to the root directory. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() version_config: str = f'{root_dir}/{GRUB_DIR_VYOS_VERS}/{vyos_version}.cfg' Path(version_config).unlink(missing_ok=True) def version_list(root_dir: str = '') -> list[str]: """Generate a list with installed VyOS versions Args: root_dir (str): an optional path to the root directory. Defaults to empty. Returns: list: A list with versions names N.B. coreutils stat reports st_birthtime, but not available in Path.stat()/os.stat() """ if not root_dir: root_dir = disk.find_persistence() versions_files = Path(f'{root_dir}/{GRUB_DIR_VYOS_VERS}').glob('*.cfg') versions_order: dict[str, int] = {} for file in versions_files: p = Path(root_dir).joinpath('boot').joinpath(file.stem) command = f'stat -c %W {p.as_posix()}' rc, out = rc_cmd(command) if rc == 0: versions_order[file.stem] = int(out) versions_order = sorted(versions_order, key=versions_order.get, reverse=True) versions_list: list[str] = list(versions_order) return versions_list def read_env(env_file: str = '') -> dict[str, str]: """Read GRUB environment Args: env_file (str, optional): a path to grub environment file. Defaults to empty. Returns: dict: dictionary with GRUB environment """ if not env_file: root_dir: str = disk.find_persistence() env_file = f'{root_dir}/{GRUB_DIR_MAIN}/grubenv' env_content: str = cmd(f'grub-editenv {env_file} list').splitlines() regex_filter = re_compile(r'^(?P<variable_name>.*)=(?P<variable_value>.*)$') env_dict: dict[str, str] = {} for env_item in env_content: search_result = regex_filter.fullmatch(env_item) if search_result: search_result_dict: dict[str, str] = search_result.groupdict() variable_name: str = search_result_dict.get('variable_name', '') variable_value: str = search_result_dict.get('variable_value', '') if variable_name and variable_value: env_dict.update({variable_name: variable_value}) return env_dict def get_cfg_ver(root_dir: str = '') -> int: """Get current version of GRUB configuration Args: root_dir (str, optional): an optional path to the root directory. Defaults to empty. Returns: int: a configuration version """ if not root_dir: root_dir = disk.find_persistence() cfg_ver: str = vars_read(f'{root_dir}/{CFG_VYOS_HEADER}').get( 'VYOS_CFG_VER') if cfg_ver: cfg_ver_int: int = int(cfg_ver) else: cfg_ver_int: int = 0 return cfg_ver_int def write_cfg_ver(cfg_ver: int, root_dir: str = '') -> None: """Write version number of GRUB configuration Args: cfg_ver (int): a version number to write root_dir (str, optional): an optional path to the root directory. Defaults to empty. Returns: int: a configuration version """ if not root_dir: root_dir = disk.find_persistence() vars_file: str = f'{root_dir}/{CFG_VYOS_HEADER}' vars_current: dict[str, str] = vars_read(vars_file) vars_current['VYOS_CFG_VER'] = str(cfg_ver) vars_write(vars_file, vars_current) def vars_read(grub_cfg: str) -> dict[str, str]: """Read variables from a GRUB configuration file Args: grub_cfg (str): a path to the GRUB config file Returns: dict: a dictionary with variables and values """ vars_dict: dict[str, str] = {} regex_filter = re_compile(REGEX_GRUB_VARS) try: config_text: list[str] = Path(grub_cfg).read_text().splitlines() except FileNotFoundError: return vars_dict for line in config_text: search_result = regex_filter.fullmatch(line) if search_result: search_dict = search_result.groupdict() variable_name: str = search_dict.get('variable_name', '') variable_value: str = search_dict.get('variable_value', '') if variable_name and variable_value: vars_dict.update({variable_name: variable_value}) return vars_dict def modules_read(grub_cfg: str) -> list[str]: """Read modules list from a GRUB configuration file Args: grub_cfg (str): a path to the GRUB config file Returns: list: a list with modules to load """ mods_list: list[str] = [] regex_filter = re_compile(REGEX_GRUB_MODULES, MULTILINE) try: config_text = Path(grub_cfg).read_text() except FileNotFoundError: return mods_list mods_list = regex_filter.findall(config_text) return mods_list def modules_write(grub_cfg: str, mods_list: list[str]) -> None: """Write modules list to a GRUB configuration file (overwrite everything) Args: grub_cfg (str): a path to GRUB configuration file mods_list (list): a list with modules to load """ render(grub_cfg, TMPL_GRUB_MODULES, {'mods_list': mods_list}) def vars_write(grub_cfg: str, grub_vars: dict[str, str]) -> None: """Write variables to a GRUB configuration file (overwrite everything) Args: grub_cfg (str): a path to GRUB configuration file grub_vars (dict): a dictionary with new variables """ render(grub_cfg, TMPL_GRUB_VARS, {'vars': grub_vars}) def get_boot_opts(version_name: str, root_dir: str = '') -> str: """Read boot_opts setting from version file; return default setting on any failure. Args: version_name (str): version name root_dir (str, optional): an optional path to the root directory. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() boot_opts_default: str = BOOT_OPTS_STEM + version_name boot_opts: str = '' regex_filter = re_compile(REGEX_GRUB_BOOT_OPTS) version_config: str = f'{root_dir}/{GRUB_DIR_VYOS_VERS}/{version_name}.cfg' try: config_text: list[str] = Path(version_config).read_text().splitlines() except FileNotFoundError: return boot_opts_default for line in config_text: search_result = regex_filter.fullmatch(line) if search_result: search_dict = search_result.groupdict() boot_opts = search_dict.get('boot_opts', '') break if not boot_opts: boot_opts = boot_opts_default return boot_opts def set_default(version_name: str, root_dir: str = '') -> None: """Set version as default boot entry Args: version_name (str): version name root_dir (str, optional): an optional path to the root directory. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() vars_file = f'{root_dir}/{CFG_VYOS_VARS}' vars_current = vars_read(vars_file) vars_current['default'] = gen_version_uuid(version_name) vars_write(vars_file, vars_current) def common_write(root_dir: str = '', grub_common: dict[str, str] = {}) -> None: """Write common GRUB configuration file (overwrite everything) Args: root_dir (str, optional): an optional path to the root directory. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() common_config = f'{root_dir}/{CFG_VYOS_COMMON}' render(common_config, TMPL_GRUB_COMMON, grub_common) def create_structure(root_dir: str = '') -> None: """Create GRUB directories structure Args: root_dir (str, optional): an optional path to the root directory. Defaults to ''. """ if not root_dir: root_dir = disk.find_persistence() Path(f'{root_dir}/{GRUB_DIR_VYOS_VERS}').mkdir(parents=True, exist_ok=True) def set_console_type(console_type: str, root_dir: str = '') -> None: """Write default console type to GRUB configuration Args: console_type (str): a default console type root_dir (str, optional): an optional path to the root directory. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() vars_file: str = f'{root_dir}/{CFG_VYOS_VARS}' vars_current: dict[str, str] = vars_read(vars_file) vars_current['console_type'] = str(console_type) vars_write(vars_file, vars_current) def set_console_speed(console_speed: str, root_dir: str = '') -> None: """Write default console speed to GRUB configuration Args: console_speed (str): default console speed root_dir (str, optional): an optional path to the root directory. Defaults to empty. """ if not root_dir: root_dir = disk.find_persistence() vars_file: str = f'{root_dir}/{CFG_VYOS_VARS}' vars_current: dict[str, str] = vars_read(vars_file) vars_current['console_speed'] = str(console_speed) vars_write(vars_file, vars_current) def set_kernel_cmdline_options(cmdline_options: str, version_name: str, root_dir: str = '') -> None: """Write additional cmdline options to GRUB configuration Args: cmdline_options (str): cmdline options to add to default boot line version_name (str): image version name root_dir (str, optional): an optional path to the root directory. """ if not root_dir: root_dir = disk.find_persistence() version_add(version_name=version_name, root_dir=root_dir, boot_opts_config=cmdline_options) def sort_inodes(dir_path: str) -> None: """Sort inodes for files inside a folder Regenerate inodes for each file to get the same order for both inodes and file names GRUB iterates files by inodes, not alphabetically. Therefore, if we want to read them in proper order, we need to sort inodes for all config files in a folder. Args: dir_path (str): a path to directory """ dir_content: list[Path] = sorted(Path(dir_path).iterdir()) temp_list_old: list[Path] = [] temp_list_new: list[Path] = [] # create a copy of all files, to get new inodes for item in dir_content: # skip directories if item.is_dir(): continue # create a new copy of file with a temporary name copy_path = Path(f'{item.as_posix()}_tmp') copy2(item, Path(copy_path)) temp_list_old.append(item) temp_list_new.append(copy_path) # delete old files and rename new ones for item in temp_list_old: item.unlink() for item in temp_list_new: new_name = Path(f'{item.as_posix()[0:-4]}') item.rename(new_name) diff --git a/python/vyos/utils/boot.py b/python/vyos/utils/boot.py index 3aecbec64..708bef14d 100644 --- a/python/vyos/utils/boot.py +++ b/python/vyos/utils/boot.py @@ -1,35 +1,39 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os def boot_configuration_complete() -> bool: """ Check if the boot config loader has completed """ from vyos.defaults import config_status if os.path.isfile(config_status): return True return False def boot_configuration_success() -> bool: from vyos.defaults import config_status try: with open(config_status) as f: res = f.read().strip() except FileNotFoundError: return False if int(res) == 0: return True return False + +def is_uefi_system() -> bool: + efi_fw_dir = '/sys/firmware/efi' + return os.path.exists(efi_fw_dir) and os.path.isdir(efi_fw_dir) diff --git a/python/vyos/utils/system.py b/python/vyos/utils/system.py index fca93d118..7b12efb14 100644 --- a/python/vyos/utils/system.py +++ b/python/vyos/utils/system.py @@ -1,141 +1,149 @@ # Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2.1 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os from subprocess import run def sysctl_read(name: str) -> str: """Read and return current value of sysctl() option Args: name (str): sysctl key name Returns: str: sysctl key value """ tmp = run(['sysctl', '-nb', name], capture_output=True) return tmp.stdout.decode() def sysctl_write(name: str, value: str | int) -> bool: """Change value via sysctl() Args: name (str): sysctl key name value (str | int): sysctl key value Returns: bool: True if changed, False otherwise """ # convert other types to string before comparison if not isinstance(value, str): value = str(value) # do not change anything if a value is already configured if sysctl_read(name) == value: return True # return False if sysctl call failed if run(['sysctl', '-wq', f'{name}={value}']).returncode != 0: return False # compare old and new values # sysctl may apply value, but its actual value will be # different from requested if sysctl_read(name) == value: return True # False in other cases return False def sysctl_apply(sysctl_dict: dict[str, str], revert: bool = True) -> bool: """Apply sysctl values. Args: sysctl_dict (dict[str, str]): dictionary with sysctl keys with values revert (bool, optional): Revert to original values if new were not applied. Defaults to True. Returns: bool: True if all params configured properly, False in other cases """ # get current values sysctl_original: dict[str, str] = {} for key_name in sysctl_dict.keys(): sysctl_original[key_name] = sysctl_read(key_name) # apply new values and revert in case one of them was not applied for key_name, value in sysctl_dict.items(): if not sysctl_write(key_name, value): if revert: sysctl_apply(sysctl_original, revert=False) return False # everything applied return True def find_device_file(device): """ Recurively search /dev for the given device file and return its full path. If no device file was found 'None' is returned """ from fnmatch import fnmatch for root, dirs, files in os.walk('/dev'): for basename in files: if fnmatch(basename, device): return os.path.join(root, basename) return None def load_as_module(name: str, path: str): import importlib.util spec = importlib.util.spec_from_file_location(name, path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def load_as_module_source(name: str, path: str): """ Necessary modification of load_as_module for files without *.py extension """ import importlib.util from importlib.machinery import SourceFileLoader loader = SourceFileLoader(name, path) spec = importlib.util.spec_from_loader(name, loader) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod def get_uptime_seconds(): """ Returns system uptime in seconds """ from re import search from vyos.utils.file import read_file data = read_file("/proc/uptime") seconds = search(r"([0-9\.]+)\s", data).group(1) res = int(float(seconds)) return res def get_load_averages(): """ Returns load averages for 1, 5, and 15 minutes as a dict """ from re import search from vyos.utils.file import read_file from vyos.utils.cpu import get_core_count data = read_file("/proc/loadavg") matches = search(r"\s*(?P<one>[0-9\.]+)\s+(?P<five>[0-9\.]+)\s+(?P<fifteen>[0-9\.]+)\s*", data) core_count = get_core_count() res = {} res[1] = float(matches["one"]) / core_count res[5] = float(matches["five"]) / core_count res[15] = float(matches["fifteen"]) / core_count return res + +def get_secure_boot_state() -> bool: + from vyos.utils.process import cmd + from vyos.utils.boot import is_uefi_system + if not is_uefi_system(): + return False + tmp = cmd('mokutil --sb-state') + return bool('enabled' in tmp) diff --git a/src/etc/sudoers.d/vyos b/src/etc/sudoers.d/vyos index 63a944f41..67d7babc4 100644 --- a/src/etc/sudoers.d/vyos +++ b/src/etc/sudoers.d/vyos @@ -1,60 +1,63 @@ # # VyOS modifications to sudo configuration # Defaults syslog_goodpri=info Defaults env_keep+=VYATTA_* # # Command groups allowed for operator users # Cmnd_Alias IPTABLES = /sbin/iptables --list -n,\ /sbin/iptables -L -vn,\ /sbin/iptables -L * -vn,\ /sbin/iptables -t * -L *, \ /sbin/iptables -Z *,\ /sbin/iptables -Z -t nat, \ /sbin/iptables -t * -Z * Cmnd_Alias IP6TABLES = /sbin/ip6tables -t * -Z *, \ /sbin/ip6tables -t * -L * Cmnd_Alias CONNTRACK = /usr/sbin/conntrack -L *, \ /usr/sbin/conntrack -G *, \ /usr/sbin/conntrack -E * Cmnd_Alias IPFLUSH = /sbin/ip route flush cache, \ /sbin/ip route flush cache *,\ /sbin/ip neigh flush to *, \ /sbin/ip neigh flush dev *, \ /sbin/ip -f inet6 route flush cache, \ /sbin/ip -f inet6 route flush cache *,\ /sbin/ip -f inet6 neigh flush to *, \ /sbin/ip -f inet6 neigh flush dev * Cmnd_Alias ETHTOOL = /sbin/ethtool -p *, \ /sbin/ethtool -S *, \ /sbin/ethtool -a *, \ /sbin/ethtool -c *, \ /sbin/ethtool -i * Cmnd_Alias DMIDECODE = /usr/sbin/dmidecode Cmnd_Alias DISK = /usr/bin/lsof, /sbin/fdisk -l *, /sbin/sfdisk -d * Cmnd_Alias DATE = /bin/date, /usr/sbin/ntpdate Cmnd_Alias PPPOE_CMDS = /sbin/pppd, /sbin/poff, /usr/sbin/pppstats Cmnd_Alias PCAPTURE = /usr/bin/tcpdump Cmnd_Alias HWINFO = /usr/bin/lspci Cmnd_Alias FORCE_CLUSTER = /usr/share/heartbeat/hb_takeover, \ /usr/share/heartbeat/hb_standby Cmnd_Alias DIAGNOSTICS = /bin/ip vrf exec * /bin/ping *, \ /bin/ip vrf exec * /bin/traceroute *, \ /bin/ip vrf exec * /usr/bin/mtr *, \ /usr/libexec/vyos/op_mode/* Cmnd_Alias KEA_IP6_ROUTES = /sbin/ip -6 route replace *,\ /sbin/ip -6 route del * %operator ALL=NOPASSWD: DATE, IPTABLES, ETHTOOL, IPFLUSH, HWINFO, \ PPPOE_CMDS, PCAPTURE, /usr/sbin/wanpipemon, \ DMIDECODE, DISK, CONNTRACK, IP6TABLES, \ FORCE_CLUSTER, DIAGNOSTICS # Allow any user to run files in sudo-users %users ALL=NOPASSWD: /opt/vyatta/bin/sudo-users/ # Allow members of group sudo to execute any command %sudo ALL=NOPASSWD: ALL +# Allow any user to query Machine Owner Key status +%sudo ALL=NOPASSWD: /usr/bin/mokutil + _kea ALL=NOPASSWD: KEA_IP6_ROUTES diff --git a/src/op_mode/secure_boot.py b/src/op_mode/secure_boot.py new file mode 100755 index 000000000..5f6390a15 --- /dev/null +++ b/src/op_mode/secure_boot.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import vyos.opmode + +from vyos.utils.boot import is_uefi_system +from vyos.utils.system import get_secure_boot_state + +def _get_raw_data(name=None): + sb_data = { + 'state' : get_secure_boot_state(), + 'uefi' : is_uefi_system() + } + return sb_data + +def _get_formatted_output(raw_data): + if not raw_data['uefi']: + print('System run in legacy BIOS mode!') + state = 'enabled' if raw_data['state'] else 'disabled' + return f'SecureBoot {state}' + +def show(raw: bool): + sb_data = _get_raw_data() + if raw: + return sb_data + else: + return _get_formatted_output(sb_data) + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/version.py b/src/op_mode/version.py index 09d69ad1d..71a40dd50 100755 --- a/src/op_mode/version.py +++ b/src/op_mode/version.py @@ -1,88 +1,97 @@ #!/usr/bin/env python3 # # Copyright (C) 2016-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # # Purpose: # Displays image version and system information. # Used by the "run show version" command. import sys import typing import vyos.opmode import vyos.version import vyos.limericks +from vyos.utils.boot import is_uefi_system +from vyos.utils.system import get_secure_boot_state + from jinja2 import Template version_output_tmpl = """ Version: VyOS {{version}} Release train: {{release_train}} Release flavor: {{flavor}} Built by: {{built_by}} Built on: {{built_on}} Build UUID: {{build_uuid}} Build commit ID: {{build_git}} {%- if build_comment %} Build comment: {{build_comment}} {% endif %} Architecture: {{system_arch}} Boot via: {{boot_via}} System type: {{system_type}} +Secure Boot: {{secure_boot}} Hardware vendor: {{hardware_vendor}} Hardware model: {{hardware_model}} Hardware S/N: {{hardware_serial}} Hardware UUID: {{hardware_uuid}} Copyright: VyOS maintainers and contributors {%- if limerick %} {{limerick}} {% endif -%} """ def _get_raw_data(funny=False): version_data = vyos.version.get_full_version_data() + version_data["secure_boot"] = "n/a (BIOS)" + if is_uefi_system(): + version_data["secure_boot"] = "disabled" + if get_secure_boot_state(): + version_data["secure_boot"] = "enabled" if funny: version_data["limerick"] = vyos.limericks.get_random() return version_data def _get_formatted_output(version_data): tmpl = Template(version_output_tmpl) return tmpl.render(version_data).strip() def show(raw: bool, funny: typing.Optional[bool]): """ Display neighbor table contents """ version_data = _get_raw_data(funny=funny) if raw: return version_data else: return _get_formatted_output(version_data) if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) if res: print(res) except (ValueError, vyos.opmode.Error) as e: print(e) sys.exit(1)