Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add grub support to allow secure boot #1

Merged
merged 5 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
ipxeblue
vendors
.podman-data
.podman-data
!tftp/.gitkeep
tftp/
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/ipxeblue -mod
############################
# STEP 2 build webui
############################
FROM node:lts-buster as builderui
FROM node:16-bullseye as builderui


WORKDIR /webui/
Expand Down
63 changes: 62 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ Supported environment variables
- `MINIO_SECURE`
- `MINIO_BUCKETNAME`
- default: `ipxeblue`
- `GRUB_SUPPORT_ENABLED`
- default: `False`
- `TFTP_ENABLED`
- default: `False`
- `DEFAULT_BOOTENTRY_NAME`
- default: ``

## DHCP or ipxe config for connection to ipxeblue

Expand All @@ -44,7 +50,7 @@ set crosscert http://ca.ipxe.org/auto
chain https://USERNAME:PASSWORD@FQDN/?asset=${asset}&buildarch=${buildarch}&hostname=${hostname}&mac=${mac:hexhyp}&ip=${ip}&manufacturer=${manufacturer}&platform=${platform}&product=${product}&serial=${serial}&uuid=${uuid}&version=${version}
```

For isc-dhcp-server
### For isc-dhcp-server

you need to set `iPXE-specific options` see https://ipxe.org/howto/dhcpd

Expand All @@ -64,6 +70,61 @@ you need to set `iPXE-specific options` see https://ipxe.org/howto/dhcpd
next-server 10.123.123.123;
```

### For kea-dhcp-server
```text
"Dhcp4": {
...
"option-def": [
{ "space": "dhcp4", "name": "ipxe-encap-opts", "code": 175, "type": "empty", "array": false, "record-types": "", "encapsulate": "ipxe" },
{ "space": "ipxe", "name": "crosscert", "code": 93, "type": "string" },
{ "space": "ipxe", "name": "username", "code": 190, "type": "string" },
{ "space": "ipxe", "name": "password", "code": 191, "type": "string" }
],
"client-classes": [
{
"name": "XClient_iPXE",
"test": "substring(option[77].hex,0,4) == 'iPXE'",
"boot-file-name": "ipxeblue.ipxe",
"option-data": [
{ "space": "dhcp4", "name": "ipxe-encap-opts", "code": 175 },
{ "space": "ipxe", "name": "crosscert", "data": "http://ca.ipxe.org/auto" },
{ "space": "ipxe", "name": "username", "data": "demo" },
{ "space": "ipxe", "name": "password", "data": "demo" }
]
},
{
"name": "UEFI-64",
"test": "substring(option[60].hex,0,20) == 'PXEClient:Arch:00007'",
"boot-file-name": "snponly.efi"
},
{
"name": "Legacy",
"test": "substring(option[60].hex,0,20) == 'PXEClient:Arch:00000'",
"boot-file-name": "undionly.kpxe"
}
],
"subnet4": [
{
...
"next-server": "10.123.123.123",
...
}
]
...
}
```

### Grub over PXE:
> Secure Boot supported with signed binaries

/srv/tftp/bootx64.efi (sha256sum 8c885fa9886ab668da267142c7226b8ce475e682b99e4f4afc1093c5f77ce275)
/srv/tftp/grubx64.efi (sha256sum d0d6d85f44a0ffe07d6a856ad5a1871850c31af17b7779086b0b9384785d5449)
/srv/tftp/grub/grub.cfg
```text
insmod http
source (http,192.168.32.7)/grub/
```

## screenshots

![Computer List](docs/images/computer-list.png?raw=true "Computer List")
Expand Down
132 changes: 132 additions & 0 deletions controllers/grub.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package controllers

import (
"bytes"
"fmt"
"github.com/aarnaud/ipxeblue/models"
"github.com/aarnaud/ipxeblue/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/jackc/pgtype"
"gorm.io/gorm"
"net/http"
"text/template"
)

