Skip to content

Commit

Permalink
add custom ipxe firmware building support
Browse files Browse the repository at this point in the history
This commit adds the following:

  - Support for detecting iPXE certificates
  - When iPXE certs are detected, the iPXE services
    and configs will be advertised with https addresses
  - Enforce chainloading of the iPXE firmware provided by
    the user even if the client machine comes with an iPXE
    firmware by default
  - Adds modular iPXE apache2 config file to enable separate
    iPXE specific virtual host (port) where TLS is enforced
  - Custom iPXE config file template has been added thus Ironic can
    generate node specific iPXE config files that use the custom iPXE
    firmware
  - A set of environment variable that control the new iPXE related
    features

Signed-off-by: Adam Rozman <adam.rozman@est.tech>
  • Loading branch information
Rozzii committed Nov 20, 2023
1 parent 0bc1496 commit 00e3478
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 4 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ COPY --from=ironic-builder /tmp/ipxe/src/bin/undionly.kpxe /tmp/ipxe/src/bin-x86
COPY --from=ironic-builder /tmp/esp.img /tmp/uefi_esp.img

COPY ironic-config/ironic.conf.j2 /etc/ironic/
COPY ironic-config/inspector.ipxe.j2 ironic-config/httpd-ironic-api.conf.j2 /tmp/
COPY ironic-config/inspector.ipxe.j2 ironic-config/httpd-ironic-api.conf.j2 ironic-config/ipxe_config.template /tmp/

# DNSMASQ
COPY ironic-config/dnsmasq.conf.j2 /etc/
Expand All @@ -57,6 +57,7 @@ COPY ironic-config/dnsmasq.conf.j2 /etc/
COPY ironic-config/httpd.conf.j2 /etc/httpd/conf/
COPY ironic-config/httpd-modules.conf /etc/httpd/conf.modules.d/
COPY ironic-config/apache2-vmedia.conf.j2 /etc/httpd-vmedia.conf.j2
COPY ironic-config/apache2-ipxe.conf.j2 /etc/httpd-ipxe.conf.j2

# IRONIC-INSPECTOR #
RUN mkdir -p /var/lib/ironic /var/lib/ironic-inspector && \
Expand Down
35 changes: 35 additions & 0 deletions ironic-config/apache2-ipxe.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
Listen {{ env.IPXE_TLS_PORT }}

<VirtualHost *:{{ env.IPXE_TLS_PORT }}>
ErrorLog /dev/stderr
LogLevel debug
CustomLog /dev/stdout combined

SSLEngine on
SSLProtocol {{ env.IPXE_SSL_PROTOCOL }}
SSLCertificateFile {{ env.IPXE_CERT_FILE }}
SSLCertificateKeyFile {{ env.IPXE_KEY_FILE }}

<Directory "/shared/html">
Order Allow,Deny
Allow from all
</Directory>
<Directory "/shared/html/(redfish|ilo|images)/">
Order Deny,Allow
Deny from all
</Directory>
</VirtualHost>

<Location ~ "^/grub.*/">
SSLRequireSSL
</Location>
<Location ~ "^/pxelinux.cfg/">
SSLRequireSSL
</Location>
<Location ~ "^/.*\.conf/">
SSLRequireSSL
</Location>
<Location ~ "^/(([0-9]|[a-z]).*-){4}([0-9]|[a-z]).*/">
SSLRequireSSL
</Location>

