mirror of
https://github.com/outbackdingo/incus-os.git
synced 2026-01-27 10:19:24 +00:00
Merge pull request #617 from stgraber/main
Add certificate generation to the customizer
This commit is contained in:
@@ -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----- <snip> -----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.<os>.incus.<arch></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='{ "interfaces": [ { "name": "management", "hwaddr": "00:11:22:33:44:55", "addresses": [ "dhcp4", "slaac" ] } ] }'></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>
|
||||
|
||||
@@ -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": {}
|
||||
|
||||
@@ -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", "*")
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user