David's Tech Blog

Ramblings of a random Irish nerd.

03 Jan 2020

Securing your Kubernetes WebApp on a Shoestring.

Here we're going to cover:

It's a pre-requisite that the ingress-nginx controller is being used.

Installing Cert-Manager

Cert-Manager makes managing SSL certs on your Kubernetes cluster child's play. It watches your Kubernetes ingress resources for changes and if you add the required annotations, it will try to issue an SSL cert from the provider you configure. In this case we can use LetsEncrypt.

Thanks to the Cert-Manager team, from v0.12.0 it now has multi-arch builds, which means it has now an arm build and just works.

To install it on your cluster simply issue:

kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v0.12.0/cert-manager.yaml

You will see the Cert-Manager pods come up:

➜  ~ kubectl get pods -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-webhook-547567b88f-wvx5k      1/1     Running   0          16m
cert-manager-5c47f46f57-kdsz2              1/1     Running   0          16m
cert-manager-cainjector-6659d6844d-xlsqp   1/1     Running   0          16m

Lets Encrypt

Now you must set up LetsEncrypt as a cluster issuer. This is quite simple also.

Create a manifest like this one:

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
 name: letsencrypt-prod
spec:
 acme:
   # The ACME server URL
   server: https://acme-v02.api.letsencrypt.org/directory
   # Email address used for ACME registration
   email: replaceme@example.com
   # Name of a secret used to store the ACME account private key
   privateKeySecretRef:
     name: letsencrypt-prod
   # Enable the HTTP-01 challenge provider
   solvers:
   - http01:
       ingress:
         class: nginx

Make sure to replace replaceme@example.com with your actual email address

After you've loaded the manifest it should be visible using this command:

➜  ~ kubectl get clusterissuer
NAME               READY   AGE
letsencrypt-prod   True    5d20h

At this point we should be able to deploy an ingress definition that requests a cert.

It's important the the DNS entry for the domain you want the cert for points to the load balancer.

➜  ~ kubectl get svc -n ingress-nginx
NAME            TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)                      AGE
ingress-nginx   LoadBalancer   10.43.81.154   188.141.110.72   80:30712/TCP,443:31239/TCP   5d21h

Cert issuance.

This is done by putting the correct annotations on an ingress definition:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: basic-ingress
  namespace: blogdemo
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/ingress.class: "nginx"

spec:
  tls:
  - hosts:
    - blogdemo.home.dmarkey.com
    secretName: blogdemo-tls
  rules:
  - host: blogdemo.home.dmarkey.com
    http:
      paths:
      - backend:
          serviceName: my-service
          servicePort: 80

After you create this ingress resource, the cert-manager pod will create a corresponding cert resource.

See the cert resources in your namespace by using this command.

➜  ~ kubectl get cert -n blogdemo
NAME           READY   SECRET         AGE
blogdemo-tls   False   blogdemo-tls   9s

Notice the Ready status is False. This means the cert has not been completely initialised yet.

If everything is configured correctly, this should switch to True, and then the cert should be ready(takes upto 30s).

➜  ~ kubectl get cert -n blogdemo
NAME           READY   SECRET         AGE
blogdemo-tls   True    blogdemo-tls   14m

You can see the new cert in action using curl.

  ~ curl -vvv https://blogdemo.home.dmarkey.com
