improve cert-signing ux

This commit is contained in:
Mikaël Cluseau
2026-01-19 17:57:15 +01:00
parent 512177cab0
commit 2af7ff85c1
3 changed files with 30 additions and 21 deletions

View File

@ -156,28 +156,25 @@ func sshCASign(cluster string, userPubKey []byte, principal, validity string, op
return return
} }
_, identity, _, _, err := ssh.ParseAuthorizedKey(userPubKey) pubkey, identity, _, _, err := ssh.ParseAuthorizedKey(bytes.TrimSpace(userPubKey))
if err != nil { if err != nil {
return return
} }
ak := ssh.MarshalAuthorizedKey(pubkey)
userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub") userPubKeyFile, err := os.CreateTemp("/tmp", "user.pub")
if err != nil { if err != nil {
return return
} }
defer os.Remove(userPubKeyFile.Name()) defer os.Remove(userPubKeyFile.Name())
_, err = io.Copy(userPubKeyFile, bytes.NewBuffer(userPubKey)) _, err = io.Copy(userPubKeyFile, bytes.NewBuffer(ak))
userPubKeyFile.Close() userPubKeyFile.Close()
if err != nil { if err != nil {
return return
} }
err = os.WriteFile(userPubKeyFile.Name(), userPubKey, 0600)
if err != nil {
return
}
serial := strconv.FormatInt(time.Now().Unix(), 10) serial := strconv.FormatInt(time.Now().Unix(), 10)
cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal) cmd := exec.Command("ssh-keygen", "-q", "-s", "/dev/stdin", "-I", identity, "-z", serial, "-n", principal)

View File

@ -15,8 +15,8 @@ export default {
sshUserCert: null, sshUserCert: null,
kubeSignReq: { kubeSignReq: {
CSR: "", CSR: "",
User: "anonymous", User: "",
Group: "", Group: "system:masters",
}, },
kubeUserCert: null, kubeUserCert: null,
}; };
@ -28,8 +28,13 @@ export default {
method: 'POST', method: 'POST',
body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }), body: JSON.stringify({ ...this.sshSignReq, Validity: this.signReqValidity }),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => resp.blob()) }).then((resp) => {
.then((cert) => { this.sshUserCert = URL.createObjectURL(cert) }) if (resp.ok) {
resp.blob().then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
} else {
resp.json().then((resp) => alert('failed to sign: '+resp.message))
}
})
.catch((e) => { alert('failed to sign: '+e); }) .catch((e) => { alert('failed to sign: '+e); })
}, },
kubeCASign() { kubeCASign() {
@ -38,8 +43,13 @@ export default {
method: 'POST', method: 'POST',
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }), body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' }, headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
}).then((resp) => resp.blob()) }).then((resp) => {
.then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) }) if (resp.ok) {
resp.blob().then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
} else {
resp.json().then((resp) => alert('failed to sign: '+resp.message))
}
})
.catch((e) => { alert('failed to sign: '+e); }) .catch((e) => { alert('failed to sign: '+e); })
}, },
}, },
@ -78,11 +88,12 @@ export default {
<p>Public key (OpenSSH format):<br/> <p>Public key (OpenSSH format):<br/>
<textarea v-model="sshSignReq.PubKey" style="width:64em;height:2lh"></textarea> <textarea v-model="sshSignReq.PubKey" style="width:64em;height:2lh"></textarea>
</p> </p>
<p>Principal: <input type="text" v-model="sshSignReq.Principal"/></p> <p>User: <input type="text" v-model="sshSignReq.Principal"/></p>
<p><button @click="sshCASign">Sign SSH access request</button></p> <p><button @click="sshCASign">Sign SSH access (validity: {{signReqValidity}})</button>
<p v-if="sshUserCert"> <template v-if="sshUserCert">
<a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a> =&gt; <a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
</template>
</p> </p>
<h4>Grant Kubernetes API access</h4> <h4>Grant Kubernetes API access</h4>
@ -93,9 +104,10 @@ export default {
<p>User: <input type="text" v-model="kubeSignReq.User"/></p> <p>User: <input type="text" v-model="kubeSignReq.User"/></p>
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p> <p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
<p><button @click="kubeCASign">Sign Kubernetes API access request</button></p> <p><button @click="kubeCASign">Sign Kubernetes API access (validity: {{signReqValidity}})</button>
<p v-if="kubeUserCert"> <template v-if="kubeUserCert">
<a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a> =&gt; <a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
</template>
</p> </p>
` `
} }

View File

@ -65,7 +65,7 @@ createApp({
(this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`})); (this.state.Clusters||[]).forEach((c) => views.push({type: "cluster", name: c.Name, title: `Cluster ${c.Name}`}));
(this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`})); (this.state.Hosts ||[]).forEach((c) => views.push({type: "host", name: c.Name, title: `Host ${c.Name}`}));
return views.filter((v) => v.name.includes(this.viewFilter)); return views.filter((v) => v.type != "host" || v.name.includes(this.viewFilter));
}, },
viewObj() { viewObj() {
if (this.view) { if (this.view) {