Esta pagina se ve mejor con JavaScript habilitado

Máquinas virtuales bhyve bajo vm-bhyve

 ·  🎃 kr0m

Bhyve es el hypervisor de virtualización oficial de FreeBSD a partir de la versión 10.0-RELEASE, en este artículo utilizaremos vm-bhyve, un wrapper que nos facilitará mucho la gestion de máquinas y de la red.


Instalación

Instalamos todo lo necesario:

pkg install vm-bhyve qemu-tools bhyve-firmware grub2-bhyve cdrkit-genisoimage tmux

Creamos un pool ZFS donde se almacenarán las máquinas virtuales:

zfs create zroot/vm
zfs set recordsize=64K zroot/vm

Habilitamos el servicio y le indicamos donde estarán alojadas las máquinas virtuales:

sysrc vm_enable="YES"
sysrc vm_dir="zfs:zroot/vm"

Inicializamos vm-bhyve:

vm init

Copiamos los templates de ejemplo:

cp /usr/local/share/examples/vm-bhyve/* /zroot/vm/.templates/

Creamos un switch para conectar las máquinas virtuales con la interfaz de red física:

vm switch create public

Añadimos la interfaz física al switch:

vm switch add public bge0

Podemos ver los switches disponibles:

vm switch list

NAME    TYPE      IFACE      ADDRESS  PRIVATE  MTU  VLAN  PORTS
public  standard  vm-public  -        no       -    -     bge0

Cuando arranquemos una máquina virtual la interfaz tap correspondiente de la máquina se añadirá de forma automática al bridge: vm-public asociado con el switch: public.

Una interfaz física solo puede estar en un bridge, así que si algún software previo ya la está utilizando en algún bridge tendremos que reutilizar el bridge existente.
En mi caso el bridge existente se llama bridge0, por lo tanto creo un switch de vm-bhyve llamado bridge0:

vm switch create -t manual -b bridge0 bridge0

NAME     TYPE      IFACE      ADDRESS  PRIVATE  MTU  VLAN  PORTS
public   standard  vm-public  -        no       -    -     -
bridge0  manual    bridge0    n/a      no       n/a  n/a   n/a

Para que las máquinas utilicen dicho switch debemos modificar el template que vayamos a utilizar:

grep switch /zroot/vm/.templates/*.conf

/zroot/vm/.templates/alpine.conf:network0_switch="public"
/zroot/vm/.templates/arch.conf:network0_switch="public"
/zroot/vm/.templates/centos6.conf:network0_switch="public"
/zroot/vm/.templates/centos7.conf:network0_switch="public"
/zroot/vm/.templates/coreos.conf:network0_switch="public"
/zroot/vm/.templates/debian.conf:network0_switch="public"
/zroot/vm/.templates/default.conf:network0_switch="public"
/zroot/vm/.templates/dragonfly.conf:network0_switch="public"
/zroot/vm/.templates/freebsd-zvol.conf:network0_switch="public"
/zroot/vm/.templates/freepbx.conf:network0_switch="public"
/zroot/vm/.templates/linux-zvol.conf:network0_switch="public"
/zroot/vm/.templates/netbsd.conf:network0_switch="public"
/zroot/vm/.templates/openbsd.conf:network0_switch="public"
/zroot/vm/.templates/resflash.conf:network0_switch="public"
/zroot/vm/.templates/ubuntu.conf:network0_switch="public"
/zroot/vm/.templates/windows.conf:network0_switch="public"

Nos bajamos la imágenes de los sistemas operativos que queramos instalar, en mi caso Kali-linux:

ls -la /zroot/vm/.iso/kali-linux-2022.1-installer-amd64.iso
-rw-r--r--  1 root  wheel  2994323456 Feb  7 19:46 /zroot/vm/.iso/kali-linux-2022.1-installer-amd64.iso

Una VM Linux puede arrancar con distintos loaders grub/uefi, según el que utilicemos accederemos a la VM de un modo u otro, mi consejo es utilizar grub y acceder mediante puerto serie a las VMs ya que no precisa de ningún software adicional, además si necesitamos arrancar alguna aplicación gráfica siempre podremos forwardear las X.


Puerto serie

Creamos la máquina virtual:

vm create -c 2 -m 2G -s 15G -t linux-zvol kali

Instalamos desde la ISO:

vm install -f kali kali-linux-2022.1-installer-amd64.iso

Veremos el instalador en la propia consola:

Podemos consultar información de la máquina virtual mediante el siguiente comando:

vm info kali

------------------------
Virtual Machine: kali
------------------------
  state: running (99075)
  datastore: default
  loader: grub
  uuid: 05ced6c1-d3a7-11ec-875e-5065f37a62c0
  uefi: default
  cpu: 2
  memory: 2G
  memory-resident: 215412736 (205.433M)

  network-interface
    number: 0
    emulation: virtio-net
    virtual-switch: public
    fixed-mac-address: 58:9c:fc:00:81:dd
    fixed-device: -
    active-device: tap0
    desc: vmnet-kali-0-public
    mtu: 1500
    bridge: vm-public
    bytes-in: 0 (0.000B)
    bytes-out: 0 (0.000B)

  virtual-disk
    number: 0
    device-type: sparse-zvol
    emulation: virtio-blk
    options: -
    system-path: /dev/zvol/zroot/vm/kali/disk0
    bytes-size: 16106127360 (15.000G)
    bytes-used: 57344 (56.000K)

Si listamos las máquinas virtuales veremos que está corriendo:

vm list

NAME  DATASTORE  LOADER  CPU  MEMORY  VNC  AUTOSTART  STATE
kali  default    grub    2    2G      -    No         Running (99075)

Para salir de la consola debemos presionar:

~ + (CtrDerecha + d)

VNC

Creamos la máquina virtual:

vm create -c 2 -m 2G -s 15G -t linux-zvol kali2

La configuramos para que el loader sea uefi y definimos los parámetros de configuración VNC:

vm configure kali2

loader="grub" -> loader="uefi"
graphics="yes"
xhci_mouse="yes" -> Mejora la experiencia con el ratón
graphics_listen="IP_SERVER_PADRE"
graphics_port="5900"
graphics_res="1600x900"
graphics_wait="yes" --> Deja la VM en pausa hasta que conectemos por VNC, brutal. Recordad eliminar este parámetro una vez hayamos realizado la instalación inicial.

Iniciamos la instalación:

vm install kali2 kali-linux-2022.1-installer-amd64.iso

Arrancamos la máquina:

vm start kali2

Al consultar las máquinas virtuales veremos que esta última tiene asociado un socket VNC:

vm list

NAME   DATASTORE  LOADER  CPU  MEMORY  VNC                  AUTOSTART  STATE
kali   default    grub    2    2G      -                    No         Running (42962)
kali2  default    uefi    2    2G      192.168.69.180:5900  No         Locked (odyssey.alfaexploit.com)

Accedemos al instalador vía VNC:

vinagre 192.168.69.180:5900


Cloud image

Para hacer la prueba nos bajamos la versión cloud de Ubuntu:

Podemos ver la imagen descargada:

vm img

DATASTORE           FILENAME
default             ubuntu-22.04-server-cloudimg-amd64.img

Creamos la máquina virtual:

vm create -c 2 -m 2G -s 15G -t linux-zvol -i ubuntu-22.04-server-cloudimg-amd64.img -C -k /home/kr0m/.ssh/id_rsa.pub ubuntu-cloud

Arrancamos la imagen:

vm start ubuntu-cloud

El problema de estas imágenes es que el cloud-init de vm-bhyve está muy limitado tan solo nos deja meter las keys SSH, pero no asignar un ip estática, por lo tanto la VM obtendrá una por DHCP que tendremos que descubrir:

vm info ubuntu-cloud|grep 'fixed-mac-address'

fixed-mac-address: 58:9c:fc:02:a4:96
fping -ag 192.168.69.0/24
MAC=$(vm info ubuntu-cloud|grep 'fixed-mac-address'|tr -d " "|awk -F "fixed-mac-address:" '{print$2}')
arp -a |grep $MAC
? (192.168.69.225) at 58:9c:fc:02:a4:96 on em0 expires in 1110 seconds [ethernet]

Accedemos vía SSH ya que nuestra pubkey debe estar autorizada:

Asignamos password a los usuarios para poder acceder por consola vm-bhyve:

sudo su -l
passwd
passwd ubuntu

Ahora también podremos acceder por consola:

vm console ubuntu-cloud

Consultamos la información básica:

ubuntu@ubuntu-cloud:~$ uname -a

Linux ubuntu-cloud 5.15.0-83-generic #92-Ubuntu SMP Mon Aug 14 09:30:42 UTC 2023 x86_64 x86_64 x86_64 GNU/Linux

Cloud image: Provisioning

El soporte de cloud-init en vm-bhyve es muy limitado, tan solo permite autorizar las keys SSH, para superar estas limitaciones tendremos que generar los ficheros de cloud-init nosotros mismos.

Para configurar correctamente cloud-init debemos comprender primero el proceso de configuración que sigue:

  • cloud-init descubre el datasource y obtiene los siguientes ficheros de configuración:
    • meta-data(Ex: pre-network): Información sobre la instancia, machine ID, hostname.
    • network-config(Ex: pre-network): Configuración de red.
    • vendro-data(Ex: post-network): Optimizaciones de hardware, este fichero es opcional.
    • user-data(Ex: post-network): Usuarios, claves SSH, custom scripts, integración con sistemas como Puppet/Chef/Ansible.
  • cloud-init configura la red, si no existe configuración pedirá ip por DHCP.
  • cloud-init configura la instancia acorde a los ficheros vendor-data y user-data.

Cloud-init soporta varios datasources , en nuestro caso utilizaremos NoCloud que nos permite configurar máquinas virtuales a través de:

  • Sistema de ficheros local con volid: CIDATA.
  • Ficheros locales: SMBIOS serial number/Kernel command line.
  • Servidor IMDS(Requiere servidor DHCP): SMBIOS serial number/Kernel command line.

Cloud image: cloud-init/CIDATA

Cloud-init leerá estos ficheros si la máquina virtual dispone de una unidad de cd-rom con el volid: CIDATA.

Instalamos cdrkit-genisoimage:

pkg install cdrkit-genisoimage

Creamos la máquina virtual:

vm create -c 2 -m 2G -s 15G -t linux-zvol -i ubuntu-22.04-server-cloudimg-amd64.img ubuntu-cloud2

Creamos los ficheros de cloud-init :

mkdir /zroot/vm/ubuntu-cloud2/.cloud-init
cd /zroot/vm/ubuntu-cloud2/

cat << EOF > .cloud-init/meta-data
instance-id: Alfaexploit/Ubuntu-CloudImg2
local-hostname: Ubuntu-CloudImg2
EOF
PASS_HASH=$(python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))')
cat << EOF >  .cloud-init/user-data
#cloud-config
users:
# Following entry create user1 and assigns password specified in plain text.
# Please not use of plain text password is not recommended from security best
# practises standpoint
  - name: user1
    groups: sudo
    sudo: ALL=(ALL) NOPASSWD:ALL
    plain_text_passwd: PASSWORD
    lock_passwd: false
# Following entry creates user2 and attaches a hashed passwd to the user. Hashed
# passwords can be generated with:
# python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
  - name: user2
    passwd: $PASS_HASH
    lock_passwd: false
# Following entry creates user3, disables password based login and enables an SSH public key
  - name: user3
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow
    lock_passwd: true
# Install some basic software
packages:
  - htop
  - screen
EOF
cat << EOF > .cloud-init/network-config
version: 2
ethernets:
  enp0s5:
    addresses: [192.168.69.30/24]
    routes:
      - to: default
        via: 192.168.69.200
    nameservers:
      addresses: [1.1.1.1,8.8.8.8]
EOF

Generamos la imagen semilla con el volid: cidata.

genisoimage -output seed.iso -volid cidata -joliet -rock .cloud-init/*

Añadimos la imagen cd-rom a la máquina virtual:

cat << EOF >> /zroot/vm/ubuntu-cloud2/ubuntu-cloud2.conf
disk1_type="ahci-cd"
disk1_dev="custom"
disk1_name="/zroot/vm/ubuntu-cloud2/seed.iso"
EOF

Arrancamos la máquina virtual, esta leerá la imagen semilla e interpretará la configuración cloud-init:

vm start ubuntu-cloud2
vm console ubuntu-cloud2

Veremos como configura la red:

[   10.606424] cloud-init[649]: Cloud-init v. 23.2.2-0ubuntu0~22.04.1 running 'init' at Wed, 27 Sep 2023 20:13:02 +0000. Up 10.59 seconds.
[   10.615655] cloud-init[649]: ci-info: ++++++++++++++++++++++++++++++++++++++++++++++Net device info++++++++++++++++++++++++++++++++++++++++++++++
[   10.616879] cloud-init[649]: ci-info: +--------+------+--------------------------------------------+---------------+--------+-------------------+
[   10.618050] cloud-init[649]: ci-info: | Device |  Up  |                  Address                   |      Mask     | Scope  |     Hw-Address    |
[   10.619210] cloud-init[649]: ci-info: +--------+------+--------------------------------------------+---------------+--------+-------------------+
[   10.620362] cloud-init[649]: ci-info: | enp0s5 | True |               192.168.69.30                | 255.255.255.0 | global | 58:9c:fc:02:5c:df |
[   10.621520] cloud-init[649]: ci-info: | enp0s5 | True | 2a0c:5a86:d204:ca00:5a9c:fcff:fe02:5cdf/64 |       .       | global | 58:9c:fc:02:5c:df |
[   10.622819] cloud-init[649]: ci-info: | enp0s5 | True |        fe80::5a9c:fcff:fe02:5cdf/64        |       .       |  link  | 58:9c:fc:02:5c:df |
[   10.624148] cloud-init[649]: ci-info: |   lo   | True |                 127.0.0.1                  |   255.0.0.0   |  host  |         .         |
[   10.625316] cloud-init[649]: ci-info: |   lo   | True |                  ::1/128                   |       .       |  host  |         .         |
[   10.626463] cloud-init[649]: ci-info: +--------+------+--------------------------------------------+---------------+--------+-------------------+

Podemos ver que ha generado los usuarios tal y como le hemos indicado.

Usuario user1 local ya que por defecto no acepta login SSH mediante password y no hemos autorizado ninguna key:

Ubuntu-CloudImg2 login: user1
$ id
uid=1000(user1) gid=1000(user1) groups=1000(user1),27(sudo)

Usuario user2 local ya que por defecto no acepta login SSH mediante password y no hemos autorizado ninguna key, pero el password se había definido mediante hash a diferencia de user1:

Ubuntu-CloudImg2 login: user2
$ id
uid=1001(user2) gid=1001(user2) groups=1001(user2)

Usuario user3 con acceso solo por SSH, sin login local:

$ id
uid=1002(user3) gid=1002(user3) groups=1002(user3)

Cloud image: cloud-init/Ficheros locales

Los pasos a seguir para utilizar ficheros locales son:

Mediante SMBIOS serial number:

Creamos los ficheros de cloud-init :

mkdir ~/ubuntu-cloud3
cd ~/ubuntu-cloud3

Por alguna razón no es posible dejar la configuración de red en el fichero network-config como hemos hecho con la ISO semilla si no que hay que meterla en el meta-data:

cat << EOF > ~/ubuntu-cloud3/meta-data
instance-id: Alfaexploit/Ubuntu-CloudImg3
local-hostname: Ubuntu-CloudImg3
network-interfaces: |
  iface enp0s5 inet static
  address 192.168.69.30
  network 192.168.69.0
  netmask 255.255.255.0
  broadcast 192.168.69.255
  gateway 192.168.69.200  
EOF
PASS_HASH=$(python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))')
cat << EOF > ~/ubuntu-cloud3/user-data
#cloud-config
users:
# Following entry create user1 and assigns password specified in plain text.
# Please not use of plain text password is not recommended from security best
# practises standpoint
  - name: user1
    groups: sudo
    sudo: ALL=(ALL) NOPASSWD:ALL
    plain_text_passwd: PASSWORD
    lock_passwd: false
# Following entry creates user2 and attaches a hashed passwd to the user. Hashed
# passwords can be generated with:
# python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
  - name: user2
    passwd: $PASS_HASH
    lock_passwd: false
# Following entry creates user3, disables password based login and enables an SSH public key
  - name: user3
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow
    lock_passwd: true
# Install some basic software
packages:
  - htop
  - screen
EOF

Convertimos la imagen a formato RAW para poder montarla:

qemu-img convert /zroot/vm/.img/ubuntu-22.04-server-cloudimg-amd64.img ~/ubuntu-22.04-server-cloudimg-amd64-ubuntu-cloud3.raw

Generamos un memory disk a partir de la imagen:

mdconfig -a -t vnode -f ~/ubuntu-22.04-server-cloudimg-amd64-ubuntu-cloud3.raw -u 0

Podemos ver las particiones que contiene:

gpart show /dev/md0

=>     34  4612029  md0  GPT  (2.2G)
       34     2014       - free -  (1.0M)
     2048     8192   14  bios-boot  (4.0M)
    10240   217088   15  efi  (106M)
   227328  4384735    1  linux-data  (2.1G)

Montamos la / del sistema Linux:

mount -t ext2fs -o rw /dev/md0p1 /mnt/aux/

Copiamos los ficheros:

cp -r ~/ubuntu-cloud3 /mnt/aux/

Desmontamos:

umount /mnt/aux

Eliminamos el memory disk:

mdconfig -d -u 0

Creamos la máquina virtual a partir de la imagen modificada:

vm create -c 2 -m 2G -s 15G -t linux-zvol -i ~/ubuntu-22.04-server-cloudimg-amd64-ubuntu-cloud3.raw ubuntu-cloud3

vm-bhyve nos permite pasarle argumentos directamente a Bhyve mediante el parámetro bhyve_options , a su vez Bhyve permite configurar variables desde CLI: -o .
La variable que necesitamos modificar es system.serial_number , identificar la variable correcta ha sido un proceso de prueba/error.

Añadimos las opciones Bhyve:

cat << EOF >> /zroot/vm/ubuntu-cloud3/ubuntu-cloud3.conf
bhyve_options="-o system.serial_number=ds=nocloud;s=file://ubuntu-cloud3/"
EOF

Arrancamos la máquina virtual:

vm start ubuntu-cloud3

Los usuarios se habrán generado tal y como indican los ficheros de configuración, además podemos ver la información inyectada en SMBIOS:

dmidecode

System Information
	Manufacturer: FreeBSD
	Product Name: BHYVE
	Version: 1.0
	Serial Number: ds=nocloud;s=file://ubuntu-cloud3/
	UUID: b7ff0817-5d72-11ee-b808-3497f636bf45
	Wake-up Type: Power Switch
	SKU Number: None
	Family: Virtual Machine

Mediante parámetros del kernel:

Creamos los ficheros de cloud-init :

mkdir ~/ubuntu-cloud4
cd ~/ubuntu-cloud4

Por alguna razón no es posible dejar la configuración de red en el fichero network-config como hemos hecho con la ISO `semilla`` si no que hay que meterla en el meta-data:

cat << EOF > ~/ubuntu-cloud4/meta-data
instance-id: Alfaexploit/Ubuntu-CloudImg4
local-hostname: Ubuntu-CloudImg4
network-interfaces: |
  iface enp0s5 inet static
  address 192.168.69.30
  network 192.168.69.0
  netmask 255.255.255.0
  broadcast 192.168.69.255
  gateway 192.168.69.200  
EOF
PASS_HASH=$(python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))')
cat << EOF > ~/ubuntu-cloud4/user-data
#cloud-config
users:
# Following entry create user1 and assigns password specified in plain text.
# Please not use of plain text password is not recommended from security best
# practises standpoint
  - name: user1
    groups: sudo
    sudo: ALL=(ALL) NOPASSWD:ALL
    plain_text_passwd: PASSWORD
    lock_passwd: false
# Following entry creates user2 and attaches a hashed passwd to the user. Hashed
# passwords can be generated with:
# python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
  - name: user2
    passwd: $PASS_HASH
    lock_passwd: false
# Following entry creates user3, disables password based login and enables an SSH public key
  - name: user3
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow
    lock_passwd: true
# Install some basic software
packages:
  - htop
  - screen
EOF

Convertimos la imagen a formato RAW para poder montarla:

qemu-img convert /zroot/vm/.img/ubuntu-22.04-server-cloudimg-amd64.img ~/ubuntu-22.04-server-cloudimg-amd64-ubuntu-cloud4.raw

Generamos un memory disk a partir de la imagen:

mdconfig -a -t vnode -f ~/ubuntu-22.04-server-cloudimg-amd64-ubuntu-cloud4.raw -u 0

Podemos ver las particiones que contiene:

gpart show /dev/md0

=>     34  4612029  md0  GPT  (2.2G)
       34     2014       - free -  (1.0M)
     2048     8192   14  bios-boot  (4.0M)
    10240   217088   15  efi  (106M)
   227328  4384735    1  linux-data  (2.1G)

Montamos la / del sistema Linux:

mount -t ext2fs -o rw /dev/md0p1 /mnt/aux/

Copiamos los ficheros:

cp -r ~/ubuntu-cloud4 /mnt/aux/

Desmontamos:

umount /mnt/aux

Eliminamos el memory disk:

mdconfig -d -u 0

Creamos la máquina virtual a partir de la imagen modificada:

vm create -c 2 -m 2G -s 15G -t linux-zvol -i ~/ubuntu-22.04-server-cloudimg-amd64-ubuntu-cloud4.raw ubuntu-cloud4

Añadimos las opciones Bhyve:

cat << EOF >> /zroot/vm/ubuntu-cloud4/ubuntu-cloud4.conf
grub_run_partition="gpt1"
grub_run0="linux /boot/vmlinuz root=/dev/vda1 rw ds='nocloud;s=file://ubuntu-cloud4/'"
grub_run1="initrd /boot/initrd.img"
EOF

Arrancamos la máquina virtual:

vm start ubuntu-cloud4
vm console ubuntu-cloud4

Podemos ver que ha generado los usuarios tal y como le hemos indicado, además podemos consultar los parámetros recibidos por el kernel:

dmesg | grep 'Kernel command line'

[    0.038996] Kernel command line: console=ttyS0 BOOT_IMAGE=/boot/vmlinuz root=/dev/vda1 rw ds=nocloud;s=file://ubuntu-cloud4/

Cloud image: cloud-init/IMDS

La ventaja de utilizar un servidor IMDS(Instance Metadata Service) es que centralizaremos todos los ficheros de configuración, esto resulta útil para eliminar una máquina virtual en un nodo físico y redesplegarla en otro sin tener que copiar o regenerar los ficheros: meta-data/user-data/vendor-data.

Pero también impone una dependencia de los servidores DHCP/WebServer que pueden llegar a fallar(incluso montándolos en alta disponibilidad) situación que debería evitarse a toda costa cuando estamos hablando de servidores en producción, yo personalmente recomiendo utilizar la ISO semilla ya que se puede scriptar fácilmente y no tiene dependencias.

Para poder utilizar un servidor IMDS debemos proceder del siguiente modo:

Mediante SMBIOS serial number:

Creamos la máquina virtual:

vm create -c 2 -m 2G -s 15G -t linux-zvol -i ubuntu-22.04-server-cloudimg-amd64.img ubuntu-cloud5

Generamos los ficheros a servir:

mkdir -p ~/IMDS/ubuntu-cloud5
cd ~/IMDS/ubuntu-cloud5

cat << EOF > meta-data
instance-id: Alfaexploit/Ubuntu-CloudImg5
local-hostname: Ubuntu-CloudImg5
EOF
PASS_HASH=$(python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))')
cat << EOF > user-data
#cloud-config
users:
# Following entry create user1 and assigns password specified in plain text.
# Please not use of plain text password is not recommended from security best
# practises standpoint
  - name: user1
    groups: sudo
    sudo: ALL=(ALL) NOPASSWD:ALL
    plain_text_passwd: PASSWORD
    lock_passwd: false
# Following entry creates user2 and attaches a hashed passwd to the user. Hashed
# passwords can be generated with:
# python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
  - name: user2
    passwd: $PASS_HASH
    lock_passwd: false
# Following entry creates user3, disables password based login and enables an SSH public key
  - name: user3
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow
    lock_passwd: true
# Install some basic software
packages:
  - htop
  - screen
EOF
touch vendor-data

Arrancamos el servidor web:

cd ~/IMDS
python -m http.server 8000

vm-bhyve nos permite pasarle argumentos directamente a Bhyve mediante el parámetro bhyve_options , a su vez Bhyve permite configurar variables desde CLI: -o .
La variable que necesitamos modificar es system.serial_number , identificar la variable correcta ha sido un proceso de prueba/error.

Añadimos las opciones Bhyve:

cat << EOF >> /zroot/vm/ubuntu-cloud5/ubuntu-cloud5.conf
bhyve_options="-o system.serial_number=ds=nocloud;s=http://192.168.69.4:8000/ubuntu-cloud5/"
EOF

Arrancamos la máquina virtual:

vm start ubuntu-cloud5

En la consola del servidor web veremos las peticiones recibidas:

Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:192.168.69.233 - - [27/Sep/2023 22:33:36] "GET /ubuntu-cloud5/meta-data HTTP/1.1" 200 -
::ffff:192.168.69.233 - - [27/Sep/2023 22:33:36] "GET /ubuntu-cloud5/user-data HTTP/1.1" 200 -
::ffff:192.168.69.233 - - [27/Sep/2023 22:33:36] "GET /ubuntu-cloud5/vendor-data HTTP/1.1" 200 -

Los usuarios se habrán generado tal y como indican los ficheros de configuración, además podemos ver la información inyectada en SMBIOS:

dmidecode

System Information
	Manufacturer: FreeBSD
	Product Name: BHYVE
	Version: 1.0
	Serial Number: ds=nocloud;s=http://192.168.69.4:8000/ubuntu-cloud5/
	UUID: 9ce6a5f8-5d74-11ee-b808-3497f636bf45
	Wake-up Type: Power Switch
	SKU Number: None
	Family: Virtual Machine

Mediante parámetros del kernel:

Creamos la máquina virtual:

vm create -c 2 -m 2G -s 15G -t linux-zvol -i ubuntu-22.04-server-cloudimg-amd64.img ubuntu-cloud6

Generamos los ficheros a servir:

mkdir -p ~/IMDS/ubuntu-cloud6
cd ~/IMDS/ubuntu-cloud6

cat << EOF > meta-data
instance-id: Alfaexploit/Ubuntu-CloudImg6
local-hostname: Ubuntu-CloudImg6
EOF
PASS_HASH=$(python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))')
cat << EOF > user-data
#cloud-config
users:
# Following entry create user1 and assigns password specified in plain text.
# Please not use of plain text password is not recommended from security best
# practises standpoint
  - name: user1
    groups: sudo
    sudo: ALL=(ALL) NOPASSWD:ALL
    plain_text_passwd: PASSWORD
    lock_passwd: false
# Following entry creates user2 and attaches a hashed passwd to the user. Hashed
# passwords can be generated with:
# python -c 'import crypt,getpass; print(crypt.crypt(getpass.getpass(), crypt.mksalt(crypt.METHOD_SHA512)))'
  - name: user2
    passwd: $PASS_HASH
    lock_passwd: false
# Following entry creates user3, disables password based login and enables an SSH public key
  - name: user3
    ssh_authorized_keys:
      - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCiF8zV98gaV87zVDwhZ8IDxUXs3Xq0NKVT1HY2t3XMjMU6/Sv6WeMeKQtJn/Py5PPXhYkOQ92in5+70WE6fQPt0shH6Hh4ZwY8lKDboLh96XLiCsxZc/IMq0h86QeT2E7FjAXcGo/2gziSYvbnp2Z+JAe5uBm4HtV3419mlEmaRAXGtjQxqcnhoUsLj4gD53ak1H/KUro+GArVDPUoMdSsqxvL0BHGbbcFOIaA3H3GwnGp09bHBNd0fR+Ip6CHRlRIu9oohezOAU+/3ul+FFbZuVlJ8zssaaFOzxjwEhD/2Ghsae3+z6tkrbhEOYN7HvBDejK9WySeI0+bMRqfSaID kr0m@DirtyCow
    lock_passwd: true
# Install some basic software
packages:
  - htop
  - screen
EOF
touch vendor-data

Arrancamos el servidor web:

cd ~/IMDS
python -m http.server 8000

Añadimos las opciones Bhyve:

cat << EOF >> /zroot/vm/ubuntu-cloud6/ubuntu-cloud6.conf
grub_run_partition="gpt1"
grub_run0="linux /boot/vmlinuz root=/dev/vda1 rw ds='nocloud;s=http://192.168.69.4:8000/ubuntu-cloud6/'"
grub_run1="initrd /boot/initrd.img"
EOF

Arrancamos la máquina virtual:

vm start ubuntu-cloud6
vm console ubuntu-cloud6

Veremos en la consola del servidor web como llegan peticiones:

Serving HTTP on :: port 8000 (http://[::]:8000/) ...
::ffff:192.168.69.234 - - [27/Sep/2023 22:36:07] "GET /ubuntu-cloud6/meta-data HTTP/1.1" 200 -
::ffff:192.168.69.234 - - [27/Sep/2023 22:36:07] "GET /ubuntu-cloud6/user-data HTTP/1.1" 200 -
::ffff:192.168.69.234 - - [27/Sep/2023 22:36:07] "GET /ubuntu-cloud6/vendor-data HTTP/1.1" 200 -

Podemos ver que ha generado los usuarios tal y como le hemos indicado, además podemos consultar los parámetros recibidos por el kernel:

dmesg | grep 'Kernel command line'

[    0.039752] Kernel command line: console=ttyS0 BOOT_IMAGE=/boot/vmlinuz root=/dev/vda1 rw ds=nocloud;s=http://192.168.69.4:8000/ubuntu-cloud6/

Firewall

Lo mas sencillo es deshabilitar el filtrado de tráfico en todos los bridges del host padre y filtrar de forma específica dentro de cada VM:

vi /etc/sysctl.conf

net.inet.ip.forwarding=1       # Enable IP forwarding between interfaces
net.link.bridge.pfil_onlyip=0  # Only pass IP packets when pfil is enabled
net.link.bridge.pfil_bridge=0  # Packet filter on the bridge interface
net.link.bridge.pfil_member=0  # Packet filter on the member interface

Wifi

Si queremos conectar las VMs por wifi se complica un poco ya que tendremos que NATear una red interna de VMs.

Primero habilitamos el forwarding:

sysctl net.inet.ip.forwarding=1
sysctl net.inet6.ip6.forwarding=1

Nos aseguramos de que si reiniciamos, siga habilitado:

sysrc gateway_enable=YES
sysrc ipv6_gateway_enable=YES

Definimos las reglas de PF / PF_NAT :

vi /etc/pf.conf

ext_if = "wlan0"
int_if = "vm-public:"
localnet = "10.0.0.0/8"

set block-policy return
scrub in on $ext_if all fragment reassemble
set skip on lo

nat on $ext_if from {$localnet} to any -> ($ext_if)

antispoof for $ext_if inet

block log all
pass out
pass in

Habilitamos el servicio PF:

sysrc pf_enable=YES
sysrc pf_rules="/etc/pf.conf"
sysrc pflog_enable=YES
sysrc pflog_logfile="/var/log/pflog"

Arrancamos PF:

service pf start

Añadimos la interfaz wifi al bridge:

vm switch add public wlan0

Configuramos una ip de la red interna al bridge, esta actuará como gateway de las VMs:

vm switch address public 10.0.0.1/8

Comprobamos que todo esté en orden:

vm switch list

NAME    TYPE      IFACE      ADDRESS     PRIVATE  MTU  VLAN  PORTS
public  standard  vm-public  10.0.0.1/8  no       -    -     wlan0

Creamos la VM:

vm create -c 2 -m 4G -s 25G -t linux-zvol ubuntu-server

Instalamos el sistema operativo:

vm install ubuntu-server ubuntu-22.04.3-live-server-amd64.iso

Seguimos los pasos del instalador:

vm console ubuntu-server

La configuración de red será la siguiente:

IP: 10.0.0.2
GW: 10.0.0.1
DNS: 8.8.8.8,1.1.1.1

Cuando termine la instalación, reiniciamos y accedemos a la VM:

vm console ubuntu-server

GRUB no arranca ya que /boot no se instaló en: hd0, partition 1
https://github-wiki-see.page/m/churchers/vm-bhyve/wiki/Configuring-Grub-Guests

Desde la interfaz de Grub vamos a averiguar que partición es la correcta:

ls

(hd0) (hd0,gpt3) (hd0,gpt2) (hd0,gpt1) (host) (lvm/ubuntu--vg-ubuntu--lv)

Podemos listar el contenido de la partición:

ls (hd0,gpt2)/

lost+found/ config-5.15.0-84-generic initrd.img.old vmlinuz.old grub/ System.ma
p-5.15.0-84-generic vmlinuz-5.15.0-84-generic initrd.img vmlinuz initrd.img-5.1
5.0-84-generic

Ahora que ya sabemos la partición donde está el kernel, la partición raíz y la localización del initramfs, procedemos con el arranque:

linux (hd0,gpt2)/vmlinuz root=/dev/mapper/ubuntu–vg-ubuntu–lv
initrd (hd0,gpt2)/initrd.img
boot

Si arranca correctamente apagamos el sistema operativo para configurarlo desde Bhyve sin tener que estar tocando el Grub en cada arranque:

vm configure ubuntu-server

grub_run_partition="gpt2"
grub_run0="linux /vmlinuz root=/dev/mapper/ubuntu--vg-ubuntu--lv rw"
grub_run1="initrd /initrd.img"

Ya podemos acceder sin problemas por SSH:

Por supuesto la VM saldrá NATeada por la interfaz wifi.


Troubleshooting

Si estamos haciendo pruebas y cerrando las conexiones al puerto serie de forma incorrecta(cerrando el terminal), veremos que se acumulan procesos ‘cu’ y cuando conectamos de nuevo por puerto serie aparecerán carácteres extraños, para solventarlo mataremos los ‘cus’ huérfanos, yo suelo matarlos todos y reconectar:

killall cu
vm console VM_NAME

Si queremos probar la configuración de cloud-init con Qemu tal como indica la documentación oficial , podemos hacerlo:

pkg install qemu qemu-tools
qemu-system-x86_64 -net nic -net user -m 2048 -nographic -hda /zroot/vm/.img/ubuntu-22.04-server-cloudimg-amd64.img -smbios type=1,serial=ds=‘nocloud;s=http://192.168.69.4:8000/ubuntu-cloud3/’

Si tenemos algún problema con el arranque o con los parámetros pasados a Bhyve, podemos chequear los siguientes ficheros de log:

cat /zroot/vm/ubuntu-cloud3/vm-bhyve.log
cat /zroot/vm/ubuntu-cloud3/grub.cfg

Si queremos debugear todavía mas el arranque, habilitamos el debug:

vm configure VM_NAME

debug="yes"
vm start ubuntu-cloud3
cat /zroot/vm/ubuntu-cloud3/bhyve.log

Si tenemos algún problema con la imagen semilla podemos acceder a su contenido mediante los siguientes comandos:

mkdir decompressed
cp seed.iso decompressed
cd decompressed
tar xzf seed.iso

Podemos comprobar que cloud-init se haya ejecutado correctamente:

cloud-init status –wait
cloud-init query userdata
cloud-init schema –system

También podemos re-ejecutar cloud-init manualmente:

cloud-init clean
cloud-init -d init
cloud-init -d modules --mode final

En algunas ocasiones las máquinas virtuales no se paran, le damos botonazo:

vm poweroff kali00

En algunas ocasiones las máquinas virtuales terminan en estado “Locked”:

vm list

NAME    DATASTORE  LOADER  CPU  MEMORY  VNC           AUTOSTART  STATE
kali00  default    uefi    1    2048M   0.0.0.0:5900  No         Locked (odyssey.alfaexploit.com)

Para solventarlo primero debemos eliminar el fichero run.lock:

rm /zroot/vm/kali00/run.lock

Ahora aparecerá como “Stopped”:

vm list

NAME    DATASTORE  LOADER  CPU  MEMORY  VNC  AUTOSTART  STATE
kali00  default    uefi    1    2048M   -    No         Stopped

Si intentamos eliminarla y nos causa problemas, tendremos que liberar los recursos desde bhyvectl:

vm destroy kali00

/usr/local/sbin/vm: WARNING: kali00 appears to be running locally (vmm exists)

Liberamos los recursos:

bhyvectl --vm kali00 --destroy

La función de bhyvectl --destroy es:

After the VM has been shutdown, its resources can be reclaimed

Incluso con los recursos liberados puede que el dataset ZFS se resista:

vm list

NAME    DATASTORE  LOADER  CPU  MEMORY  VNC  AUTOSTART  STATE
kali00  default    uefi    1    2048M   -    No         Stopped
vm destroy kali00
Are you sure you want to completely remove this virtual machine (y/n)? y
/usr/local/sbin/vm: ERROR: failed to destroy ZFS dataset zroot/vm/kali00

En tal caso lo eliminamos manualmente:

zfs destroy -r zroot/vm/kali00

Finalmente eliminamos la máquina virtual:

vm destroy kali00

Are you sure you want to completely remove this virtual machine (y/n)? y

Comprobamos que no quede rastro de ella:

vm list

NAME  DATASTORE  LOADER  CPU  MEMORY  VNC  AUTOSTART  STATE
Si te ha gustado el artículo puedes invitarme a un RedBull aquí