Skip to content

Commit 193679e

Browse files
committed
MCS: Validate provisioning token
Implements: openshift/enhancements#443 Requires: openshift/installer#4372 Basically we need to protect the Ignition config since it contains secrets (pull secret, kubeconfig) and we can't rely solely on network firewalling in all cases. The installer will generate a secret into both the pointer config and as a secret in our namespace, the MCS checks it.
1 parent 4e6d106 commit 193679e

File tree

7 files changed

+129
-4
lines changed

7 files changed

+129
-4
lines changed

manifests/machineconfigserver/clusterrole.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ rules:
77
- apiGroups: ["machineconfiguration.openshift.io"]
88
resources: ["machineconfigs", "machineconfigpools"]
99
verbs: ["*"]
10+
- apiGroups: [""]
11+
resources: ["configmaps", "secrets"]
12+
verbs: ["get", "list", "watch"]

manifests/machineconfigserver/daemonset.yaml

+6
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ spec:
4949
- name: certs
5050
secret:
5151
secretName: machine-config-server-tls
52+
# See https://github.com/openshift/enhancements/pull/443
53+
# This will only exist in 4.7+ clusters by default.
54+
- name: provisioning-token
55+
secret:
56+
secretName: provisioning-token
57+
optional: true

pkg/operator/assets/bindata.go

+9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/server/api.go