func GrubScript(c *gin.Context) {
config := c.MustGet("config").(*utils.Config)

// basic check or reply with ipxe chain
_, uuidExist := c.GetQuery("uuid")
_, macExist := c.GetQuery("mac")
_, ipExist := c.GetQuery("ip")
if !uuidExist || !macExist || !ipExist {
baseURL := *config.BaseURL
// use the same scheme from request to generate URL
if schem := c.Request.Header.Get("X-Forwarded-Proto"); schem != "" {
baseURL.Scheme = schem
}
c.HTML(http.StatusOK, "grub_index.gohtml", gin.H{
"BaseURL": config.BaseURL.String(),
"Scheme": config.BaseURL.Scheme,
"Host": config.BaseURL.Host,
})
return
}

// process query params
db := c.MustGet("db").(*gorm.DB)
id, err := uuid.Parse(c.Query("uuid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{
"error": err.Error(),
})
return
}

mac := pgtype.Macaddr{}
err = mac.DecodeText(nil, []byte(c.Query("mac")))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{
"error": err.Error(),
})
return
}

ip := pgtype.Inet{}
err = ip.DecodeText(nil, []byte(c.Query("ip")))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotAcceptable, gin.H{
"error": err.Error(),
})
return
}

computer := updateOrCreateComputer(c, id, mac, ip)
// Add computer in gin context to use it in template function
c.Set("computer", &computer)

c.Header("Content-Type", "text/plain; charset=utf-8")
bootorder := models.Bootorder{}
result := db.Preload("Bootentry").Preload("Bootentry.Files").
Where("computer_uuid = ?", computer.Uuid).Order("bootorders.order").First(&bootorder)
if result.RowsAffected == 0 {
c.HTML(http.StatusOK, "grub_empty.gohtml", gin.H{})
return
}
bootentry := bootorder.Bootentry

// Create template name by the uuid
tpl := template.New(bootentry.Uuid.String())
// provide a func in the FuncMap which can access tpl to be able to look up templates
tpl.Funcs(utils.GetCustomFunctions(c, tpl))

tpl, err = tpl.Parse(bootentry.GrupScript)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}

writer := bytes.NewBuffer([]byte{})
writer.Write([]byte("set timeout=2\n"))
writer.Write([]byte(fmt.Sprintf("set prefix=(http,%s)\n", config.BaseURL.Host)))
writer.Write([]byte(fmt.Sprintf("echo 'Booting %s'\n", bootentry.Description)))

// if bootentry selected is menu load all bootentries as template
if bootentry.Name == "menu" {
bootentries := make([]models.Bootentry, 0)
db.Preload("Files").Where("name != 'menu'").Find(&bootentries)
for _, be := range bootentries {
// test if empty
tpl.New(be.Uuid.String()).Parse(be.GrupScript)
}
err = tpl.ExecuteTemplate(writer, bootentry.Uuid.String(), gin.H{
"Computer": computer,
"Bootentry": bootentry,
"Bootentries": bootentries,
})
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
} else {
err = tpl.ExecuteTemplate(writer, bootentry.Uuid.String(), bootentry)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"error": err.Error(),
})
return
}
}

// reset bootentry if not persistent
if !*bootentry.Persistent {
db.Model(&bootorder).Delete(&bootorder)
}

c.Data(http.StatusOK, "text/plain", writer.Bytes())
}
69 changes: 51 additions & 18 deletions controllers/ipxescript.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,34 +21,67 @@ import (
)

