Merge branch 'master' into update-docker
This commit is contained in:
commit
572bf0de96
12 changed files with 226 additions and 21 deletions
2
Gopkg.lock
generated
2
Gopkg.lock
generated
|
@ -298,7 +298,7 @@
|
||||||
"utils",
|
"utils",
|
||||||
]
|
]
|
||||||
pruneopts = "UT"
|
pruneopts = "UT"
|
||||||
revision = "a2e2d27fd5eff22ba94b1f2bd2fc946f5bb7f041"
|
revision = "3e1e2dcfa54298e0fb86e0be86ab36d79f36473e"
|
||||||
|
|
||||||
[[projects]]
|
[[projects]]
|
||||||
branch = "master"
|
branch = "master"
|
||||||
|
|
12
Makefile
12
Makefile
|
@ -303,16 +303,20 @@ bundle-darwin: binary-darwin
|
||||||
.PHONY: binary-linux binary-darwin bundle-linux bundle-darwin
|
.PHONY: binary-linux binary-darwin bundle-linux bundle-darwin
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
# Targets for creating OS specific artifacts
|
# Targets for creating OS specific artifacts and archives
|
||||||
#################################################
|
#################################################
|
||||||
|
|
||||||
artifacts-linux-tag: bundle-linux debian
|
artifacts-linux-tag: bundle-linux debian
|
||||||
|
|
||||||
artifacts-darwin-tag: bundle-darwin
|
artifacts-darwin-tag: bundle-darwin
|
||||||
|
|
||||||
artifacts-tag: artifacts-linux-tag artifacts-darwin-tag
|
artifacts-archive-tag:
|
||||||
|
$Q mkdir -p $(RELEASE)
|
||||||
|
$Q git archive v$(VERSION) | gzip > $(RELEASE)/step-certificates_$(VERSION).tar.gz
|
||||||
|
|
||||||
.PHONY: artifacts-linux-tag artifacts-darwin-tag artifacts-tag
|
artifacts-tag: artifacts-linux-tag artifacts-darwin-tag artifacts-archive-tag
|
||||||
|
|
||||||
|
.PHONY: artifacts-linux-tag artifacts-darwin-tag artifacts-archive-tag artifacts-tag
|
||||||
|
|
||||||
#################################################
|
#################################################
|
||||||
# Targets for creating step artifacts
|
# Targets for creating step artifacts
|
||||||
|
@ -321,7 +325,7 @@ artifacts-tag: artifacts-linux-tag artifacts-darwin-tag
|
||||||
# For all builds that are not tagged
|
# For all builds that are not tagged
|
||||||
artifacts-master:
|
artifacts-master:
|
||||||
|
|
||||||
# For all build with a release candidate tag
|
# For all builds with a release-candidate (-rc) tag
|
||||||
artifacts-release-candidate: artifacts-tag
|
artifacts-release-candidate: artifacts-tag
|
||||||
|
|
||||||
# For all builds with a release tag
|
# For all builds with a release tag
|
||||||
|
|
|
@ -42,7 +42,7 @@ mTLS](https://raw.githubusercontent.com/smallstep/certificates/master/images/con
|
||||||
There's just one problem: **you need certificates issued by your own
|
There's just one problem: **you need certificates issued by your own
|
||||||
certificate authority (CA)**. Building and operating a CA, issuing
|
certificate authority (CA)**. Building and operating a CA, issuing
|
||||||
certificates, and making sure they're renewed before they expire is tricky.
|
certificates, and making sure they're renewed before they expire is tricky.
|
||||||
This project provides the infratructure, automations, and workflows you'll
|
This project provides the infrastructure, automations, and workflows you'll
|
||||||
need.
|
need.
|
||||||
|
|
||||||
`step certificates` is part of smallstep's broader security architecture, which
|
`step certificates` is part of smallstep's broader security architecture, which
|
||||||
|
|
|
@ -174,3 +174,7 @@ $ kubectl get mutatingwebhookconfiguration
|
||||||
NAME CREATED AT
|
NAME CREATED AT
|
||||||
autocert-webhook-config 2019-01-17T22:57:57Z
|
autocert-webhook-config 2019-01-17T22:57:57Z
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Move on to usage instructions
|
||||||
|
|
||||||
|
Make sure to follow the autocert usage steps at https://github.com/smallstep/certificates/tree/master/autocert#usage
|
||||||
|
|
|
@ -51,6 +51,18 @@ type Config struct {
|
||||||
Bootstrapper corev1.Container `yaml:"bootstrapper"`
|
Bootstrapper corev1.Container `yaml:"bootstrapper"`
|
||||||
Renewer corev1.Container `yaml:"renewer"`
|
Renewer corev1.Container `yaml:"renewer"`
|
||||||
CertsVolume corev1.Volume `yaml:"certsVolume"`
|
CertsVolume corev1.Volume `yaml:"certsVolume"`
|
||||||
|
RestrictCertificatesToNamespace bool `yaml:"restrictCertificatesToNamespace"`
|
||||||
|
ClusterDomain string `yaml:"clusterDomain"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetClusterDomain returns the Kubernetes cluster domain, defaults to
|
||||||
|
// "cluster.local" if not specified in the configuration.
|
||||||
|
func (c Config) GetClusterDomain() string {
|
||||||
|
if c.ClusterDomain != "" {
|
||||||
|
return c.ClusterDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
return "cluster.local"
|
||||||
}
|
}
|
||||||
|
|
||||||
// PatchOperation represents a RFC6902 JSONPatch Operation
|
// PatchOperation represents a RFC6902 JSONPatch Operation
|
||||||
|
@ -216,6 +228,7 @@ func mkBootstrapper(config *Config, commonName string, namespace string, provisi
|
||||||
Name: "COMMON_NAME",
|
Name: "COMMON_NAME",
|
||||||
Value: commonName,
|
Value: commonName,
|
||||||
})
|
})
|
||||||
|
|
||||||
b.Env = append(b.Env, corev1.EnvVar{
|
b.Env = append(b.Env, corev1.EnvVar{
|
||||||
Name: "STEP_TOKEN",
|
Name: "STEP_TOKEN",
|
||||||
ValueFrom: &corev1.EnvVarSource{
|
ValueFrom: &corev1.EnvVarSource{
|
||||||
|
@ -357,7 +370,8 @@ func addAnnotations(existing, new map[string]string) (ops []PatchOperation) {
|
||||||
func patch(pod *corev1.Pod, namespace string, config *Config, provisioner Provisioner) ([]byte, 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]
|
annotations := pod.ObjectMeta.GetAnnotations()
|
||||||
|
commonName := annotations[admissionWebhookAnnotationKey]
|
||||||
renewer := mkRenewer(config)
|
renewer := mkRenewer(config)
|
||||||
bootstrapper, err := mkBootstrapper(config, commonName, namespace, provisioner)
|
bootstrapper, err := mkBootstrapper(config, commonName, namespace, provisioner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -376,7 +390,10 @@ func patch(pod *corev1.Pod, namespace string, config *Config, provisioner Provis
|
||||||
// shouldMutate checks whether a pod is subject to mutation by this admission controller. A pod
|
// shouldMutate checks whether a pod is subject to mutation by this admission controller. A pod
|
||||||
// is subject to mutation if it's annotated with the `admissionWebhookAnnotationKey` and if it
|
// is subject to mutation if it's annotated with the `admissionWebhookAnnotationKey` and if it
|
||||||
// has not already been processed (indicated by `admissionWebhookStatusKey` set to `injected`).
|
// has not already been processed (indicated by `admissionWebhookStatusKey` set to `injected`).
|
||||||
func shouldMutate(metadata *metav1.ObjectMeta) bool {
|
// If the pod requests a certificate with a subject matching a namespace other than its own
|
||||||
|
// and restrictToNamespace is true, then shouldMutate will return a validation error
|
||||||
|
// that should be returned to the client.
|
||||||
|
func shouldMutate(metadata *metav1.ObjectMeta, namespace string, clusterDomain string, restrictToNamespace bool) (bool, error) {
|
||||||
annotations := metadata.GetAnnotations()
|
annotations := metadata.GetAnnotations()
|
||||||
if annotations == nil {
|
if annotations == nil {
|
||||||
annotations = map[string]string{}
|
annotations = map[string]string{}
|
||||||
|
@ -385,10 +402,26 @@ func shouldMutate(metadata *metav1.ObjectMeta) bool {
|
||||||
// Only mutate if the object is annotated appropriately (annotation key set) and we haven't
|
// Only mutate if the object is annotated appropriately (annotation key set) and we haven't
|
||||||
// mutated already (status key isn't set).
|
// mutated already (status key isn't set).
|
||||||
if annotations[admissionWebhookAnnotationKey] == "" || annotations[admissionWebhookStatusKey] == "injected" {
|
if annotations[admissionWebhookAnnotationKey] == "" || annotations[admissionWebhookStatusKey] == "injected" {
|
||||||
return false
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
if !restrictToNamespace {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
subject := strings.Trim(annotations[admissionWebhookAnnotationKey], ".")
|
||||||
|
|
||||||
|
err := fmt.Errorf("subject \"%s\" matches a namespace other than \"%s\" and is not permitted. This check can be disabled by setting restrictCertificatesToNamespace to false in the autocert-config ConfigMap", subject, namespace)
|
||||||
|
|
||||||
|
if strings.HasSuffix(subject, ".svc") && !strings.HasSuffix(subject, fmt.Sprintf(".%s.svc", namespace)) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(subject, fmt.Sprintf(".svc.%s", clusterDomain)) && !strings.HasSuffix(subject, fmt.Sprintf(".%s.svc.%s", namespace, clusterDomain)) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// mutate takes an `AdmissionReview`, determines whether it is subject to mutation, and returns
|
// mutate takes an `AdmissionReview`, determines whether it is subject to mutation, and returns
|
||||||
|
@ -418,7 +451,20 @@ func mutate(review *v1beta1.AdmissionReview, config *Config, provisioner Provisi
|
||||||
"user": request.UserInfo,
|
"user": request.UserInfo,
|
||||||
})
|
})
|
||||||
|
|
||||||
if !shouldMutate(&pod.ObjectMeta) {
|
mutationAllowed, validationErr := shouldMutate(&pod.ObjectMeta, request.Namespace, config.GetClusterDomain(), config.RestrictCertificatesToNamespace)
|
||||||
|
|
||||||
|
if validationErr != nil {
|
||||||
|
ctxLog.WithField("error", validationErr).Info("Validation error")
|
||||||
|
return &v1beta1.AdmissionResponse{
|
||||||
|
Allowed: false,
|
||||||
|
UID: request.UID,
|
||||||
|
Result: &metav1.Status{
|
||||||
|
Message: validationErr.Error(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !mutationAllowed {
|
||||||
ctxLog.WithField("annotations", pod.Annotations).Info("Skipping mutation")
|
ctxLog.WithField("annotations", pod.Annotations).Info("Skipping mutation")
|
||||||
return &v1beta1.AdmissionResponse{
|
return &v1beta1.AdmissionResponse{
|
||||||
Allowed: true,
|
Allowed: true,
|
||||||
|
|
75
autocert/controller/main_test.go
Normal file
75
autocert/controller/main_test.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGetClusterDomain(t *testing.T) {
|
||||||
|
c := Config{}
|
||||||
|
if c.GetClusterDomain() != "cluster.local" {
|
||||||
|
t.Errorf("cluster domain should default to cluster.local, not: %s", c.GetClusterDomain())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.ClusterDomain = "mydomain.com"
|
||||||
|
if c.GetClusterDomain() != "mydomain.com" {
|
||||||
|
t.Errorf("cluster domain should default to cluster.local, not: %s", c.GetClusterDomain())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldMutate(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
description string
|
||||||
|
subject string
|
||||||
|
namespace string
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{"full cluster domain", "test.default.svc.cluster.local", "default", true},
|
||||||
|
{"full cluster domain wrong ns", "test.default.svc.cluster.local", "kube-system", false},
|
||||||
|
{"left dots get stripped", ".test.default.svc.cluster.local", "default", true},
|
||||||
|
{"left dots get stripped wrong ns", ".test.default.svc.cluster.local", "kube-system", false},
|
||||||
|
{"right dots get stripped", "test.default.svc.cluster.local.", "default", true},
|
||||||
|
{"right dots get stripped wrong ns", "test.default.svc.cluster.local.", "kube-system", false},
|
||||||
|
{"dots get stripped", ".test.default.svc.cluster.local.", "default", true},
|
||||||
|
{"dots get stripped wrong ns", ".test.default.svc.cluster.local.", "kube-system", false},
|
||||||
|
{"partial cluster domain", "test.default.svc.cluster", "default", true},
|
||||||
|
{"partial cluster domain wrong ns is still allowed because not valid hostname", "test.default.svc.cluster", "kube-system", true},
|
||||||
|
{"service domain", "test.default.svc", "default", true},
|
||||||
|
{"service domain wrong ns", "test.default.svc", "kube-system", false},
|
||||||
|
{"two part domain", "test.default", "default", true},
|
||||||
|
{"two part domain different ns", "test.default", "kube-system", true},
|
||||||
|
{"one hostname", "test", "default", true},
|
||||||
|
{"no subject specified", "", "default", false},
|
||||||
|
{"three part not cluster", "test.default.com", "kube-system", true},
|
||||||
|
{"four part not cluster", "test.default.svc.com", "kube-system", true},
|
||||||
|
{"five part not cluster", "test.default.svc.cluster.com", "kube-system", true},
|
||||||
|
{"six part not cluster", "test.default.svc.cluster.local.com", "kube-system", true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
t.Run(testCase.description, func(t *testing.T) {
|
||||||
|
mutationAllowed, validationErr := shouldMutate(&metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
admissionWebhookAnnotationKey: testCase.subject,
|
||||||
|
},
|
||||||
|
}, testCase.namespace, "cluster.local", true)
|
||||||
|
if mutationAllowed != testCase.expected {
|
||||||
|
t.Errorf("shouldMutate did not return %t for %s", testCase.expected, testCase.description)
|
||||||
|
}
|
||||||
|
if testCase.subject != "" && mutationAllowed == false && validationErr == nil {
|
||||||
|
t.Errorf("shouldMutate should return validation error for invalid hostname")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestShouldMutateNotRestrictToNamespace(t *testing.T) {
|
||||||
|
mutationAllowed, _ := shouldMutate(&metav1.ObjectMeta{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
admissionWebhookAnnotationKey: "test.default.svc.cluster.local",
|
||||||
|
},
|
||||||
|
}, "kube-system", "cluster.local", false)
|
||||||
|
if mutationAllowed == false {
|
||||||
|
t.Errorf("shouldMutate should return true even with a wrong namespace if restrictToNamespace is false.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,6 +21,8 @@ metadata:
|
||||||
data:
|
data:
|
||||||
config.yaml: |
|
config.yaml: |
|
||||||
logFormat: json # or text
|
logFormat: json # or text
|
||||||
|
restrictCertificatesToNamespace: true
|
||||||
|
clusterDomain: cluster.local
|
||||||
caUrl: https://ca.step.svc.cluster.local
|
caUrl: https://ca.step.svc.cluster.local
|
||||||
certLifetime: 24h
|
certLifetime: 24h
|
||||||
renewer:
|
renewer:
|
||||||
|
|
|
@ -421,7 +421,7 @@ Please enter a password to encrypt the provisioner private key? password
|
||||||
},
|
},
|
||||||
[...]
|
[...]
|
||||||
|
|
||||||
# launch CA...
|
## launch CA...
|
||||||
$ step-ca $(step path)/config/ca.json
|
$ step-ca $(step path)/config/ca.json
|
||||||
Please enter the password to decrypt ~/.step/secrets/intermediate_ca_key: password
|
Please enter the password to decrypt ~/.step/secrets/intermediate_ca_key: password
|
||||||
2019/02/21 12:09:51 Serving HTTPS on :9443 ...
|
2019/02/21 12:09:51 Serving HTTPS on :9443 ...
|
||||||
|
@ -456,6 +456,79 @@ $ step ca renew site.crt site.key
|
||||||
error renewing certificate: Unauthorized
|
error renewing certificate: Unauthorized
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Use Oauth OIDC to obtain personal certificates
|
||||||
|
|
||||||
|
To authenticate users with the CA you can leverage services that expose OAuth
|
||||||
|
OpenID Connect identity providers. One of the most common providers, and the
|
||||||
|
one we'll use in this example, is G-Suite.
|
||||||
|
|
||||||
|
Navigate to the Google APIs developer console and pick a suitable project from the
|
||||||
|
top navbar's dropdown.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
In the masthead navigation click **Credentials** (key symbol) and then "OAuth
|
||||||
|
consent screen" from the subnav. Fill out naming details, all mandatory fields,
|
||||||
|
and decide if your app is of type **Public** or **Internal**. Internal
|
||||||
|
will make sure the access scope is bound to your G-Suite organization.
|
||||||
|
**Public** will let anybody with a Google Account log in, incl.
|
||||||
|
`gmail.com` accounts.
|
||||||
|
|
||||||
|
Move back to **Credentials** on the subnav and choose "OAuth client ID" from the
|
||||||
|
**Create credentials** dropdown. Since OIDC will be used from the `step CLI` pick **Other**
|
||||||
|
from the available options and pick a name (e.g. **Step CLI**).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
On successful completion, a confirmation modal with both `clientID` and
|
||||||
|
`clientSecret` will be presented. Please note that the `clientSecret` will
|
||||||
|
allow applications access to the configured OAuth consent screen. However, it
|
||||||
|
will not allow direct authentication of users without their own MfA credentials
|
||||||
|
per account.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Now using `clientID` and `clientSecret` run the following command to add
|
||||||
|
G-Suite as a provisioner to `step certificates`. Please see [`step ca
|
||||||
|
provisioner add`](https://smallstep.com/docs/cli/ca/provisioner/add/)'s docs
|
||||||
|
for all available configuration options and descriptions.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ step ca provisioner add Google --type oidc --ca-config $(step path)/config/ca.json \
|
||||||
|
--client-id 972437157139-ssiqna0g4ibuhafl3pkrrcb52tbroekt.apps.googleusercontent.com \
|
||||||
|
--client-secret RjEk-GwKBvdsFAICiJhn_RiF \
|
||||||
|
--configuration-endpoint https://accounts.google.com/.well-known/openid-configuration \
|
||||||
|
--domain yourdomain.com --domain gmail.com
|
||||||
|
```
|
||||||
|
|
||||||
|
Start up the online CA or send a HUP signal if it's already running to reload
|
||||||
|
the configuration and pick up the new provisioner. Now users should be able to
|
||||||
|
obtain certificates using the familiar `step ca certificate` flow:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ step ca certificate sebastian@smallstep.com personal.crt personal.key
|
||||||
|
Use the arrow keys to navigate: ↓ ↑ → ←
|
||||||
|
What provisioner key do you want to use?
|
||||||
|
fYDoiQdYueq_LAXx2kqA4N_Yjf_eybe-wari7Js5iXI (admin)
|
||||||
|
▸ 972437157139-ssiqna0g4ibuhafl3pkrrcb52tbroekt.apps.googleusercontent.com (Google)
|
||||||
|
✔ Key ID: 972437157139-ssiqna0g4ibuhafl3pkrrcb52tbroekt.apps.googleusercontent.com (Google)
|
||||||
|
✔ CA: https://localhost
|
||||||
|
✔ Certificate: personal.crt
|
||||||
|
✔ Private Key: personal.key
|
||||||
|
|
||||||
|
$ step certificate inspect --short personal.crt ⏎
|
||||||
|
X.509v3 TLS Certificate (ECDSA P-256) [Serial: 6169...4235]
|
||||||
|
Subject: 106202051347258973689
|
||||||
|
sebastian@smallstep.com
|
||||||
|
Issuer: Local CA Intermediate CA
|
||||||
|
Provisioner: Google [ID: 9724....com]
|
||||||
|
Valid from: 2019-03-26T20:36:28Z
|
||||||
|
to: 2019-03-27T20:36:28Z
|
||||||
|
```
|
||||||
|
|
||||||
|
Now it's easy for anybody in the G-Suite organization to obtain valid personal
|
||||||
|
certificates!
|
||||||
|
|
||||||
## Notes on Securing the Step CA and your PKI.
|
## Notes on Securing the Step CA and your PKI.
|
||||||
|
|
||||||
In this section we recommend a few best practices when it comes to
|
In this section we recommend a few best practices when it comes to
|
||||||
|
|
|
@ -88,9 +88,10 @@ e.g. `v1.0.2`
|
||||||
|
|
||||||
Travis will build and upload the following artifacts:
|
Travis will build and upload the following artifacts:
|
||||||
|
|
||||||
* **step-ca_1.0.3_amd64.deb**: debian package for installation on linux.
|
* **step-certificates_1.0.3_amd64.deb**: debian package for installation on linux.
|
||||||
* **step-ca_1.0.3_linux_amd64.tar.gz**: tarball containing a statically compiled linux binary.
|
* **step-certificates_1.0.3_linux_amd64.tar.gz**: tarball containing a statically compiled linux binary.
|
||||||
* **step-ca_1.0.3_darwin_amd64.tar.gz**: tarball containing a statically compiled darwin binary.
|
* **step-certificates_1.0.3_darwin_amd64.tar.gz**: tarball containing a statically compiled darwin binary.
|
||||||
|
* **step-certificates.tar.gz**: tarball containing a git archive of the full repo.
|
||||||
|
|
||||||
*All Done!*
|
*All Done!*
|
||||||
|
|
||||||
|
|
BIN
docs/oidc1.png
Normal file
BIN
docs/oidc1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 59 KiB |
BIN
docs/oidc2.png
Normal file
BIN
docs/oidc2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
docs/oidc3.png
Normal file
BIN
docs/oidc3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 57 KiB |
Loading…
Add table
Reference in a new issue