+15-4
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ const (
2727

2828
type poolRequest struct {
2929
machineConfigPool string
30-
version *semver.Version
30+
// The provisioning token, see https://github.com/openshift/enhancements/pull/443
31+
token string
32+
version *semver.Version
3133
}
3234

3335
// APIServer provides the HTTP(s) endpoint
@@ -114,7 +116,10 @@ func (sh *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
114116
poolName := path.Base(r.URL.Path)
115117
useragent := r.Header.Get("User-Agent")
116118
acceptHeader := r.Header.Get("Accept")
117-
glog.Infof("Pool %s requested by address:%q User-Agent:%q Accept-Header: %q", poolName, r.RemoteAddr, useragent, acceptHeader)
119+
q := r.URL.Query()
120+
token := q.Get("token")
121+
tokenProvided := token != ""
122+
glog.Infof("Pool %s requested by address:%q User-Agent:%q Accept-Header: %q TokenPresent: %v", poolName, r.RemoteAddr, useragent, acceptHeader, tokenProvided)
118123

119124
reqConfigVer, err := detectSpecVersionFromAcceptHeader(acceptHeader)
120125
if err != nil {
@@ -126,14 +131,20 @@ func (sh *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
126131

127132
cr := poolRequest{
128133
machineConfigPool: poolName,
134+
token: token,
129135
version: reqConfigVer,
130136
}
131137

132138
conf, err := sh.server.GetConfig(cr)
133139
if err != nil {
134140
w.Header().Set("Content-Length", "0")
135-
w.WriteHeader(http.StatusInternalServerError)
136-
glog.Errorf("couldn't get config for req: %v, error: %v", cr, err)
141+
if IsForbidden(err) {
142+
w.WriteHeader(http.StatusForbidden)
143+
glog.Infof("Denying unauthorized request: %v", err)
144+
} else {
145+
w.WriteHeader(http.StatusInternalServerError)
146+
glog.Errorf("couldn't get config for req: %v, error: %v", cr, err)
147+
}
137148
return
138149
}
139150
if conf == nil {

pkg/server/cluster_server.go

+41
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ import (
66
"fmt"
77
"io/ioutil"
88
"path/filepath"
9+
"time"
910

1011
yaml "github.com/ghodss/yaml"
1112
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
13+
"github.com/pkg/errors"
1214
corev1 "k8s.io/api/core/v1"
15+
apierrors "k8s.io/apimachinery/pkg/api/errors"
1316
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1417
"k8s.io/apimachinery/pkg/runtime"
18+
"k8s.io/client-go/kubernetes"
1519
rest "k8s.io/client-go/rest"
1620
clientcmd "k8s.io/client-go/tools/clientcmd"
1721
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
@@ -33,6 +37,8 @@ const (
3337
var _ = Server(&clusterServer{})
3438

3539
type clusterServer struct {
40+
client kubernetes.Interface
41+
3642
// machineClient is used to interact with the
3743
// machine config, pool objects.
3844
machineClient v1.MachineconfigurationV1Interface
@@ -52,16 +58,51 @@ func NewClusterServer(kubeConfig, apiserverURL string) (Server, error) {
5258
return nil, fmt.Errorf("Failed to create Kubernetes rest client: %v", err)
5359
}
5460

61+
client, err := kubernetes.NewForConfig(restConfig)
62+
if err != nil {
63+
return nil, errors.Wrapf(err, "creating core client")
64+
}
65+
5566
mc := v1.NewForConfigOrDie(restConfig)
5667
return &clusterServer{
68+
client: client,
5769
machineClient: mc,
5870
kubeconfigFunc: func() ([]byte, []byte, error) { return kubeconfigFromSecret(bootstrapTokenDir, apiserverURL) },
5971
}, nil
6072
}
6173

74+
// authorizeRequest checks the provided token
75+
func (cs *clusterServer) authorizeRequest(cr poolRequest) (bool, error) {
76+
s, err := cs.client.CoreV1().Secrets("openshift-machine-config-operator").Get(context.TODO(), "provisioning-token", metav1.GetOptions{})
77+
if err != nil {
78+
if !apierrors.IsNotFound(err) {
79+
return false, errors.Wrapf(err, "Fetching provisioning-token")
80+
}
81+
// If the cluster doesn't have a `provisioning-token` secret, we don't require it.
82+
return true, nil
83+
}
84+
// Unconditionally sleep to mitigate brute force attacks
85+
time.Sleep(1 * time.Second)
86+
if cr.token != string(s.Data["token"]) {
87+
return false, nil
88+
}
89+
return true, nil
90+
}
91+
6292
// GetConfig fetches the machine config(type - Ignition) from the cluster,
6393
// based on the pool request.
6494
func (cs *clusterServer) GetConfig(cr poolRequest) (*runtime.RawExtension, error) {
95+
authorized, err := cs.authorizeRequest(cr)
96+
if err != nil {
97+
return nil, err
98+
}
99+
if !authorized {
100+
return nil, &configError{
101+
msg: "Provided token is invalid",
102+
forbidden: true,
103+
}
104+
}
105+
65106
mp, err := cs.machineClient.MachineConfigPools().Get(context.TODO(), cr.machineConfigPool, metav1.GetOptions{})
66107
if err != nil {
67108
return nil, fmt.Errorf("could not fetch pool. err: %v", err)

pkg/server/server.go

+21
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,27 @@ type kubeconfigFunc func() (kubeconfigData []byte, rootCAData []byte, err error)
3737
// appenderFunc appends Config.
3838
type appenderFunc func(*igntypes.Config, *mcfgv1.MachineConfig) error
3939

40+
// configError is returned by the GetConfig API
41+
type configError struct {
42+
msg string
43+
forbidden bool
44+
}
45+
46+
// configError returns the string
47+
func (e *configError) Error() string {
48+
return e.msg
49+
}
50+
51+
// IsForbidden says if err is an configError with forbidden set
52+
func IsForbidden(err error) bool {
53+
switch t := err.(type) {
54+
case *configError:
55+
return t.forbidden
56+
default:
57+
return false
58+
}
59+
}
60+
4061
// Server defines the interface that is implemented by different
4162
// machine config server implementations.
4263
type Server interface {

pkg/server/server_test.go

+34
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/stretchr/testify/assert"
2121
corev1 "k8s.io/api/core/v1"
2222
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23+
k8sfake "k8s.io/client-go/kubernetes/fake"
2324

2425
mcfgv1 "github.com/openshift/machine-config-operator/pkg/apis/machineconfiguration.openshift.io/v1"
2526
ctrlcommon "github.com/openshift/machine-config-operator/pkg/controller/common"
@@ -243,6 +244,8 @@ func TestClusterServer(t *testing.T) {
243244
t.Fatalf("unexpected error while unmarshaling machine-config: %s, err: %v", mcPath, err)
244245
}
245246

247+
basecs := k8sfake.NewSimpleClientset()
248+
246249
cs := fake.NewSimpleClientset()
247250
_, err = cs.MachineconfigurationV1().MachineConfigPools().Create(context.TODO(), mp, metav1.CreateOptions{})
248251
if err != nil {
@@ -254,6 +257,7 @@ func TestClusterServer(t *testing.T) {
254257
}
255258

256259
csc := &clusterServer{
260+
client: basecs,
257261
machineClient: cs.MachineconfigurationV1(),
258262
kubeconfigFunc: func() ([]byte, []byte, error) { return getKubeConfigContent(t) },
259263
}
@@ -320,6 +324,36 @@ func TestClusterServer(t *testing.T) {
320324
if !foundEncapsulated {
321325
t.Errorf("missing %s", daemonconsts.MachineConfigEncapsulatedPath)
322326
}
327+
328+
// Test https://github.com/openshift/enhancements/pull/443
329+
provisioningSecret := &corev1.Secret{
330+
ObjectMeta: metav1.ObjectMeta{Name: "provisioning-token"},
331+
Data: map[string][]byte{
332+
"token": []byte("somesecrettoken"),
333+
},
334+
}
335+
basecs.CoreV1().Secrets("openshift-machine-config-operator").Create(context.TODO(), provisioningSecret, metav1.CreateOptions{})
336+
337+
// Do a request without a token
338+
res, err = csc.GetConfig(poolRequest{
339+
machineConfigPool: testPool,
340+
})
341+
assert.Error(t, err)
342+
343+
// Incorrect token
344+
res, err = csc.GetConfig(poolRequest{
345+
token: "someothertoken",
346+
machineConfigPool: testPool,
347+
})
348+
assert.Error(t, err)
349+
350+
// Valid token
351+
res, err = csc.GetConfig(poolRequest{
352+
token: "somesecrettoken",
353+
machineConfigPool: testPool,
354+
})
355+
assert.Nil(t, err)
356+
assert.NotNil(t, res)
323357
}
324358

325359
func getKubeConfigContent(t *testing.T) ([]byte, []byte, error) {

0 commit comments

Comments
 (0)