12 changes: 11 additions & 1 deletion ironic-config/dnsmasq.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,23 @@ dhcp-option=option{% if ":" in env["DNS_IP"] %}6{% endif %}:dns-server,{{ env["D
# IPv4 Configuration:
dhcp-match=ipxe,175
# Client is already running iPXE; move to next stage of chainloading
{%- if env.IPXE_TLS_SETUP == "true" %}
# iPXE with (U)EFI
dhcp-boot=tag:efi,tag:ipxe,http://{{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }}/custom-ipxe/snponly.efi
# iPXE with BIOS
dhcp-boot=tag:ipxe,http://{{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }}/custom-ipxe/undionly.kpxe
{% else %}
dhcp-boot=tag:ipxe,http://{{ env.IRONIC_URL_HOST }}:{{ env.HTTP_PORT }}/boot.ipxe
{% endif %}

# Note: Need to test EFI booting
dhcp-match=set:efi,option:client-arch,7
dhcp-match=set:efi,option:client-arch,9
dhcp-match=set:efi,option:client-arch,11
# Client is PXE booting over EFI without iPXE ROM; send EFI version of iPXE chainloader
# Client is PXE booting over EFI without iPXE ROM; send EFI version of iPXE chainloader do the same also if iPXE ROM boots but TLS is enabled
{%- if env.IPXE_TLS_SETUP == "true" %}
dhcp-boot=tag:efi,tag:ipxe,snponly.efi
{% endif %}
dhcp-boot=tag:efi,tag:!ipxe,snponly.efi

# Client is running PXE over BIOS; send BIOS version of iPXE chainloader
Expand Down
81 changes: 81 additions & 0 deletions ironic-config/ipxe_config.template
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
#!ipxe

set attempts:int32 10
set i:int32 0

goto deploy

:deploy
imgfree
{%- if pxe_options.deployment_aki_path %}
{%- set aki_path_https_elements = pxe_options.deployment_aki_path.split(':') %}
{%- set aki_port_and_path = aki_path_https_elements[2].split('/') %}
{%- set aki_afterport = aki_port_and_path[1:]|join('/') %}
{%- set aki_path_https = ['https:', aki_path_https_elements[1], ':8084/', aki_afterport]|join %}
{%- endif %}
{%- if pxe_options.deployment_ari_path %}
{%- set ari_path_https_elements = pxe_options.deployment_ari_path.split(':') %}
{%- set ari_port_and_path = ari_path_https_elements[2].split('/') %}
{%- set ari_afterport = ari_port_and_path[1:]|join('/') %}
{%- set ari_path_https = ['https:', ari_path_https_elements[1], ':8084/', ari_afterport]|join %}
{%- endif %}
kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ aki_path_https }} selinux=0 troubleshoot=0 text {{ pxe_options.pxe_append_params|default("", true) }} BOOTIF=${mac} initrd={{ pxe_options.initrd_filename|default("deploy_ramdisk", true) }} || goto retry

initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ ari_path_https }} || goto retry
boot

:retry
iseq ${i} ${attempts} && goto fail ||
inc i
echo No response, retrying in ${i} seconds.
sleep ${i}
goto deploy

:fail
echo Failed to get a response after ${attempts} attempts
echo Powering off in 30 seconds.
sleep 30
poweroff

:boot_anaconda
imgfree
kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ aki_path_https }} text {{ pxe_options.pxe_append_params|default("", true) }} inst.ks={{ pxe_options.ks_cfg_url }} {% if pxe_options.repo_url %}inst.repo={{ pxe_options.repo_url }}{% else %}inst.stage2={{ pxe_options.stage2_url }}{% endif %} initrd=ramdisk || goto boot_anaconda
initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ ari_path_https }} || goto boot_anaconda
boot

:boot_ramdisk
imgfree
{%- if pxe_options.boot_iso_url %}
sanboot {{ pxe_options.boot_iso_url }}
{%- else %}
kernel {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ aki_path_https }} root=/dev/ram0 text {{ pxe_options.pxe_append_params|default("", true) }} {{ pxe_options.ramdisk_opts|default('', true) }} initrd=ramdisk || goto boot_ramdisk
initrd {% if pxe_options.ipxe_timeout > 0 %}--timeout {{ pxe_options.ipxe_timeout }} {% endif %}{{ ari_path_https }} || goto boot_ramdisk
boot
{%- endif %}

{%- if pxe_options.boot_from_volume %}

:boot_iscsi
imgfree
{% if pxe_options.username %}set username {{ pxe_options.username }}{% endif %}
{% if pxe_options.password %}set password {{ pxe_options.password }}{% endif %}
{% if pxe_options.iscsi_initiator_iqn %}set initiator-iqn {{ pxe_options.iscsi_initiator_iqn }}{% endif %}
sanhook --drive 0x80 {{ pxe_options.iscsi_boot_url }} || goto fail_iscsi_retry
{%- if pxe_options.iscsi_volumes %}{% for i, volume in enumerate(pxe_options.iscsi_volumes) %}
set username {{ volume.username }}
set password {{ volume.password }}
{%- set drive_id = 129 + i %}
sanhook --drive {{ '0x%x' % drive_id }} {{ volume.url }} || goto fail_iscsi_retry
{%- endfor %}{% endif %}
{% if pxe_options.iscsi_volumes %}set username {{ pxe_options.username }}{% endif %}
{% if pxe_options.iscsi_volumes %}set password {{ pxe_options.password }}{% endif %}
sanboot --no-describe || goto fail_iscsi_retry

