Merge pull request #617 from stgraber/main

Add certificate generation to the customizer
This commit is contained in:
Stéphane Graber
2025-11-26 17:31:50 -05:00
committed by GitHub
5 changed files with 205 additions and 22 deletions

View File

@@ -84,19 +84,6 @@
</div>
</div>
<div>
<h3>Image architecture</h3>
<div class="mb-3">
<input class="form-check-input" type="radio" name="imageArchitecture" id="imageArchitectureX86_64" checked>
<label class="form-check-label" for="imageArchitectureX86_64">x86_64 - For 64-bit Intel and AMD systems</label>
</div>
<div class="mb-3">
<input class="form-check-input" type="radio" name="imageArchitecture" id="imageArchitectureAARCH64">
<label class="form-check-label" for="imageArchitectureAARCH64">aarch64 - For 64-bit Arm servers</label>
</div>
</div>
<div>
<h3>Image usage</h3>
<div class="mb-3">
@@ -110,6 +97,19 @@
</div>
</div>
<div>
<h3>Image architecture</h3>
<div class="mb-3">
<input class="form-check-input" type="radio" name="imageArchitecture" id="imageArchitectureX86_64" checked>
<label class="form-check-label" for="imageArchitectureX86_64">x86_64 - For 64-bit Intel and AMD systems</label>
</div>
<div class="mb-3">
<input class="form-check-input" type="radio" name="imageArchitecture" id="imageArchitectureAARCH64">
<label class="form-check-label" for="imageArchitectureAARCH64">aarch64 - For 64-bit Arm servers</label>
</div>
</div>
<div>
<h3>Image application</h3>
<div class="mb-3">
@@ -151,7 +151,13 @@
</div>
<div class="mb-3">
<label class="form-label" for="imageInstallTarget">Drive identifier (as seen in /dev/disk/by-id), can be a partial string but must match exactly one drive.<br>If empty, IncusOS will auto-install so long as only one drive is present.</label>
<label class="form-label" for="imageInstallTarget">
<b>Target drive identifier</b>
<br>
This is a (potentially partial) value (as seen in /dev/disk/by-id). The value must match exactly one drive.
<br>
If empty, IncusOS will auto-install so long as only one drive is present.
</label>
<input type="text" class="form-control" id="imageInstallTarget" placeholder="nvme-eui.123456789">
</div>
</div>
@@ -173,15 +179,15 @@
</div>
<div class="mb-3">
<label class="form-label" for="applicationClientCertificate">TLS certificate of the initial trusted client.<br>This can be retrieved by running <code>incus remote get-client-certificate</code>.</label>
<label class="form-label" for="applicationClientCertificate">
<b>TLS client certificate of the initial trusted client.</b>
<br>
If using the <a href="https://github.com/lxc/incus/releases/latest/" target="_blank">Incus CLI tool</a>, this can be retrieved by running <code>incus remote get-client-certificate</code>.
<br>
Alternatively, one can be <a onclick="certificate()" href="#certificate">generated</a> for you which can be imported in your web browser for UI access.
</label>
<textarea class="form-control" id="applicationClientCertificate" rows="10" placeholder="-----BEGIN CERTIFICATE-----&#10;<snip>&#10;-----END CERTIFICATE-----"></textarea>
<br />
<p>
<b>Tip:</b> If you don't already have the Incus client installed, you can download a statically-linked binary for Linux, MacOS, or Windows from the latest <a href="https://github.com/lxc/incus/releases/latest/">Incus release</a> on GitHub.
<br />
Select the <code>bin.&lt;os&gt;.incus.&lt;arch&gt;</code> binary that is correct for your operating system and architecture.
</p>
</div>
</div>
</div>
@@ -216,7 +222,7 @@
can be provided instead.
</p>
<p>Supported options and examples can be <a href="https://linuxcontainers.org/incus-os/docs/main/reference/system/network/">found here</a>.</p>
<p>Supported options and examples can be <a href="https://linuxcontainers.org/incus-os/docs/main/reference/system/network/" target="_blank">found here</a>.</p>
<div class="mb-3">
<textarea class="form-control" id="networkConfiguration" rows="12" placeholder='{&#10; "interfaces": [&#10; {&#10; "name": "management",&#10; "hwaddr": "00:11:22:33:44:55",&#10; "addresses": [&#10; "dhcp4",&#10; "slaac"&#10; ]&#10; }&#10; ]&#10;}'></textarea>
@@ -230,5 +236,37 @@
<button onclick="download()" type="submit" class="btn btn-primary">Download</button>
</div>
</div>
<div class="modal modal-xl fade" id="certificateModal" tabindex="-1" role="dialog" aria-labelledby="certificateModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="certificateModalLabel">Certificate generated</h5>
</div>
<div class="modal-body">
<p>
Three files have been generated and downloaded for you:
<br>
<ul>
<li><code>client.crt</code> is a PEM encoded client certificate (can be placed as <code>~/.config/incus/client.crt</code>)</li>
<li><code>client.key</code> is a PEM encoded client key (can be placed as <code>~/.config/incus/client.key</code>)</li>
<li><code>client.pfx</code> is a PPKCS#12 certificate that can be imported in your web browser (password is <b>IncusOS</b>)</li>
</ul>
</p>
<br>
<p>
<b>NOTE:</b> This certificate was generated server-side.
<br>
While we don't keep any record of it, it is generally preferrable to manually generate your certificate locally to avoid any chance of it getting intercepted.
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>
</body>
</html>