*   Trying 188.141.110.72:443...
* TCP_NODELAY set
* Connected to blogdemo.home.dmarkey.com (188.141.110.72) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* successfully set certificate verify locations:
*   CAfile: /etc/ssl/certs/ca-certificates.crt
  CApath: none
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Server key exchange (12):
* TLSv1.2 (IN), TLS handshake, Server finished (14):
* TLSv1.2 (OUT), TLS handshake, Client key exchange (16):
* TLSv1.2 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS handshake, Finished (20):
* TLSv1.2 (IN), TLS handshake, Finished (20):
* SSL connection using TLSv1.2 / ECDHE-RSA-AES256-GCM-SHA384
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=blogdemo.home.dmarkey.com
*  start date: Jan  3 16:37:57 2020 GMT
*  expire date: Apr  2 16:37:57 2020 GMT
*  subjectAltName: host "blogdemo.home.dmarkey.com" matched cert's "blogdemo.home.dmarkey.com"
*  issuer: C=US; O=Let's Encrypt; CN=Let's Encrypt Authority X3
*  SSL certificate verify ok.
* Using HTTP2, server supports multi-use
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* Using Stream ID: 1 (easy handle 0xf36148)
> GET / HTTP/2
> Host: blogdemo.home.dmarkey.com
> user-agent: curl/7.67.0
> accept: */*
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 128)!
< HTTP/2 503
< server: openresty/1.15.8.2
< date: Fri, 03 Jan 2020 17:52:59 GMT
< content-type: text/html
< content-length: 203
< strict-transport-security: max-age=15724800; includeSubDomains

Modsecurity Firewall

The final piece is to protect your site with a WAF(Web Application Firewall).

Ingress-nginx comes with the excellent Modsecurity by default, and it also comes with the OWASP ModSecurity Core Rule Set (CRS) which protects against various different attacks.

The documentation for the modsecurity 3rd party add on is here: here but it does detail that the firewall is by default in Audit mode only and will not block any traffic.

Given our ingress example from earlier, here is a further example with the ModSecurity WAF in blocking mode.

piVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: basic-ingress
  namespace: blogdemo
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/ingress.class: "nginx"
    nginx.ingress.kubernetes.io/enable-modsecurity: "true"
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
        SecRuleEngine On
        SecRequestBodyAccess On
        SecAuditEngine RelevantOnly
        SecAuditLogParts ABIJDEFHZ
        SecAuditLogFormat JSON
        SecAuditLogType Serial
        SecAuditLog /dev/stdout
        Include /etc/nginx/owasp-modsecurity-crs/crs-setup.conf
        SecAction \"id:900200,phase:1,nolog,pass,t:none,setvar:\'tx.allowed_methods=GET\'\"
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-901-INITIALIZATION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-903.9001-DRUPAL-EXCLUSION-RULES.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-903.9002-WORDPRESS-EXCLUSION-RULES.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-905-COMMON-EXCEPTIONS.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-910-IP-REPUTATION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-911-METHOD-ENFORCEMENT.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-912-DOS-PROTECTION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-913-SCANNER-DETECTION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-921-PROTOCOL-ATTACK.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-931-APPLICATION-ATTACK-RFI.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-932-APPLICATION-ATTACK-RCE.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-933-APPLICATION-ATTACK-PHP.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-943-APPLICATION-ATTACK-SESSION-FIXATION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/REQUEST-949-BLOCKING-EVALUATION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-950-DATA-LEAKAGES.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-951-DATA-LEAKAGES-SQL.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-952-DATA-LEAKAGES-JAVA.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-953-DATA-LEAKAGES-PHP.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-954-DATA-LEAKAGES-IIS.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-959-BLOCKING-EVALUATION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-980-CORRELATION.conf
        Include /etc/nginx/owasp-modsecurity-crs/rules/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

spec:
  tls:
  - hosts:
    - blogdemo.home.dmarkey.com
    secretName: blogdemo-tls
  rules:
  - host: blogdemo.home.dmarkey.com
    http:
      paths:
      - backend:
          serviceName: my-service
          servicePort: 80

Lines of note:

        SecRuleEngine On

This sets the rule engine to block traffic that ModSecurity deems to have broken the ruleset. Users will get a 403

        SecAction \"id:900200,phase:1,nolog,pass,t:none,setvar:\'tx.allowed_methods=GET\'\"`

Be default, the ruleset allows GET,POST and PATCH. In this case I only want GET requests to be allowed - be careful of escaping.

        SecAuditLogFormat JSON
        SecAuditLogType Serial
        SecAuditLog /dev/stdout

These lines instructs ModSecurity to log to stdout(this will be of stdout of the nginx ingress controller)

Assuming you're using some log shipper to Elasticsearch etc, you will get nice JSON output when a request is blocked.

comments powered by Disqus