forked from TrueCloudLab/certificates
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",
|
||||
]
|
||||
pruneopts = "UT"
|
||||
revision = "a2e2d27fd5eff22ba94b1f2bd2fc946f5bb7f041"
|
||||
revision = "3e1e2dcfa54298e0fb86e0be86ab36d79f36473e"
|
||||
|
||||
[[projects]]
|
||||
branch = "master"
|
||||
|
|
12
Makefile
12
Makefile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -45,12 +45,24 @@ const (
|
|||
|
||||
// Config options for the autocert admission controller.
|
||||
type Config struct {
|
||||
LogFormat string `yaml:"logFormat"`
|
||||
CaURL string `yaml:"caUrl"`
|
||||
CertLifetime string `yaml:"certLifetime"`
|
||||
Bootstrapper corev1.Container `yaml:"bootstrapper"`
|
||||
Renewer corev1.Container `yaml:"renewer"`
|
||||
CertsVolume corev1.Volume `yaml:"certsVolume"`
|
||||
LogFormat string `yaml:"logFormat"`
|
||||
CaURL string `yaml:"caUrl"`
|
||||
CertLifetime string `yaml:"certLifetime"`
|
||||
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,
|
||||
|
|
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:
|
||||
config.yaml: |
|
||||
logFormat: json # or text
|
||||
restrictCertificatesToNamespace: true
|
||||
clusterDomain: cluster.local
|
||||
caUrl: https://ca.step.svc.cluster.local
|
||||
certLifetime: 24h
|
||||
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
|
||||
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
|
||||
|
|
|
@ -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
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…
Reference in a new issue