Merge branch 'master' into update-docker

This commit is contained in:
Mariano Cano 2019-04-09 12:18:56 -07:00 committed by GitHub
commit 572bf0de96
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 226 additions and 21 deletions

2
Gopkg.lock generated
View file

@ -298,7 +298,7 @@
"utils",
]
pruneopts = "UT"
revision = "a2e2d27fd5eff22ba94b1f2bd2fc946f5bb7f041"
revision = "3e1e2dcfa54298e0fb86e0be86ab36d79f36473e"
[[projects]]
branch = "master"

View file

@ -303,16 +303,20 @@ bundle-darwin: binary-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-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
@ -321,7 +325,7 @@ artifacts-tag: artifacts-linux-tag artifacts-darwin-tag
# For all builds that are not tagged
artifacts-master:
# For all build with a release candidate tag
# For all builds with a release-candidate (-rc) tag
artifacts-release-candidate: artifacts-tag
# For all builds with a release tag

View file

@ -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
certificate authority (CA)**. Building and operating a CA, issuing
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.
`step certificates` is part of smallstep's broader security architecture, which

View file

@ -174,3 +174,7 @@ $ kubectl get mutatingwebhookconfiguration
NAME CREATED AT
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

View file

@ -51,6 +51,18 @@ type Config struct {
Bootstrapper corev1.Container `yaml:"bootstrapper"`
Renewer corev1.Container `yaml:"renewer"`
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
@ -216,6 +228,7 @@ func mkBootstrapper(config *Config, commonName string, namespace string, provisi
Name: "COMMON_NAME",
Value: commonName,
})
b.Env = append(b.Env, corev1.EnvVar{
Name: "STEP_TOKEN",
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) {
var ops []PatchOperation
commonName := pod.ObjectMeta.GetAnnotations()[admissionWebhookAnnotationKey]
annotations := pod.ObjectMeta.GetAnnotations()
commonName := annotations[admissionWebhookAnnotationKey]
renewer := mkRenewer(config)
bootstrapper, err := mkBootstrapper(config, commonName, namespace, provisioner)
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
// 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`).
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()
if annotations == nil {
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
// mutated already (status key isn't set).
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
@ -418,7 +451,20 @@ func mutate(review *v1beta1.AdmissionReview, config *Config, provisioner Provisi
"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")
return &v1beta1.AdmissionResponse{
Allowed: true,

View 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.")
}
}

View file

@ -21,6 +21,8 @@ metadata:
data:
config.yaml: |
logFormat: json # or text
restrictCertificatesToNamespace: true
clusterDomain: cluster.local
caUrl: https://ca.step.svc.cluster.local
certLifetime: 24h
renewer:

View file

@ -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
Please enter the password to decrypt ~/.step/secrets/intermediate_ca_key: password
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
```
## 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.
![Google Dev Console](oidc1.png)
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**).
![Create credential](oidc2.png)
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.
![OIDC credentials](oidc3.png)
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.
In this section we recommend a few best practices when it comes to

View file

@ -88,9 +88,10 @@ e.g. `v1.0.2`
Travis will build and upload the following artifacts:
* **step-ca_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-ca_1.0.3_darwin_amd64.tar.gz**: tarball containing a statically compiled darwin binary.
* **step-certificates_1.0.3_amd64.deb**: debian package for installation on linux.
* **step-certificates_1.0.3_linux_amd64.tar.gz**: tarball containing a statically compiled linux 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!*

BIN
docs/oidc1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

BIN
docs/oidc2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
docs/oidc3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB