Post

Deploying a Password Generator Application in AWS EKS - Part 2

Giving the EKS password generator a custom Cloudflare domain, a Let's Encrypt certificate via cert-manager, and sticky sessions with cookies or upstream hashing.

Tutorials
Contents

In the previous post we started to deploy a password generator application in AWS EKS. By the end of it, two problems came to light:

  • There is no SSL certificate assuring that our domain is ours. It could be someone else, faking a connection to sniff data from your computer.
  • We need a ✨remarkable✨ address.

We are going to start with registering an address. In order to follow, we need to buy a domain from a registrar [1]. In our example, we use Cloudflare.

Using a Custom Domain With Cloudflare

After buying a domain, click on the left side menu, select DNS then Records.

The Cloudflare dashboard sidebar for the leandroasaservice account, with the DNS menu expanded and Records selected.
The Cloudflare dashboard sidebar for the leandroasaservice account, with the DNS menu expanded and Records selected.

After that, we land in the DNS management page. Then, click on the blue button Add record.

The Cloudflare DNS management page for leandroasaservice.com, listing the existing CNAME, MX and TXT records, with the blue Add record button above the table.
The Cloudflare DNS management page for leandroasaservice.com, listing the existing CNAME, MX and TXT records, with the blue Add record button above the table.

Now, choose the type CNAME, set the required name, and finally paste the Load Balancer URL into the Target box. Be sure to disable the Proxy Status. In the image below, the CNAME senhas.leandroasaservice.com is set as an alias for the Load Balancer ad287… that is managed by the Nginx Ingress. Remember to get this address from the command kubectl get svc -n ingress-nginx.

The Add record form in Cloudflare: type CNAME, name senhas, the AWS load balancer hostname as the target, and the proxy status toggled off to DNS only. A preview line reads that senhas.leandroasaservice.com is an alias of the us-east-1 elb.amazonaws.com address.
The Add record form in Cloudflare: type CNAME, name senhas, the AWS load balancer hostname as the target, and the proxy status toggled off to DNS only. A preview line reads that senhas.leandroasaservice.com is an alias of the us-east-1 elb.amazonaws.com address.

In your preferred browser, type the address you chose. Here, it's senhas.leandroasaservice.com. Similar windows are expected to appear since the certificate isn't implemented yet.

Safari warning that the connection to senhas.leandroasaservice.com is not private, with Show Details and Close Page buttons.
Safari warning that the connection to senhas.leandroasaservice.com is not private, with Show Details and Close Page buttons.
The same Safari warning with details expanded, explaining that the certificate is not valid and offering links to view the certificate or visit the website anyway.
The same Safari warning with details expanded, explaining that the certificate is not valid and offering links to view the certificate or visit the website anyway.

After authorizing the insecure connection, we land on the application page:

The LinuxTips Gerador de Senhas password generator served at senhas.leandroasaservice.com, with a password length slider, options to include numbers and special characters, a Gerar button, and an empty list of recently created passwords.
The LinuxTips Gerador de Senhas password generator served at senhas.leandroasaservice.com, with a password length slider, options to include numbers and special characters, a Gerar button, and an empty list of recently created passwords.

Now, it's possible to proceed with the certificate implementation so that users will know the connection to the website is legitimate.

Implementing a Certificate Using Cert Manager

We are going to install the Cert Manager via Helm. To do so, run the following command:

SH
helm install \
  cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --version v1.16.3 \
  --set crds.enabled=true

The output must look similar to the following:

SH
...
NAME: cert-manager
LAST DEPLOYED: ...
NAMESPACE: cert-manager
STATUS: deployed
REVISION: 1
TEST SUITE: None
NOTES:
cert-manager v1.16.3 has been deployed successfully!

In order to begin issuing certificates, you will need to set up a ClusterIssuer
or Issuer resource (for example, by creating a 'letsencrypt-staging' issuer).

More information on the different types of issuers and how to configure them
can be found in our documentation:

https://cert-manager.io/docs/configuration/

