Skip to content

Commit

Permalink
feat: prefix based IP whitelisting
Browse files Browse the repository at this point in the history
  • Loading branch information
clementnuss committed Apr 1, 2022
1 parent 6b49658 commit accb23e
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 6 deletions.
35 changes: 35 additions & 0 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ jobs:
- check-gh-token
if: |
!startsWith(github.ref, 'refs/tags/v') &&
github.ref == 'refs/heads/dev' &&
!github.event.pull_request.head.repo.fork &&
needs.check-gh-token.outputs.gh-token == 'true'
runs-on: ubuntu-latest
Expand All @@ -203,3 +204,37 @@ jobs:
export REF=${{ github.ref}}
export COMMIT=${{ github.sha}}
ko publish ./cmd/kubelet-csr-approver/ --base-import-paths --platform=linux/amd64,linux/arm64,linux/arm
publish-feature:
needs:
- lint
- test
- check-gh-token
if: |
startsWith(github.ref, 'refs/heads/feat') &&
!startsWith(github.ref, 'refs/tags/v') &&
!github.event.pull_request.head.repo.fork &&
needs.check-gh-token.outputs.gh-token == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-go@v2
with:
go-version: 1.17
stable: true

- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: postfinance
password: ${{ secrets.DOCKERHUB_TOKEN }}
- uses: imjasonh/[email protected]
name: Setup ko
env:
KO_DOCKER_REPO: docker.io/postfinance
- name: Run ko publish
# TODO: make the tag correspond to the branch name
run: |
export REF=${{ github.ref}}
export COMMIT=${{ github.sha}}
ko publish ./cmd/kubelet-csr-approver/ --base-import-paths --platform=linux/amd64,linux/arm64,linux/arm -t feat
35 changes: 33 additions & 2 deletions internal/cmd/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import (
"net"
"os"
"regexp"
"strings"

"go.uber.org/zap/zapcore"
"inet.af/netaddr"
clientset "k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

Expand All @@ -33,6 +35,7 @@ type Config struct {
metricsAddr string
probeAddr string
RegexStr string
IPPrefixesStr string
MaxSec int
K8sConfig *rest.Config
DNSResolver controller.HostResolver
Expand Down Expand Up @@ -85,6 +88,27 @@ func CreateControllerManager(config *Config) (

providerRegexp := regexp.MustCompile(config.RegexStr)

// IP Prefixes parsing and IPSet construction
var setBuilder netaddr.IPSetBuilder

for _, ipPrefix := range strings.Split(config.IPPrefixesStr, ",") {
ipPref, err := netaddr.ParseIPPrefix(ipPrefix)
if err != nil {
z.V(-5).Info(fmt.Sprintf("Unable to parse IP prefix: %s, exiting", ipPrefix))

return nil, nil, 10
}

setBuilder.AddPrefix(ipPref)
}

providerIPSet, err := setBuilder.IPSet()
if err != nil {
z.V(-5).Info("Unable to build the Set of valid IP addresses, exiting")

return nil, nil, 10
}

if config.MaxSec < 0 || config.MaxSec > 367*24*3600 {
err := fmt.Errorf("the maximum expiration seconds env variable cannot be lower than 0 nor greater than 367 days")
z.Error(err, "reduce the maxExpirationSec value")
Expand All @@ -93,7 +117,7 @@ func CreateControllerManager(config *Config) (
}

ctrl.SetLogger(z)
mgr, err := ctrl.NewManager(config.K8sConfig, ctrl.Options{
mgr, err = ctrl.NewManager(config.K8sConfig, ctrl.Options{
MetricsBindAddress: config.metricsAddr,
HealthProbeBindAddress: config.probeAddr,
})
Expand All @@ -109,6 +133,7 @@ func CreateControllerManager(config *Config) (
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
ProviderRegexp: providerRegexp.MatchString,
ProviderIPSet: providerIPSet,
MaxExpirationSeconds: int32(config.MaxSec),
Resolver: config.DNSResolver,
BypassDNSResolution: config.BypassDNSResolution,
Expand Down Expand Up @@ -136,9 +161,14 @@ func prepareCmdlineConfig() *Config {
logLevel = fs.Int("level", 0, "level ranges from -5 (Fatal) to 10 (Verbose)")
metricsAddr = fs.String("metrics-bind-address", ":8080", "address the metric endpoint binds to.")
probeAddr = fs.String("health-probe-bind-address", ":8081", "address the probe endpoint binds to.")
regexStr = fs.String("provider-regex", "", "provider-specified regex to validate CSR SAN names against")
regexStr = fs.String("provider-regex", ".*", "provider-specified regex to validate CSR SAN names against. accepts everything unless specified")
maxSec = fs.Int("max-expiration-sec", 367*24*3600, "maximum seconds a CSR can request a cerficate for. defaults to 367 days")
bypassDNSResolution = fs.Bool("bypass-dns-resolution", false, "set this parameter to true to bypass DNS resolution checks")
ipPrefixesStr = fs.String("provider-ip-prefixes", "0.0.0.0/0,::/0",
`provider-specified, comma separated ip prefixes that CSR IP addresses shall fall into.
left unspecified, all IPv4/v6 are allowed. example prefix definition:
192.168.0.0/16,fc00/7`,
)
)

err := ff.Parse(fs, os.Args[1:], ff.WithEnvVarNoPrefix())
Expand All @@ -153,6 +183,7 @@ func prepareCmdlineConfig() *Config {
metricsAddr: *metricsAddr,
probeAddr: *probeAddr,
RegexStr: *regexStr,
IPPrefixesStr: *ipPrefixesStr,
BypassDNSResolution: *bypassDNSResolution,
MaxSec: *maxSec,
}
Expand Down
6 changes: 6 additions & 0 deletions internal/controller/csr_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,12 @@ func (r *CertificateSigningRequestReconciler) Reconcile(ctx context.Context, req
l.V(0).Info("Denying kubelet-serving CSR. DNS checks failed. Reason:" + reason)

appendCondition(&csr, false, reason)
} else if valid, reason, err := r.WhitelistedIPCheck(&csr, x509cr); !valid {
if err != nil {
l.V(0).Error(err, reason)
return res, err // returning a non-nil error to make this request be processed again in the reconcile function
}
l.V(0).Info("Denying kubelet-serving CSR. IP whitelist check failed. Reason:" + reason)
} else if csr.Spec.ExpirationSeconds != nil && *csr.Spec.ExpirationSeconds > r.MaxExpirationSeconds {
reason := "CSR spec.expirationSeconds is longer than the maximum allowed expiration second"
l.V(0).Info("Denying kubelet-serving CSR. Reason:" + reason)
Expand Down
24 changes: 24 additions & 0 deletions internal/controller/csr_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,27 @@ func TestBypassDNSResolution(t *testing.T) {
assert.True(t, approved)
assert.False(t, denied)
}

func TestIPNotWhitelisted(t *testing.T) {
csrParams := CsrParams{
csrName: "ip-non-whitelisted",
nodeName: testNodeName,
ipAddresses: []net.IP{{9, 9, 9, 9}},
dnsName: testNodeName + "-non-whitelisted.test.ch",
}
dnsResolver.Zones[csrParams.dnsName+"."] = mockdns.Zone{
A: []string{"9.9.9.9"},
}

csr := createCsr(t, csrParams)
_, nodeClientSet, _ := createControlPlaneUser(t, csr.Spec.Username, []string{"system:masters"})

_, err := nodeClientSet.CertificatesV1().CertificateSigningRequests().Create(
testContext, &csr, metav1.CreateOptions{})
require.Nil(t, err, "Could not create the CSR.")

approved, denied, err := waitCsrApprovalStatus(csr.Name)
require.Nil(t, err, "Could not retrieve the CSR to check its approval status")
assert.False(t, approved)
assert.True(t, denied)
}
9 changes: 5 additions & 4 deletions internal/controller/testenv_setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,11 @@ func packageSetup() {
}

testingConfig := cmd.Config{
RegexStr: `^[\w-]*\.test\.ch$`,
MaxSec: 367 * 24 * 3600,
K8sConfig: cfg,
DNSResolver: &dnsResolver,
RegexStr: `^[\w-]*\.test\.ch$`,
MaxSec: 367 * 24 * 3600,
K8sConfig: cfg,
DNSResolver: &dnsResolver,
IPPrefixesStr: "192.168.0.0/16",
}

csrCtrl, mgr, errorCode := cmd.CreateControllerManager(&testingConfig)
Expand Down

0 comments on commit accb23e

Please sign in to comment.