Skip to content

Commit

Permalink
Merge pull request #1 from aarnaud/develop
Browse files Browse the repository at this point in the history
Add grub support to allow secure boot
  • Loading branch information
aarnaud authored Jan 23, 2024
2 parents 4f18085 + 77b290c commit 11ae3f5
Show file tree
Hide file tree
Showing 18 changed files with 444 additions and 27 deletions.
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

0 comments on commit 11ae3f5

Please sign in to comment.