For information on how to configure cert-manager to automatically provision
Certificates for Ingress resources, take a look at the `ingress-shim`
documentation:

https://cert-manager.io/docs/usage/ingress/

Now, we are able to create a ClusterIssuer, which is a Kubernetes resource that represents a certificate authority (CA) [2]. Certificate Authorities are organizations responsible for validating identities of entities, like our application website [3]. In the app-prod-issuer.yaml file below, Cert Manager will create a public-private key pair and store it in a secret named letsencrypt-prod [4]. The public key will then be sent to the chosen Certificate Authority (Let's Encrypt), validating the domain identity [3]. Remember to replace the placeholder <your-email> with a proper e-mail.

YAML
apiVersion: cert-manager.io/v1
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: <your-email>
    # 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:
          ingressClassName: nginx

After that, add the annotation cert-manager.io/cluster-issuer: letsencrypt-prod in the app-ingress.yaml file. Remember to replace the placeholder <your-domain> with a valid domain in your control:

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: giropops-senhas
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/limit-rps: "2"
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - <your-domain>
    secretName: giropops-containers-secret
  rules:
  - host: <your-domain>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: giropops-senhas
            port:
              number: 5000

After that, create the ClusterIssuer and update the Ingress configuration by running the following commands:

SH
kubectl apply -f app-prod-issuer.yaml
kubectl apply -f app-ingress.yaml

In order to check if the certificate was created, run the command kubectl get certificate. The result will look similar to the output below:

SH
NAME                         READY   SECRET                       AGE
giropops-containers-secret   True    giropops-containers-secret   58s

You can also check for the certificate details by running the command kubectl describe certificate giropops-containers-secret:

SH
Name:         giropops-containers-secret
Namespace:    default
Labels:       <none>
Annotations:  <none>
API Version:  cert-manager.io/v1
Kind:         Certificate
Metadata:
  Creation Timestamp:  ...
  Generation:          1
  Owner References:
    API Version:           networking.k8s.io/v1
    Block Owner Deletion:  true
    Controller:            true
    Kind:                  Ingress
    Name:                  giropops-senhas
    UID:                   <uid>
  Resource Version:        15256
  UID:                     <uid>
Spec:
  Dns Names:
    <your domain>
  Issuer Ref:
    Group:      cert-manager.io
    Kind:       ClusterIssuer
    Name:       letsencrypt-prod
  Secret Name:  giropops-containers-secret
  Usages:
    digital signature
    key encipherment
Status:
  Conditions:
    Last Transition Time:  ...
    Message:               Certificate is up to date and has not expired
    Observed Generation:   1
    Reason:                Ready
    Status:                True
    Type:                  Ready
  Not After:               ...
  Not Before:              ...
  Renewal Time:            ...
  Revision:                1
Events:
  Type    Reason     Age   From                                       Message
  ----    ------     ----  ----                                       -------
  Normal  Issuing    103s  cert-manager-certificates-trigger          Issuing certificate as Secret does not exist
  Normal  Generated  102s  cert-manager-certificates-key-manager      Stored new private key in temporary Secret resource "giropops-containers-secret-n6w9w"
  Normal  Requested  102s  cert-manager-certificates-request-manager  Created new CertificateRequest resource "giropops-containers-secret-1"
  Normal  Issuing    80s   cert-manager-certificates-issuing          The certificate has been successfully issued

After that, refresh the page and click on the lock icon in your browser. Doing this, you will be able to see the certificate details:

Safari reporting an encrypted connection to senhas.leandroasaservice.com. The certificate chain runs from ISRG Root X1 through R10 to senhas.leandroasaservice.com, issued by Let's Encrypt and marked as valid.
Safari reporting an encrypted connection to senhas.leandroasaservice.com. The certificate chain runs from ISRG Root X1 through R10 to senhas.leandroasaservice.com, issued by Let's Encrypt and marked as valid.

Now, we can start creating passwords:

The password generator over HTTPS after one click on Gerar: the generated password appears in the Senha gerada field and as the only entry under Últimas senhas criadas.
The password generator over HTTPS after one click on Gerar: the generated password appears in the Senha gerada field and as the only entry under Últimas senhas criadas.

Enabling Session Stickiness

Keep testing and you are going to see a very weird behavior. At some point, the application "forgets" the generated passwords. In the image below, the passwords previously created don't appear.

The password generator after more clicks: the list of recently created passwords shows two passwords, and the first one generated is no longer among them.
The password generator after more clicks: the list of recently created passwords shows two passwords, and the first one generated is no longer among them.

If we keep creating new passwords, at some point we will land in the screen that contains the first password.

The password generator after further clicks: the list of recently created passwords shows four passwords, and the first one generated is back at the bottom.
The password generator after further clicks: the list of recently created passwords shows four passwords, and the first one generated is back at the bottom.

This occurs because we are using different pods each time we use the application. In order to avoid this behavior, we enable session stickiness using cookies or upstream hashing. Both mechanisms enable websites to recognize where the request comes from and direct it to the same server (or pod) in order to avert the very same behavior we have seen. When using cookies, a text file is stored in the client browser to identify where the request comes from [5] and direct it to the appropriate pod. On the other hand, when using upstream hashing, we use the request URI and create a hash without storing anything in the client browser. Then, requests that have the same hash will be directed to the same pod.

To enable stickiness we use the annotation nginx.ingress.kubernetes.io/affinity in the app-ingress.yaml file [6, day 10]. In the example below, we are using a cookie named kellercookie.

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: giropops-senhas
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/limit-rps: "2"
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/affinity: cookie
    nginx.ingress.kubernetes.io/session-cookie-name: kellercookie
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - <your-domain>
    secretName: giropops-containers-secret
  rules:
  - host: <your-domain>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: giropops-senhas
            port:
              number: 5000

It's possible to see the cookie in the developer tools in your browser.

Safari's Web Inspector Storage tab under the password generator, listing one session cookie named kellercookie for the senhas.leandroasaservice.com domain, marked Secure and HttpOnly.
Safari's Web Inspector Storage tab under the password generator, listing one session cookie named kellercookie for the senhas.leandroasaservice.com domain, marked Secure and HttpOnly.

To see the cookie in action, keep creating new passwords. It's expected to land in the same page always. It's possible to check that by comparing the passwords being created.

Now, to enable upstream hashing, we use the annotation nginx.ingress.kubernetes.io/upstream-hash-by [6, day 10]. The content for the file app-ingress.yaml that enables this feature is presented below:

YAML
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: giropops-senhas
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
    nginx.ingress.kubernetes.io/limit-rps: "2"
    cert-manager.io/cluster-issuer: letsencrypt-prod #comment: only for certificate
    #nginx.ingress.kubernetes.io/affinity: cookie
    #nginx.ingress.kubernetes.io/session-cookie-name: kellercookie
    nginx.ingress.kubernetes.io/upstream-hash-by: $request_uri
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - <your-domain>
    secretName: giropops-containers-secret
  rules:
  - host: <your-domain>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: giropops-senhas
            port:
              number: 5000

In your browser, delete the cookie and refresh the page:

The same Web Inspector Storage tab after the cookie is deleted: the cookie table for senhas.leandroasaservice.com is empty while the password generator keeps working.
The same Web Inspector Storage tab after the cookie is deleted: the cookie table for senhas.leandroasaservice.com is empty while the password generator keeps working.

Now, try creating new passwords. It's expected to land in the same page always, but now without cookies. It's possible to check that by comparing the passwords being created.

References

  1. Cloudflare, What is a domain name registrar?, Cloudflare Learning Center.
  2. The cert-manager Authors, Issuer, cert-manager documentation.
  3. Let's Encrypt, Let's Encrypt - How it works?, letsencrypt.org.
  4. The cert-manager Authors, ACME Issuer Type, cert-manager documentation.
  5. All About Cookies, What are cookies and how they are used?, allaboutcookies.org.
  6. Jeferson Fernando, Descomplicando Kubernetes (in Portuguese), LINUXtips.

Share this post