:fail_iscsi_retry
echo Failed to attach iSCSI volume(s), retrying in 10 seconds.
sleep 10
goto boot_iscsi
{%- endif %}

:boot_whole_disk
sanboot --no-describe || exit 0
2 changes: 1 addition & 1 deletion ironic-config/ironic.conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ ramdisk_image_download_source = {{ env.IRONIC_BOOT_ISO_SOURCE }}
external_http_url = {{ env.IRONIC_EXTERNAL_HTTP_URL }}
{% elif env.IRONIC_VMEDIA_TLS_SETUP == "true" %}
external_http_url = https://{{ env.IRONIC_URL_HOST }}:{{ env.VMEDIA_TLS_PORT }}
{% endif %}
{% if env.IRONIC_EXTERNAL_CALLBACK_URL %}
external_callback_url = {{ env.IRONIC_EXTERNAL_CALLBACK_URL }}
{% endif %}
Expand Down Expand Up @@ -208,6 +207,7 @@ kernel_append_params = nofb nomodeset vga=normal ipa-insecure=1 {% if env.IRONIC
enable_netboot_fallback = true
# Enable the fallback path to ironic-inspector
ipxe_fallback_script = inspector.ipxe
ipxe_config_template = /tmp/ipxe_config.template

[redfish]
use_swift = false
Expand Down
1 change: 1 addition & 0 deletions scripts/ironic-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ IRONIC_IP="${IRONIC_IP:-}"
PROVISIONING_INTERFACE="${PROVISIONING_INTERFACE:-}"
PROVISIONING_IP="${PROVISIONING_IP:-}"
PROVISIONING_MACS="${PROVISIONING_MACS:-}"
IPXE_CUSTOM_FIRMWARE_DIR="${IPXE_CUSTOM_FIRMWARE_DIR:-/shared/custom_ipxe_firmware}"

get_provisioning_interface()
{
Expand Down
10 changes: 9 additions & 1 deletion scripts/rundnsmasq
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ set -eux

# shellcheck disable=SC1091
. /bin/ironic-common.sh
# shellcheck disable=SC1091
. /bin/tls-common.sh

export HTTP_PORT=${HTTP_PORT:-80}
DNSMASQ_EXCEPT_INTERFACE=${DNSMASQ_EXCEPT_INTERFACE:-lo}
Expand All @@ -19,7 +21,13 @@ mkdir -p /shared/html/images
mkdir -p /shared/html/pxelinux.cfg

# Copy files to shared mount
cp /tftpboot/undionly.kpxe /tftpboot/snponly.efi /shared/tftpboot
if [[ -r "${IPXE_CUSTOM_FIRMWARE_DIR}" ]]; then
cp "${IPXE_CUSTOM_FIRMWARE_DIR}/undionly.kpxe" \
"${IPXE_CUSTOM_FIRMWARE_DIR}/snponly.efi" \
"/shared/tftpboot"
else
cp /tftpboot/undionly.kpxe /tftpboot/snponly.efi /shared/tftpboot
fi

# Template and write dnsmasq.conf
# we template via /tmp as sed otherwise creates temp files in /etc directory
Expand Down
10 changes: 10 additions & 0 deletions scripts/runhttpd
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,16 @@ if [[ "$IRONIC_VMEDIA_TLS_SETUP" == "true" ]]; then
render_j2_config /etc/httpd-vmedia.conf.j2 /etc/httpd/conf.d/vmedia.conf
fi

# Render httpd TLS configuration for /shared/html
if [[ "$IPXE_TLS_SETUP" == "true" ]]; then
mkdir -p /shared/html/custom-ipxe
chmod 0777 /shared/html/custom-ipxe
render_j2_config "/etc/httpd-ipxe.conf.j2" "/etc/httpd/conf.d/ipxe.conf"
cp "${IPXE_CUSTOM_FIRMWARE_DIR}/undionly.kpxe" \
"${IPXE_CUSTOM_FIRMWARE_DIR}/snponly.efi" \
"/shared/html/custom-ipxe"
fi

# Set up inotify to kill the container (restart) whenever cert files for ironic inspector change
if [[ "$IRONIC_INSPECTOR_TLS_SETUP" == "true" ]] && [[ "${RESTART_CONTAINER_CERTIFICATE_UPDATED}" == "true" ]]; then
# shellcheck disable=SC2034
Expand Down
25 changes: 25 additions & 0 deletions scripts/tls-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export IRONIC_KEY_FILE=/certs/ironic/tls.key
export IRONIC_CACERT_FILE=/certs/ca/ironic/tls.crt
export IRONIC_INSECURE=${IRONIC_INSECURE:-false}
export IRONIC_SSL_PROTOCOL=${IRONIC_SSL_PROTOCOL:-"-ALL +TLSv1.2 +TLSv1.3"}
export IPXE_SSL_PROTOCOL=${IPXE_SSL_PROTOCOL:-"-ALL +TLSv1.2 +TLSv1.3"}
export IRONIC_VMEDIA_SSL_PROTOCOL=${IRONIC_VMEDIA_SSL_PROTOCOL:-"ALL"}

export IRONIC_INSPECTOR_CERT_FILE=/certs/ironic-inspector/tls.crt
Expand All @@ -15,14 +16,21 @@ export IRONIC_INSPECTOR_INSECURE=${IRONIC_INSPECTOR_INSECURE:-$IRONIC_INSECURE}
export IRONIC_VMEDIA_CERT_FILE=/certs/vmedia/tls.crt
export IRONIC_VMEDIA_KEY_FILE=/certs/vmedia/tls.key

export IPXE_CERT_FILE=/certs/ipxe/tls.crt
export IPXE_KEY_FILE=/certs/ipxe/tls.key

export RESTART_CONTAINER_CERTIFICATE_UPDATED=${RESTART_CONTAINER_CERTIFICATE_UPDATED:-"false"}

export MARIADB_CACERT_FILE=/certs/ca/mariadb/tls.crt

export IPXE_TLS_PORT="${IPXE_TLS_PORT:-8084}"

mkdir -p /certs/ironic
mkdir -p /certs/ironic-inspector
mkdir -p /certs/ca/ironic
mkdir -p /certs/ca/ironic-inspector
mkdir -p /certs/ipxe
mkdir -p /certs/vmedia

if [[ -f "$IRONIC_CERT_FILE" ]] && [[ ! -f "$IRONIC_KEY_FILE" ]]; then
echo "Missing TLS Certificate key file $IRONIC_KEY_FILE"
Expand Down Expand Up @@ -51,6 +59,15 @@ if [[ ! -f "$IRONIC_VMEDIA_CERT_FILE" ]] && [[ -f "$IRONIC_VMEDIA_KEY_FILE" ]];
exit 1
fi

if [[ -f "$IPXE_CERT_FILE" ]] && [[ ! -f "$IPXE_KEY_FILE" ]]; then
echo "Missing TLS Certificate key file $IRONIC_IPXE_CERT_FILE"
exit 1
fi
if [[ ! -f "$IPXE_CERT_FILE" ]] && [[ -f "$IRONIC_VMEDIA_KEY_FILE" ]]; then
echo "Missing TLS Certificate file $IRONIC_VMEDIA_CERT_FILE"
exit 1
fi

copy_atomic()
{
local src="$1"
Expand Down Expand Up @@ -94,6 +111,14 @@ else
export IRONIC_VMEDIA_TLS_SETUP="false"
fi

if [[ -f "$IPXE_CERT_FILE" ]]; then
export IPXE_SCHEME="https"
export IPXE_TLS_SETUP="true"
else
export IPXE_SCHEME="http"
export IPXE_TLS_SETUP="false"
fi

if [[ -f "$MARIADB_CACERT_FILE" ]]; then
export MARIADB_TLS_ENABLED="true"
else
Expand Down

0 comments on commit 00e3478

Please sign in to comment.