diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index fadd651..560e664 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -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 @@ -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/setup-ko@v0.4 + 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 \ No newline at end of file diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go index 68bb201..33869f7 100644 --- a/internal/cmd/cmd.go +++ b/internal/cmd/cmd.go @@ -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" @@ -33,6 +35,7 @@ type Config struct { metricsAddr string probeAddr string RegexStr string + IPPrefixesStr string MaxSec int K8sConfig *rest.Config DNSResolver controller.HostResolver @@ -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") @@ -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, }) @@ -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, @@ -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()) @@ -153,6 +183,7 @@ func prepareCmdlineConfig() *Config { metricsAddr: *metricsAddr, probeAddr: *probeAddr, RegexStr: *regexStr, + IPPrefixesStr: *ipPrefixesStr, BypassDNSResolution: *bypassDNSResolution, MaxSec: *maxSec, } diff --git a/internal/controller/csr_controller.go b/internal/controller/csr_controller.go index 8cd327f..17add91 100644 --- a/internal/controller/csr_controller.go +++ b/internal/controller/csr_controller.go @@ -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) diff --git a/internal/controller/csr_controller_test.go b/internal/controller/csr_controller_test.go index 0e67710..6042b5b 100644 --- a/internal/controller/csr_controller_test.go +++ b/internal/controller/csr_controller_test.go @@ -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) +} diff --git a/internal/controller/testenv_setup_test.go b/internal/controller/testenv_setup_test.go index c4eea02..a5d7148 100644 --- a/internal/controller/testenv_setup_test.go +++ b/internal/controller/testenv_setup_test.go @@ -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)