View File

@@ -1,3 +1,52 @@
function certificate() {
// Send the request.
fetch("/1.0/certificate", {
method: "GET",
}).then(response => response.json()).then(function(response) {
if (response.status_code != 200) {
alert("Unable to get generated certificate");
return;
}
// Set the certificate.
document.getElementById("applicationClientCertificate").value = response.metadata.certificate;
// Download the various files onto the client.
const blobCert = new Blob([response.metadata.certificate], {type: 'application/x-pem-file'});
const urlCert = window.URL.createObjectURL(blobCert);
const aCert = document.createElement("a");
aCert.href = urlCert;
aCert.download = "client.crt";
aCert.click();
const blobKey = new Blob([response.metadata.key], {type: 'application/x-pem-file'});
const urlKey = window.URL.createObjectURL(blobKey);
const aKey = document.createElement("a");
aKey.href = urlKey;
aKey.download = "client.key";
aKey.click();
const byteString = window.atob(response.metadata.pfx);
var bytesPfx = new ArrayBuffer(byteString.length);
var ia = new Uint8Array(bytesPfx);
for (var i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
const blobPfx = new Blob([bytesPfx], {type: 'application/x-pkcs12'});
const urlPfx = window.URL.createObjectURL(blobPfx);
const aPfx = document.createElement("a");
aPfx.href = urlPfx;
aPfx.download = "client.pfx";
aPfx.click();
var modalDialog = new bootstrap.Modal(document.getElementById("certificateModal"), {});
modalDialog.show();
});
}
function download() {
req = {
"seeds": {}

View File

@@ -5,8 +5,16 @@ import (
"archive/tar"
"compress/gzip"
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"embed"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
@@ -24,6 +32,7 @@ import (
"github.com/google/uuid"
"github.com/timpalpant/gzran"
"gopkg.in/yaml.v3"
"software.sslmate.com/src/go-pkcs12"
apiupdate "github.com/lxc/incus-os/incus-osd/api/images"
apiseed "github.com/lxc/incus-os/incus-osd/api/seed"
@@ -46,6 +55,12 @@ var (
imagesMu sync.Mutex
)
type apiCertificateGet struct {
Certificate string `json:"certificate"`
Key string `json:"key"`
PFX string `json:"pfx"`
}
type apiImagesPost struct {
Architecture string `json:"architecture" yaml:"architecture"`
Type string `json:"type" yaml:"type"`
@@ -98,6 +113,7 @@ func do(ctx context.Context) error {
router.HandleFunc("/", apiRoot)
router.HandleFunc("/1.0", apiRoot10)
router.HandleFunc("/1.0/certificate", apiCertificate)
router.HandleFunc("/1.0/images", apiImages)
router.HandleFunc("/1.0/images/{uuid}", apiImage)
router.Handle("/ui/", http.StripPrefix("/ui/", http.FileServer(http.FS(fsUI))))
@@ -158,6 +174,83 @@ func apiRoot10(w http.ResponseWriter, r *http.Request) {
_ = response.SyncResponse(true, map[string]any{}).Render(w)
}
func apiCertificate(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if r.Method != http.MethodGet {
_ = response.NotImplemented(nil).Render(w)
return
}
// Generate the certificate.
key, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
if err != nil {
_ = response.BadRequest(err).Render(w)
return
}
certTemplate := x509.Certificate{
Subject: pkix.Name{
Organization: []string{"Linux Containers"},
CommonName: "Auto-generated IncusOS client certificate",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
DNSNames: []string{"unspecified"},
}
certDerBytes, err := x509.CreateCertificate(rand.Reader, &certTemplate, &certTemplate, &key.PublicKey, key)
if err != nil {
_ = response.BadRequest(err).Render(w)
return
}
keyDerBytes, err := x509.MarshalECPrivateKey(key)
if err != nil {
_ = response.BadRequest(err).Render(w)
return
}
certBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDerBytes})
keyBytes := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDerBytes})
// Load the cert and key.
cert, err := tls.X509KeyPair(certBytes, keyBytes)
if err != nil {
_ = response.BadRequest(err).Render(w)
return
}
// Get the PKCS12.
pfx, err := pkcs12.Modern2023.Encode(cert.PrivateKey, cert.Leaf, nil, "IncusOS")
if err != nil {
_ = response.BadRequest(err).Render(w)
return
}
slog.Info("certificate generated", "client", clientAddress(r))
resp := apiCertificateGet{
Certificate: string(certBytes),
Key: string(keyBytes),
PFX: base64.StdEncoding.EncodeToString(pfx),
}
_ = response.SyncResponse(true, resp).Render(w)
}
func apiImages(w http.ResponseWriter, r *http.Request) {
// Set CORS headers.
w.Header().Set("Access-Control-Allow-Origin", "*")

View File

@@ -89,4 +89,5 @@ require (
golang.org/x/term v0.36.0 // indirect
golang.org/x/text v0.30.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
software.sslmate.com/src/go-pkcs12 v0.6.0 // indirect
)

View File

@@ -335,3 +335,5 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
software.sslmate.com/src/go-pkcs12 v0.6.0 h1:f3sQittAeF+pao32Vb+mkli+ZyT+VwKaD014qFGq6oU=
software.sslmate.com/src/go-pkcs12 v0.6.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=