add kube CSR access
This commit is contained in:
23
cmd/dkl-local-server/parsers_test.go
Normal file
23
cmd/dkl-local-server/parsers_test.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_parseCertDuration() {
|
||||||
|
now := time.Date(2020, time.April, 28, 12, 30, 0, 0, time.UTC)
|
||||||
|
|
||||||
|
fmt.Println(parseCertDuration("", now))
|
||||||
|
fmt.Println(parseCertDuration("hi!", now))
|
||||||
|
fmt.Println(parseCertDuration("-2d3h", now))
|
||||||
|
fmt.Println(parseCertDuration("2d3h", now))
|
||||||
|
fmt.Println(parseCertDuration("+1y-1s", now))
|
||||||
|
|
||||||
|
// output:
|
||||||
|
// 0001-01-01 00:00:00 +0000 UTC <nil>
|
||||||
|
// 0001-01-01 00:00:00 +0000 UTC invalid duration: "hi!"
|
||||||
|
// 2020-04-26 09:30:00 +0000 UTC <nil>
|
||||||
|
// 2020-04-30 15:30:00 +0000 UTC <nil>
|
||||||
|
// 2021-04-28 12:29:59 +0000 UTC <nil>
|
||||||
|
}
|
@ -1,10 +1,18 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/cloudflare/cfssl/config"
|
||||||
|
"github.com/cloudflare/cfssl/csr"
|
||||||
|
"github.com/cloudflare/cfssl/signer"
|
||||||
restful "github.com/emicklei/go-restful"
|
restful "github.com/emicklei/go-restful"
|
||||||
|
|
||||||
"novit.tech/direktil/local-server/pkg/mime"
|
"novit.tech/direktil/local-server/pkg/mime"
|
||||||
@ -151,7 +159,25 @@ func wsClusterSSHUserCASign(req *restful.Request, resp *restful.Response) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cert, err := sshCASign(clusterName, []byte(signReq.PubKey), signReq.Principal, signReq.Validity, signReq.Options...)
|
now := time.Now().Truncate(time.Second)
|
||||||
|
notBefore, notAfter, err := parseCertDurationRange(signReq.Validity, now)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, fmt.Errorf("invalid validity: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const sshTimestamp = "20060102150405Z"
|
||||||
|
|
||||||
|
validity := notBefore.Format(sshTimestamp) + ":"
|
||||||
|
if notAfter.IsZero() {
|
||||||
|
validity += "forever"
|
||||||
|
} else {
|
||||||
|
validity += notAfter.Format(sshTimestamp)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("sign ssh public key, validity %s -> %s", signReq.Validity, validity)
|
||||||
|
|
||||||
|
cert, err := sshCASign(clusterName, []byte(signReq.PubKey), signReq.Principal, validity, signReq.Options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
wsError(resp, err)
|
wsError(resp, err)
|
||||||
return
|
return
|
||||||
@ -159,3 +185,147 @@ func wsClusterSSHUserCASign(req *restful.Request, resp *restful.Response) {
|
|||||||
|
|
||||||
resp.Write(cert)
|
resp.Write(cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type KubeSignReq struct {
|
||||||
|
CSR string
|
||||||
|
User string
|
||||||
|
Group string
|
||||||
|
Validity string
|
||||||
|
}
|
||||||
|
|
||||||
|
func wsClusterKubeCASign(req *restful.Request, resp *restful.Response) {
|
||||||
|
clusterName := req.PathParameter("cluster-name")
|
||||||
|
|
||||||
|
signReq := KubeSignReq{}
|
||||||
|
err := req.ReadEntity(&signReq)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
now := time.Now().Truncate(time.Second)
|
||||||
|
notBefore, notAfter, err := parseCertDurationRange(signReq.Validity, now)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, fmt.Errorf("invalid validity: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var names []csr.Name
|
||||||
|
if signReq.Group != "" {
|
||||||
|
names = []csr.Name{{O: signReq.Group}}
|
||||||
|
}
|
||||||
|
|
||||||
|
ca, err := getUsableClusterCA(clusterName, "cluster")
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, fmt.Errorf("get cluster CA failed: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
caSigner, err := ca.Signer(&config.Signing{
|
||||||
|
Default: &config.SigningProfile{
|
||||||
|
Usage: []string{"client auth"},
|
||||||
|
Expiry: notAfter.Sub(now),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
csr := signer.SignRequest{
|
||||||
|
Request: signReq.CSR,
|
||||||
|
Subject: &signer.Subject{
|
||||||
|
CN: signReq.User,
|
||||||
|
Names: names,
|
||||||
|
},
|
||||||
|
NotBefore: notBefore,
|
||||||
|
NotAfter: notAfter,
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := caSigner.Sign(csr)
|
||||||
|
if err != nil {
|
||||||
|
wsError(resp, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.Write(cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseCertDurationRange(d string, now time.Time) (notBefore, notAfter time.Time, err error) {
|
||||||
|
if d == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d1, d2, ok := strings.Cut(d, ":")
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
notBefore, err = parseCertDuration(d1, now)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
notAfter, err = parseCertDuration(d2, now)
|
||||||
|
} else {
|
||||||
|
notAfter, err = parseCertDuration(d, now)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if notBefore.IsZero() {
|
||||||
|
notBefore = now.Add(-5 * time.Minute)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var durRegex = regexp.MustCompile("^([+-]?)([0-9]+)([yMdwhms])")
|
||||||
|
|
||||||
|
func parseCertDuration(d string, now time.Time) (t time.Time, err error) {
|
||||||
|
if d == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
direction := 1
|
||||||
|
t = now
|
||||||
|
|
||||||
|
for d != "" {
|
||||||
|
match := durRegex.FindStringSubmatch(d)
|
||||||
|
if match == nil {
|
||||||
|
t = time.Time{}
|
||||||
|
err = errors.New("invalid duration: " + strconv.Quote(d))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
d = d[len(match[0]):]
|
||||||
|
|
||||||
|
switch match[1] {
|
||||||
|
case "+":
|
||||||
|
direction = 1
|
||||||
|
case "-":
|
||||||
|
direction = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
qty, _ := strconv.Atoi(match[2])
|
||||||
|
unit := match[3]
|
||||||
|
|
||||||
|
switch unit {
|
||||||
|
case "y":
|
||||||
|
t = t.AddDate(qty*direction, 0, 0)
|
||||||
|
case "M":
|
||||||
|
t = t.AddDate(0, qty*direction, 0)
|
||||||
|
case "d":
|
||||||
|
t = t.AddDate(0, 0, qty*direction)
|
||||||
|
case "w":
|
||||||
|
t = t.AddDate(0, 0, 7*qty*direction)
|
||||||
|
case "h":
|
||||||
|
t = t.Add(time.Duration(qty*direction) * time.Hour)
|
||||||
|
case "m":
|
||||||
|
t = t.Add(time.Duration(qty*direction) * time.Minute)
|
||||||
|
case "s":
|
||||||
|
t = t.Add(time.Duration(qty*direction) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
@ -134,6 +134,9 @@ func registerWS(rest *restful.Container) {
|
|||||||
cluster(POST, "/ssh/user-ca/sign").To(wsClusterSSHUserCASign).
|
cluster(POST, "/ssh/user-ca/sign").To(wsClusterSSHUserCASign).
|
||||||
Produces(mime.OCTET).
|
Produces(mime.OCTET).
|
||||||
Doc("Sign a user's SSH public key for this cluster"),
|
Doc("Sign a user's SSH public key for this cluster"),
|
||||||
|
cluster(POST, "/kube/sign").To(wsClusterKubeCASign).
|
||||||
|
Produces(mime.OCTET).
|
||||||
|
Doc("Sign a user's public key for this cluster's Kubernetes API server"),
|
||||||
} {
|
} {
|
||||||
ws.Route(builder)
|
ws.Route(builder)
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,18 @@ export default {
|
|||||||
props: [ 'cluster', 'token', 'state' ],
|
props: [ 'cluster', 'token', 'state' ],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
signReqValidity: "1d",
|
||||||
sshSignReq: {
|
sshSignReq: {
|
||||||
PubKey: "",
|
PubKey: "",
|
||||||
Principal: "root",
|
Principal: "root",
|
||||||
Validity: "+1d",
|
|
||||||
},
|
},
|
||||||
sshUserCert: null,
|
sshUserCert: null,
|
||||||
|
kubeSignReq: {
|
||||||
|
CSR: "",
|
||||||
|
User: "anonymous",
|
||||||
|
Group: "",
|
||||||
|
},
|
||||||
|
kubeUserCert: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@ -20,12 +26,22 @@ export default {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
|
fetch(`/clusters/${this.cluster.Name}/ssh/user-ca/sign`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify(this.sshSignReq),
|
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) => resp.blob())
|
||||||
.then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
|
.then((cert) => { this.sshUserCert = URL.createObjectURL(cert) })
|
||||||
.catch((e) => { alert('failed to sign: '+e); })
|
.catch((e) => { alert('failed to sign: '+e); })
|
||||||
},
|
},
|
||||||
|
kubeCASign() {
|
||||||
|
event.preventDefault();
|
||||||
|
fetch(`/clusters/${this.cluster.Name}/kube/sign`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ ...this.kubeSignReq, Validity: this.signReqValidity }),
|
||||||
|
headers: { 'Authorization': 'Bearer ' + this.token, 'Content-Type': 'application/json' },
|
||||||
|
}).then((resp) => resp.blob())
|
||||||
|
.then((cert) => { this.kubeUserCert = URL.createObjectURL(cert) })
|
||||||
|
.catch((e) => { alert('failed to sign: '+e); })
|
||||||
|
},
|
||||||
},
|
},
|
||||||
template: `
|
template: `
|
||||||
<h3>Tokens</h3>
|
<h3>Tokens</h3>
|
||||||
@ -52,17 +68,34 @@ export default {
|
|||||||
</template></td>
|
</template></td>
|
||||||
</tr></table>
|
</tr></table>
|
||||||
|
|
||||||
<h3>SSH</h3>
|
<h3>Access</h3>
|
||||||
<form @submit="sshCASign()" action="">
|
|
||||||
<p>User public key (OpenSSH format):<br/>
|
<p>Allow cluster access from a public key</p>
|
||||||
<textarea v-model="sshSignReq.PubKey"></textarea>
|
<p>Certificate time validity: <input type="text" v-model="signReqValidity"/> <small>ie: -5m:1w, 5m, 1M, 1y, 1d-1s, etc.</p>
|
||||||
|
|
||||||
|
<h4>Grant SSH access</h4>
|
||||||
|
|
||||||
|
<p>Public key (OpenSSH format):<br/>
|
||||||
|
<textarea v-model="sshSignReq.PubKey" style="width:64em;height:2lh"></textarea>
|
||||||
</p>
|
</p>
|
||||||
<p>Principal: <input type="text" v-model="sshSignReq.Principal"/></p>
|
<p>Principal: <input type="text" v-model="sshSignReq.Principal"/></p>
|
||||||
<p>Validity: <input type="text" v-model="sshSignReq.Validity"/></p>
|
|
||||||
<input type="submit" value="Sign key" />
|
<p><button @click="sshCASign">Sign SSH access request</button></p>
|
||||||
</form>
|
|
||||||
<p v-if="sshUserCert">
|
<p v-if="sshUserCert">
|
||||||
<a :href="sshUserCert" download="ssh-cert.pub">Get user SSH certificate</a>
|
<a :href="sshUserCert" download="ssh-cert.pub">Get certificate</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>Grant Kubernetes API access</h4>
|
||||||
|
|
||||||
|
<p>Certificate signing request (PEM format):<br/>
|
||||||
|
<textarea v-model="kubeSignReq.CSR" style="width:64em;height:7lh;"></textarea>
|
||||||
|
</p>
|
||||||
|
<p>User: <input type="text" v-model="kubeSignReq.User"/></p>
|
||||||
|
<p>Group: <input type="text" v-model="kubeSignReq.Group"/></p>
|
||||||
|
|
||||||
|
<p><button @click="kubeCASign">Sign Kubernetes API access request</button></p>
|
||||||
|
<p v-if="kubeUserCert">
|
||||||
|
<a :href="kubeUserCert" download="kube-cert.pub">Get certificate</a>
|
||||||
</p>
|
</p>
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user