prod{access,vider}: implement
Prodaccess/Prodvider allow issuing short-lived certificates for all SSO
users to access the kubernetes cluster.
Currently, all users get a personal-$username namespace in which they
have adminitrative rights. Otherwise, they get no access.
In addition, we define a static CRB to allow some admins access to
everything. In the future, this will be more granular.
We also update relevant documentation.
Change-Id: Ia18594eea8a9e5efbb3e9a25a04a28bbd6a42153
diff --git a/cluster/prodvider/kubernetes.go b/cluster/prodvider/kubernetes.go
new file mode 100644
index 0000000..3386625
--- /dev/null
+++ b/cluster/prodvider/kubernetes.go
@@ -0,0 +1,205 @@
+package main
+
+import (
+ "encoding/pem"
+ "fmt"
+ "time"
+
+ "github.com/golang/glog"
+ corev1 "k8s.io/api/core/v1"
+ rbacv1 "k8s.io/api/rbac/v1"
+ "k8s.io/apimachinery/pkg/api/errors"
+ metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ "k8s.io/client-go/kubernetes"
+ "k8s.io/client-go/rest"
+
+ pb "code.hackerspace.pl/hscloud/cluster/prodvider/proto"
+)
+
+func (p *prodvider) kubernetesCreds(username string) (*pb.KubernetesKeys, error) {
+ o := fmt.Sprintf("sso:%s", username)
+
+ csrPEM, keyPEM, err := p.makeKubernetesCSR(username+"@hackerspace.pl", o)
+ if err != nil {
+ return nil, err
+ }
+
+ certPEM, err := p.makeKubernetesCertificate(csrPEM, time.Now().Add(13*time.Hour))
+ if err != nil {
+ return nil, err
+ }
+
+ caCert, _ := p.sign.Certificate("", "")
+ caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
+
+ // Build certificate chain from new cert and intermediate CA.
+ chainPEM := append(certPEM, caPEM...)
+
+ glog.Infof("Generated k8s certificate for %q", username)
+ return &pb.KubernetesKeys{
+ Cluster: "k0.hswaw.net",
+ // APIServerCA
+ Ca: p.kubeCAPEM,
+ // Chain of new cert + intermediate CA
+ Cert: chainPEM,
+ Key: keyPEM,
+ }, nil
+}
+
+func (p *prodvider) kubernetesConnect() error {
+ csrPEM, keyPEM, err := p.makeKubernetesCSR("prodvider", "system:masters")
+ if err != nil {
+ return err
+ }
+
+ certPEM, err := p.makeKubernetesCertificate(csrPEM, time.Now().Add(30*24*time.Hour))
+ if err != nil {
+ return err
+ }
+
+ caCert, _ := p.sign.Certificate("", "")
+
+ caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caCert.Raw})
+
+ glog.Infof("Generated k8s certificate for self (system:masters)")
+
+ // Build certificate chain from our cert and intermediate CA.
+ chainPEM := append(certPEM, caPEM...)
+
+ config := &rest.Config{
+ Host: flagKubernetesHost,
+ TLSClientConfig: rest.TLSClientConfig{
+ // Chain to authenticate ourselves (us + intermediate CA).
+ CertData: chainPEM,
+ KeyData: keyPEM,
+ // APIServer CA for verification.
+ CAData: p.kubeCAPEM,
+ },
+ }
+
+ cs, err := kubernetes.NewForConfig(config)
+ if err != nil {
+ return err
+ }
+
+ p.k8s = cs
+
+ return nil
+}
+
+// kubernetesSetupUser ensures that for a given SSO username we:
+// - have a personal-<username> namespace
+// - have a sso:<username>:personal rolebinding that binds
+// system:admin-namespace to the user within their personal namespace
+// - have a sso:<username>:global clusterrolebinding that binds
+// system:viewer to the user at cluster level
+func (p *prodvider) kubernetesSetupUser(username string) error {
+ namespace := "personal-" + username
+ if err := p.ensureNamespace(namespace); err != nil {
+ return err
+ }
+ if err := p.ensureRoleBindingPersonal(namespace, username); err != nil {
+ return err
+ }
+ if err := p.ensureClusterRoleBindingGlobal(username); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (p *prodvider) ensureNamespace(name string) error {
+ _, err := p.k8s.CoreV1().Namespaces().Get(name, metav1.GetOptions{})
+ switch {
+ case err == nil:
+ // Already exists, nothing to do
+ return nil
+ case errors.IsNotFound(err):
+ break
+ default:
+ // Something went wrong.
+ return err
+ }
+ ns := &corev1.Namespace{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ },
+ }
+ _, err = p.k8s.CoreV1().Namespaces().Create(ns)
+ return err
+}
+
+func (p *prodvider) ensureRoleBindingPersonal(namespace, username string) error {
+ name := "sso:" + username + ":personal"
+ rb := &rbacv1.RoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ Namespace: namespace,
+ },
+ Subjects: []rbacv1.Subject{
+ {
+ APIGroup: "rbac.authorization.k8s.io",
+ Kind: "User",
+ Name: username + "@hackerspace.pl",
+ },
+ },
+ RoleRef: rbacv1.RoleRef{
+ APIGroup: "rbac.authorization.k8s.io",
+ Kind: "ClusterRole",
+ Name: "system:admin-namespace",
+ },
+ }
+
+ rbs := p.k8s.RbacV1().RoleBindings(namespace)
+ _, err := rbs.Get(name, metav1.GetOptions{})
+ switch {
+ case err == nil:
+ // Already exists, update.
+ _, err = rbs.Update(rb)
+ return err
+ case errors.IsNotFound(err):
+ // Create.
+ _, err = rbs.Create(rb)
+ return err
+ default:
+ // Something went wrong.
+ return err
+ }
+}
+
+func (p *prodvider) ensureClusterRoleBindingGlobal(username string) error {
+ name := "sso:" + username + ":global"
+ rb := &rbacv1.ClusterRoleBinding{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: name,
+ },
+ Subjects: []rbacv1.Subject{
+ {
+ APIGroup: "rbac.authorization.k8s.io",
+ Kind: "User",
+ Name: username + "@hackerspace.pl",
+ },
+ },
+ RoleRef: rbacv1.RoleRef{
+ APIGroup: "rbac.authorization.k8s.io",
+ Kind: "ClusterRole",
+ Name: "system:viewer",
+ },
+ }
+
+ crbs := p.k8s.RbacV1().ClusterRoleBindings()
+ _, err := crbs.Get(name, metav1.GetOptions{})
+ switch {
+ case err == nil:
+ // Already exists, update.
+ _, err = crbs.Update(rb)
+ return err
+ case errors.IsNotFound(err):
+ // Create.
+ _, err = crbs.Create(rb)
+ return err
+ default:
+ // Something went wrong.
+ return err
+ }
+}