func updateOrCreateComputer(c *gin.Context, id uuid.UUID, mac pgtype.Macaddr, ip pgtype.Inet) models.Computer {
config := c.MustGet("config").(*utils.Config)
var computer models.Computer
var err error
db := c.MustGet("db").(*gorm.DB)

/*
a = asset
m = manufacturer
p = product
f = family
sn = serial
uuid = uuid
c = cpu_arch
t = platform
h = hostname
v = version
*/

// auto set name based on hostname or asset for new computer
name := c.DefaultQuery("hostname", "")
if name == "" {
name = c.DefaultQuery("asset", "")
name = c.DefaultQuery("asset", c.DefaultQuery("a", ""))
}
if name == "" {
name = c.DefaultQuery("serial", c.DefaultQuery("sn", ""))
}

computer, err = searchComputer(db, id, mac)
var accountID *string
if value, ok := c.Get("account"); ok {
accountID = &value.(*models.Ipxeaccount).Username
}

if err != nil {
// Default bootentry for new computer
bootorder := make([]*models.Bootorder, 0)
if config.DefaultBootentryName != "" {
defaultBootentry := &models.Bootentry{}
result := db.Where("name = ?", config.DefaultBootentryName).Find(&defaultBootentry)
if result.RowsAffected != 0 {
bootorder = append(bootorder, &models.Bootorder{
BootentryUuid: defaultBootentry.Uuid,
})
}
}
computer = models.Computer{
Name: name,
Asset: c.DefaultQuery("asset", ""),
BuildArch: c.DefaultQuery("buildarch", ""),
Hostname: c.DefaultQuery("hostname", ""),
Asset: c.DefaultQuery("asset", c.DefaultQuery("a", "")),
BuildArch: c.DefaultQuery("buildarch", c.DefaultQuery("c", "")),
Hostname: c.DefaultQuery("hostname", c.DefaultQuery("h", "")),
LastSeen: time.Now(),
Mac: mac,
IP: ip,
Manufacturer: c.DefaultQuery("manufacturer", ""),
Platform: c.DefaultQuery("platform", ""),
Product: c.DefaultQuery("product", ""),
Serial: c.DefaultQuery("serial", ""),
Manufacturer: c.DefaultQuery("manufacturer", c.DefaultQuery("m", "")),
Platform: c.DefaultQuery("platform", c.DefaultQuery("t", "")),
Product: c.DefaultQuery("product", c.DefaultQuery("p", "")),
Serial: c.DefaultQuery("serial", c.DefaultQuery("sn", "")),
Uuid: id,
Version: c.DefaultQuery("version", ""),
LastIpxeaccountID: c.MustGet("account").(*models.Ipxeaccount).Username,
Version: c.DefaultQuery("version", c.DefaultQuery("v", "")),
LastIpxeaccountID: accountID,
Bootorder: bootorder,
}
db.FirstOrCreate(&computer)
}
Expand All @@ -59,18 +92,18 @@ func updateOrCreateComputer(c *gin.Context, id uuid.UUID, mac pgtype.Macaddr, ip
}

if time.Now().Sub(computer.LastSeen).Seconds() > 10 {
computer.Asset = c.DefaultQuery("asset", "")
computer.BuildArch = c.DefaultQuery("buildarch", "")
computer.Asset = c.DefaultQuery("asset", c.DefaultQuery("a", ""))
computer.BuildArch = c.DefaultQuery("buildarch", c.DefaultQuery("c", ""))
computer.Hostname = c.DefaultQuery("hostname", "")
computer.LastSeen = time.Now()
computer.Mac = mac
computer.IP = ip
computer.Manufacturer = c.DefaultQuery("manufacturer", "")
computer.Platform = c.DefaultQuery("platform", "")
computer.Product = c.DefaultQuery("product", "")
computer.Serial = c.DefaultQuery("serial", "")
computer.Version = c.DefaultQuery("version", "")
computer.LastIpxeaccountID = c.MustGet("account").(*models.Ipxeaccount).Username
computer.Manufacturer = c.DefaultQuery("manufacturer", c.DefaultQuery("m", ""))
computer.Platform = c.DefaultQuery("platform", c.DefaultQuery("t", ""))
computer.Product = c.DefaultQuery("product", c.DefaultQuery("p", ""))
computer.Serial = c.DefaultQuery("serial", c.DefaultQuery("sn", ""))
computer.Version = c.DefaultQuery("version", c.DefaultQuery("v", ""))
computer.LastIpxeaccountID = accountID
db.Save(computer)
}

Expand Down
Loading
Loading