|
|
|
@ -3,8 +3,8 @@ package main
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/sha256"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"net/http"
|
|
|
|
@ -18,8 +18,8 @@ import (
|
|
|
|
|
"github.com/smallstep/certificates/ca"
|
|
|
|
|
"github.com/smallstep/cli/crypto/pemutil"
|
|
|
|
|
"k8s.io/api/admission/v1beta1"
|
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
|
|
|
)
|
|
|
|
@ -46,14 +46,14 @@ const (
|
|
|
|
|
// Config options for the autocert admission controller.
|
|
|
|
|
type Config struct {
|
|
|
|
|
LogFormat string `yaml:"logFormat"`
|
|
|
|
|
CaUrl string `yaml:"caUrl"`
|
|
|
|
|
CaURL string `yaml:"caUrl"`
|
|
|
|
|
CertLifetime string `yaml:"certLifetime"`
|
|
|
|
|
Bootstrapper corev1.Container `yaml:"bootstrapper"`
|
|
|
|
|
Renewer corev1.Container `yaml:"renewer"`
|
|
|
|
|
CertsVolume corev1.Volume `yaml:"certsVolume"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RFC6902 JSONPatch Operation
|
|
|
|
|
// PatchOperation represents a RFC6902 JSONPatch Operation
|
|
|
|
|
type PatchOperation struct {
|
|
|
|
|
Op string `json:"op"`
|
|
|
|
|
Path string `json:"path"`
|
|
|
|
@ -61,7 +61,7 @@ type PatchOperation struct {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// RFC6901 JSONPath Escaping -- https://tools.ietf.org/html/rfc6901
|
|
|
|
|
func escapeJsonPath(path string) string {
|
|
|
|
|
func escapeJSONPath(path string) string {
|
|
|
|
|
// Replace`~` with `~0` then `/` with `~1`. Note that the order
|
|
|
|
|
// matters otherwise we'll turn a `/` into a `~/`.
|
|
|
|
|
path = strings.Replace(path, "~", "~0", -1)
|
|
|
|
@ -88,19 +88,19 @@ func loadConfig(file string) (*Config, error) {
|
|
|
|
|
// A goroutine is scheduled to cleanup the secret after the token expires. The secret
|
|
|
|
|
// is also labelled for easy identification and manual cleanup.
|
|
|
|
|
func createTokenSecret(prefix, namespace, token string) (string, error) {
|
|
|
|
|
secret := corev1.Secret {
|
|
|
|
|
TypeMeta: metav1.TypeMeta {
|
|
|
|
|
secret := corev1.Secret{
|
|
|
|
|
TypeMeta: metav1.TypeMeta{
|
|
|
|
|
Kind: "Secret",
|
|
|
|
|
APIVersion: "v1",
|
|
|
|
|
},
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta {
|
|
|
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
|
|
|
GenerateName: prefix,
|
|
|
|
|
Namespace: namespace,
|
|
|
|
|
Labels: map[string]string {
|
|
|
|
|
Labels: map[string]string{
|
|
|
|
|
tokenSecretLabel: "true",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
StringData: map[string]string {
|
|
|
|
|
StringData: map[string]string{
|
|
|
|
|
tokenSecretKey: token,
|
|
|
|
|
},
|
|
|
|
|
Type: corev1.SecretTypeOpaque,
|
|
|
|
@ -206,36 +206,36 @@ func mkBootstrapper(config *Config, commonName string, namespace string, provisi
|
|
|
|
|
sum := sha256.Sum256(crt.Raw)
|
|
|
|
|
fingerprint := strings.ToLower(hex.EncodeToString(sum[:]))
|
|
|
|
|
|
|
|
|
|
secretName, err := createTokenSecret(commonName + "-", namespace, token)
|
|
|
|
|
secretName, err := createTokenSecret(commonName+"-", namespace, token)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return b, errors.Wrap(err, "create token secret")
|
|
|
|
|
}
|
|
|
|
|
log.Infof("Secret name is: %s", secretName)
|
|
|
|
|
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar {
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar{
|
|
|
|
|
Name: "COMMON_NAME",
|
|
|
|
|
Value: commonName,
|
|
|
|
|
})
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar {
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar{
|
|
|
|
|
Name: "STEP_TOKEN",
|
|
|
|
|
ValueFrom: &corev1.EnvVarSource {
|
|
|
|
|
SecretKeyRef: &corev1.SecretKeySelector {
|
|
|
|
|
LocalObjectReference: corev1.LocalObjectReference {
|
|
|
|
|
ValueFrom: &corev1.EnvVarSource{
|
|
|
|
|
SecretKeyRef: &corev1.SecretKeySelector{
|
|
|
|
|
LocalObjectReference: corev1.LocalObjectReference{
|
|
|
|
|
Name: secretName,
|
|
|
|
|
},
|
|
|
|
|
Key: tokenSecretKey,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar {
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar{
|
|
|
|
|
Name: "STEP_CA_URL",
|
|
|
|
|
Value: config.CaUrl,
|
|
|
|
|
Value: config.CaURL,
|
|
|
|
|
})
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar {
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar{
|
|
|
|
|
Name: "STEP_FINGERPRINT",
|
|
|
|
|
Value: fingerprint,
|
|
|
|
|
})
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar {
|
|
|
|
|
b.Env = append(b.Env, corev1.EnvVar{
|
|
|
|
|
Name: "STEP_NOT_AFTER",
|
|
|
|
|
Value: config.CertLifetime,
|
|
|
|
|
})
|
|
|
|
@ -243,72 +243,72 @@ func mkBootstrapper(config *Config, commonName string, namespace string, provisi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mkRenewer generates a new renewer based on the template provided in Config.
|
|
|
|
|
func mkRenewer(config *Config) (corev1.Container) {
|
|
|
|
|
func mkRenewer(config *Config) corev1.Container {
|
|
|
|
|
r := config.Renewer
|
|
|
|
|
r.Env = append(r.Env, corev1.EnvVar {
|
|
|
|
|
r.Env = append(r.Env, corev1.EnvVar{
|
|
|
|
|
Name: "STEP_CA_URL",
|
|
|
|
|
Value: config.CaUrl,
|
|
|
|
|
Value: config.CaURL,
|
|
|
|
|
})
|
|
|
|
|
return r
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addContainers(existing, new []corev1.Container, path string) (ops []PatchOperation) {
|
|
|
|
|
if len(existing) == 0 {
|
|
|
|
|
return []PatchOperation {
|
|
|
|
|
PatchOperation {
|
|
|
|
|
return []PatchOperation{
|
|
|
|
|
{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: path,
|
|
|
|
|
Value: new,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, add := range new {
|
|
|
|
|
ops = append(ops, PatchOperation {
|
|
|
|
|
ops = append(ops, PatchOperation{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: path + "/-",
|
|
|
|
|
Value: add,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return ops
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addVolumes(existing, new []corev1.Volume, path string) (ops []PatchOperation) {
|
|
|
|
|
if len(existing) == 0 {
|
|
|
|
|
return []PatchOperation{
|
|
|
|
|
PatchOperation {
|
|
|
|
|
{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: path,
|
|
|
|
|
Value: new,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, add := range new {
|
|
|
|
|
ops = append(ops, PatchOperation {
|
|
|
|
|
ops = append(ops, PatchOperation{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: path + "/-",
|
|
|
|
|
Value: add,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return ops
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addCertsVolumeMount(volumeName string, containers []corev1.Container) (ops []PatchOperation) {
|
|
|
|
|
volumeMount := corev1.VolumeMount {
|
|
|
|
|
volumeMount := corev1.VolumeMount{
|
|
|
|
|
Name: volumeName,
|
|
|
|
|
MountPath: volumeMountPath,
|
|
|
|
|
ReadOnly: true,
|
|
|
|
|
}
|
|
|
|
|
for i, container := range containers {
|
|
|
|
|
if len(container.VolumeMounts) == 0 {
|
|
|
|
|
ops = append(ops, PatchOperation {
|
|
|
|
|
ops = append(ops, PatchOperation{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: fmt.Sprintf("/spec/containers/%v/volumeMounts", i),
|
|
|
|
|
Value: []corev1.VolumeMount{volumeMount},
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
ops = append(ops, PatchOperation {
|
|
|
|
|
ops = append(ops, PatchOperation{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: fmt.Sprintf("/spec/containers/%v/volumeMounts/-", i),
|
|
|
|
|
Value: volumeMount,
|
|
|
|
@ -321,7 +321,7 @@ func addCertsVolumeMount(volumeName string, containers []corev1.Container) (ops
|
|
|
|
|
func addAnnotations(existing, new map[string]string) (ops []PatchOperation) {
|
|
|
|
|
if len(existing) == 0 {
|
|
|
|
|
return []PatchOperation{
|
|
|
|
|
PatchOperation {
|
|
|
|
|
{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: "/metadata/annotations",
|
|
|
|
|
Value: new,
|
|
|
|
@ -330,15 +330,15 @@ func addAnnotations(existing, new map[string]string) (ops []PatchOperation) {
|
|
|
|
|
}
|
|
|
|
|
for k, v := range new {
|
|
|
|
|
if existing[k] == "" {
|
|
|
|
|
ops = append(ops, PatchOperation {
|
|
|
|
|
ops = append(ops, PatchOperation{
|
|
|
|
|
Op: "add",
|
|
|
|
|
Path: "/metadata/annotations/" + escapeJsonPath(k),
|
|
|
|
|
Path: "/metadata/annotations/" + escapeJSONPath(k),
|
|
|
|
|
Value: v,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
ops = append(ops, PatchOperation {
|
|
|
|
|
ops = append(ops, PatchOperation{
|
|
|
|
|
Op: "replace",
|
|
|
|
|
Path: "/metadata/annotations/" + escapeJsonPath(k),
|
|
|
|
|
Path: "/metadata/annotations/" + escapeJSONPath(k),
|
|
|
|
|
Value: v,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
@ -355,7 +355,7 @@ func addAnnotations(existing, new map[string]string) (ops []PatchOperation) {
|
|
|
|
|
// - Annotate the pod to indicate that it's been processed by this controller
|
|
|
|
|
// The result is a list of serialized JSONPatch objects (or an error).
|
|
|
|
|
func patch(pod *corev1.Pod, namespace string, config *Config, provisioner Provisioner) ([]byte, error) {
|
|
|
|
|
var ops[] PatchOperation
|
|
|
|
|
var ops []PatchOperation
|
|
|
|
|
|
|
|
|
|
commonName := pod.ObjectMeta.GetAnnotations()[admissionWebhookAnnotationKey]
|
|
|
|
|
renewer := mkRenewer(config)
|
|
|
|
@ -386,9 +386,9 @@ func shouldMutate(metadata *metav1.ObjectMeta) bool {
|
|
|
|
|
// mutated already (status key isn't set).
|
|
|
|
|
if annotations[admissionWebhookAnnotationKey] == "" || annotations[admissionWebhookStatusKey] == "injected" {
|
|
|
|
|
return false
|
|
|
|
|
} else {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// mutate takes an `AdmissionReview`, determines whether it is subject to mutation, and returns
|
|
|
|
@ -400,10 +400,10 @@ func mutate(review *v1beta1.AdmissionReview, config *Config, provisioner Provisi
|
|
|
|
|
var pod corev1.Pod
|
|
|
|
|
if err := json.Unmarshal(request.Object.Raw, &pod); err != nil {
|
|
|
|
|
ctxLog.WithField("error", err).Error("Error unmarshalling pod")
|
|
|
|
|
return &v1beta1.AdmissionResponse {
|
|
|
|
|
return &v1beta1.AdmissionResponse{
|
|
|
|
|
Allowed: false,
|
|
|
|
|
UID: request.UID,
|
|
|
|
|
Result: &metav1.Status {
|
|
|
|
|
Result: &metav1.Status{
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
@ -420,7 +420,7 @@ func mutate(review *v1beta1.AdmissionReview, config *Config, provisioner Provisi
|
|
|
|
|
|
|
|
|
|
if !shouldMutate(&pod.ObjectMeta) {
|
|
|
|
|
ctxLog.WithField("annotations", pod.Annotations).Info("Skipping mutation")
|
|
|
|
|
return &v1beta1.AdmissionResponse {
|
|
|
|
|
return &v1beta1.AdmissionResponse{
|
|
|
|
|
Allowed: true,
|
|
|
|
|
UID: request.UID,
|
|
|
|
|
}
|
|
|
|
@ -429,17 +429,17 @@ func mutate(review *v1beta1.AdmissionReview, config *Config, provisioner Provisi
|
|
|
|
|
patchBytes, err := patch(&pod, request.Namespace, config, provisioner)
|
|
|
|
|
if err != nil {
|
|
|
|
|
ctxLog.WithField("error", err).Error("Error generating patch")
|
|
|
|
|
return &v1beta1.AdmissionResponse {
|
|
|
|
|
return &v1beta1.AdmissionResponse{
|
|
|
|
|
Allowed: false,
|
|
|
|
|
UID: request.UID,
|
|
|
|
|
Result: &metav1.Status {
|
|
|
|
|
Result: &metav1.Status{
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctxLog.WithField("patch", string(patchBytes)).Info("Generated patch")
|
|
|
|
|
return &v1beta1.AdmissionResponse {
|
|
|
|
|
return &v1beta1.AdmissionResponse{
|
|
|
|
|
Allowed: true,
|
|
|
|
|
Patch: patchBytes,
|
|
|
|
|
UID: request.UID,
|
|
|
|
@ -480,7 +480,7 @@ func main() {
|
|
|
|
|
"provisionerKid": provisionerKid,
|
|
|
|
|
}).Info("Loaded provisioner configuration")
|
|
|
|
|
|
|
|
|
|
provisioner, err := NewProvisioner(provisionerName, provisionerKid, config.CaUrl, rootCAPath, provisionerPasswordFile)
|
|
|
|
|
provisioner, err := NewProvisioner(provisionerName, provisionerKid, config.CaURL, rootCAPath, provisionerPasswordFile)
|
|
|
|
|
if err != nil {
|
|
|
|
|
log.Errorf("Error loading provisioner: %v", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
@ -557,9 +557,9 @@ func main() {
|
|
|
|
|
"body": body,
|
|
|
|
|
"error": err,
|
|
|
|
|
}).Error("Can't decode body")
|
|
|
|
|
response = &v1beta1.AdmissionResponse {
|
|
|
|
|
response = &v1beta1.AdmissionResponse{
|
|
|
|
|
Allowed: false,
|
|
|
|
|
Result: &metav1.Status {
|
|
|
|
|
Result: &metav1.Status{
|
|
|
|
|
Message: err.Error(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
@ -567,7 +567,7 @@ func main() {
|
|
|
|
|
response = mutate(&review, config, provisioner)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := json.Marshal(v1beta1.AdmissionReview {
|
|
|
|
|
resp, err := json.Marshal(v1beta1.AdmissionReview{
|
|
|
|
|
Response: response,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|