CopyDisable

Showing posts with label Tips and Tricks. Show all posts
Showing posts with label Tips and Tricks. Show all posts

Wednesday, 26 August 2026

GateCHA Hands-On Guide

GateCHA is a self-hosted CAPTCHA management platform designed to block bot spam and automated form submissions on websites without annoying human visitors. It is self-hosted alternative to ALTCHA Sentinel, a paid cloud SaaS.

Unlike traditional CAPTCHA solutions that ask users to solve image puzzles or decipher distorted text, GateCHA leverages Proof-of-Work (PoW) computational challenges. When a user submits a form, their web browser silently solves a light mathematical puzzle in the background. For a real user, this process takes a split second and is completely invisible. For automated spam bots trying to submit thousands of forms at scale, these computational challenges quickly become too expensive to process, forcing them to stop the spam attacks.

Third-party CAPTCHA providers often track user IP addresses and browsing behavior for advertising profiles. GateCHA is self-hosted, meaning no data is sent to external third parties.

GateCHA integrates into a web application using a simple three-step architecture: 

1) GateCHA instance Setup, 

2) Web App's Frontend Integration, and 

3) Web App's Backend Verification.


In this hands-on demo, we are going to deploy GateCHA on a Ubuntu 24.04 server. I will provide simple code for a demo web-application, where we are going to have a simple HTML form as a client application, which going to capture user information and submit the data to a backend API. I will provide backend code for both Python and GoLang, but I will only show the backend deployment process for the Python API. 

GateCHA relies on an embedded SQLite database (MySQL also supported) and ships as a lightweight container, deploying it via Docker on Ubuntu 24.04 is the cleanest and most reliable method.

For this example we are going to use a VM with IP 10.2.11.116. In the code you will see this IP, you can replace this IP with your IP or domain name. 

Step 1: Installation 

I am running the below commands as root user:

Update package lists: apt update && apt install -y curl git jq build-essential

Install Docker and Docker Compose: 

curl -fsSL https://get.docker.com -o get-docker.sh

sh get-docker.sh



Python and GoLang is installed just for developing the API servers, not for running the GateCHA server. 

Install GoLang:

# wget https://go.dev/dl/go1.22.2.linux-amd64.tar.gz

# rm -rf /usr/local/go && tar -C /usr/local -xzf go1.22.2.linux-amd64.tar.gz

# echo 'export PATH=$PATH:/usr/local/go/bin' >> ~/.profile

# source ~/.profile



Install Python:

We are going to write the backend API using Python, so we are installing it.

# apt install python3-dev

# apt install software-properties-common

# apt install python3.12-venv


Install Nginx webserver:

Our client application (HTML form) will be served using Nginx webserver.  

# apt install nginx


Step 2: Setup GateCHA

The GateCHA docker image is fully self-contained, that means we don't need to spin up separate database.

Create a new directory for the project: 

# mkdir ~/gatecha 
# cd ~/gatecha

Create a docker-compose.yml file: 
# nano docker-compose.yml

Paste the following configuration into the file:

services:
  gatecha:
    image: ghcr.io/upellift99/gatecha:latest
    container_name: gatecha
    ports:
      - "9090:8080"
    volumes:
      - gatecha_data:/app/data
    environment:
      - GATECHA_ADMIN_PASSWORD=devpassword
volumes:
  gatecha_data:


We are going access the GateCHA dashboard using the port 9090. So the GateCHA dashboard URL will be http://10.2.11.116:9090/.
The default admin username is admin and we specified the password as devpassword (in docker-compose.yaml)

Save the file and start the container in the background: 

# docker compose up -d




Step 3: Create API key















Once the container is up, we can open the dashboard using a webbrowser.

After login we can see the dashboard.














Right now we do not have any API key for our web application. A GateCHA API key is a unique, site-specific security token. GateCHA authenticates form widgets and server-side verification requests using the API key.










We can create new API key by clicking the Create Key button. 





















Explanation of important fields for creating key:

  • Domains: To prevent someone from taking our API key and using it on their own website, GateCHA allows us to configure Domain Restrictions in the dashboard. Once we restrict the key to our production domain (e.g., myapp.com), the backend will reject any verification requests originating from unauthorized domains.
  • Difficulty: When we configure the Difficulty (or maxNumber) for a new API key in GateCHA, we are setting the maximum possible computational effort required by the user's browser to solve the proof-of-work challenge.
    Some use cases for setting this value:
    • Medium / Standard (Recommended)
      • Value: 100,000
      • Use Case: Standard user registrations, login pages, and general forms.
      • Experience: It takes less than 1 second on modern laptops and smartphones. It is enough to deter most cheap spam bots while keeping the experience completely frictionless for legitimate users.
    • Low / Fast
      • Value: 10,000 to 50,000
      •  Use Case: Low-risk applications or internal tools where user experience must be instant.
      •  Experience: Solves almost instantaneously on any device. It stops simple bots that don't execute JavaScript, but determined botnets might still brute-force it easily.
    • High / Strict
      •  Value: 500,000 to 1,000,000
      •  Use Case: High-abuse targets like password resets, payment gateways, or if our site is actively experiencing a bot attack.
      •  Experience: Highly secure. While a modern desktop might solve this in 1–3 seconds, an older budget smartphone might take 5–10 seconds. If a user clicks submit and the button spins for 10 seconds, user experience may suffer.
    • Best Practice: Start with 100,000. If we notice spam getting through, increase it to 250,000 or 500,000. We can change this value on the server at any time without having to touch our frontend HTML code.
  • HIS Sampling: HIS stands for Human Interaction Signature. It is an advanced feature in the ALTCHA (and GateCHA) protocol that acts as a behavioral biometrics collector to detect automated environments and bots.  Rather than just relying on the mathematical proof-of-work challenge, HIS analyzes how a user physically interacts with our webpage before they submit the form.


Once we click the Create Key button, the new API key is created:










We will use the Key ID in our client application and HMAC Secret will remain in the server.



Step 4: Integrating GateCHA

Now that we have installed GateCHA and created our API key, it’s time to integrate it into our web application.

GateCHA works silently in the background using Proof-of-Work (PoW). Let’s look at how the verification process works under the hood.


The 4-Step Verification Flow:

To understand how GateCHA protects our application, think of the verification process as a 4-step conversation between our webpage (Client application), our server (Backend application), and the GateCHA server:

  1. Requesting a Challenge: When a user visits our form with GateCHA widget, the GateCHA widget automatically calls our GateCHA server to request a math challenge.
  2. Solving the Puzzle: The user's web browser solves the puzzle in the background using JavaScript. This usually takes less than a second on modern devices.
  3. Submitting the Payload: Once solved, the widget appends a hidden input field containing the completed solution (called the altcha payload) into our form. When the user clicks Submit, this payload is sent to our application backend alongside their form data (e.g., name, email).
  4. Backend Verification: Before processing the submitted form data, our application backend sends this payload back to GateCHA to ask, "Is this math solution real, valid, and unexpired?" If GateCHA confirms with ok: true, our backend allows the user to proceed.


Client-Side Integration (Frontend)


Adding GateCHA to a HTML forms requires only two things: 
1) importing the official lightweight web component script and 
2) adding the <altcha-widget> HTML tag.

Step 1: Add the JavaScript Script

Add the following script tag inside the <head> of our HTML document.

    Note: Because ALTCHA uses ES Modules, ensure we include type="module".


<script type="module" async defer src="https://cdn.jsdelivr.net/npm/altcha/dist/altcha.min.js"></script>


Step 2: Add the widget to our form

Place the <altcha-widget> tag inside our HTML <form> element wherever we want the CAPTCHA checkbox to appear:

<form action="/submitdata" method="POST">
    <!-- User Input Fields -->
    <label for="email">Email Address:</label>
    <input type="email" id="email" name="email" required>
    <!-- GateCHA Widget -->
    <altcha-widget 
        challengeurl="http://10.2.11.116:9090/api/v1/challenge?apiKey=gk_96fccfb465545285bc3a3bc0"
        hidelogo
        hidefooter>
    </altcha-widget>

    <button type="submit">Submit</button>
</form>


Key Widget Options:
  • challengeurl: Points to the GateCHA server’s challenge endpoint, with the generated apiKey for the site.
  • hidelogo: (Optional) Removes the ALTCHA logo icon for a cleaner look.
  • hidefooter: (Optional) Hides the bottom footer text inside the widget.

My example implementation of client page:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Registration Form</title>
    <!-- Import the ALTCHA web component -->
    <script type="module" async defer src="altcha.min.js"></script>
    <style>
        body { font-family: sans-serif; padding: 20px; max-width: 400px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; }
        input[type="text"], input[type="email"], input[type="tel"] { width: 100%; padding: 8px; }
        button { padding: 10px 15px; background: #007bff; color: white; border: none; cursor: pointer; }
    </style>
</head>
<body>

    <h2>User Registration</h2>
   
    <!-- Form submits to our Python API -->
    <form action="http://10.2.11.116:9999/submitdata" method="POST">
        <div class="form-group">
            <label for="name">Name:</label>
            <input type="text" id="name" name="name" required>
        </div>
       
        <div class="form-group">
            <label for="email">Email:</label>
            <input type="email" id="email" name="email" required>
        </div>
       
        <div class="form-group">
            <label for="phone">Phone Number:</label>
            <input type="tel" id="phone" name="phone" required>
        </div>

        <!-- GateCHA/ALTCHA Widget -->
        <div class="form-group">
            <altcha-widget
                challengeurl="http://10.2.11.116:9090/api/v1/challenge?apiKey=gk_96fccfb465545285bc3a3bc0"
        hidelogo
                hidefooter>
            </altcha-widget>
        </div>

        <button type="submit">Register</button>
    </form>

</body>
</html>


Note: Web browsers require a Secure Context (HTTPS or localhost) to run the browser cryptography functions required by GateCHA. 
Otherwise we will get error like:

Uncaught (in promise) Error: Web Crypto is not available. Secure context is required (https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts).

Suppose we are testing from a different local machine on the network and cannot use localhost or HTTPS, we can temporarily tell our browser to treat the local IP as secure.

For Chrome we can do this as follows:
  • Open a new tab in Chrome or Edge and paste this into the URL bar:
    chrome://flags/#unsafely-treat-insecure-origin-as-secure
  • Enable the setting.
  • Add our local server's URL into the text box.
  • Click Relaunch at the bottom of the screen to restart the browser.
  • Reload the form page. The Web Crypto API will now work.











Server-Side Integration (Backend)

When the form is submitted, our backend receives all standard form fields plus the altcha hidden payload field.

To prevent malicious users from tampering with form submissions, our backend must verify this payload with GateCHA before trusting the user.

Backend Verification Logic

Regardless of what language our backend is written in (Python, Go, Node.js, PHP), the verification steps remain identical:
  1. Read the altcha field from the incoming request form data.
  2. If missing, return an error (400 Bad Request).
  3. Send a POST request to our GateCHA server at /api/v1/verify?apiKey=YOUR_API_KEY containing {"payload": "..."}.
  4. Inspect GateCHA's JSON response. If ok is true, accept the submission. Otherwise, reject it (403 Forbidden).

Example Implementation (Python / Flask)

from flask import Flask, request, jsonify
import requests

app = Flask(__name__)

# GateCHA Configuration
GATECHA_VERIFY_URL = "http://127.0.0.1:9090/api/v1/verify"
GATECHA_API_KEY = "gk_96fccfb465545285bc3a3bc0"

@app.route('/submitdata', methods=['POST'])
def submit_data():
    # 1. Extract standard form fields
    name = request.form.get('name')
    email = request.form.get('email')
    phone = request.form.get('phone')
   
    # 2. Extract the CAPTCHA payload
    altcha_payload = request.form.get('altcha')

    print("Payload: ", altcha_payload)

    if not altcha_payload:
        return "Error: CAPTCHA is missing or incomplete.", 400

    # 3. Verify the payload with our GateCHA server
    verify_url = f"{GATECHA_VERIFY_URL}?apiKey={GATECHA_API_KEY}"
   
    try:
        verify_response = requests.post(
            verify_url,
            json={"payload": altcha_payload},
            timeout=5
        )
       
        verify_result = verify_response.json()
        print(verify_result)
        # 4. Check if verification was successful
        if verify_response.status_code == 200 and verify_result.get("ok"):
            # CAPTCHA passed! Process the user registration here
            print(f"New Registration: {name}, {email}, {phone}")
            return f"Registration successful for {name}!"
        else:
            # CAPTCHA failed (expired, tampered, or reused)
            # Print the actual response from GateCHA to our server console for debugging
            print(f"GateCHA rejected payload. Status: {verify_response.status_code}, Response: {verify_result}")
            return "Error: CAPTCHA verification failed.", 403
           
    except Exception as e:
        print(f"GateCHA connection error: {e}")
        return "Internal Server Error during CAPTCHA validation.", 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=9999)



This is the example webpage:
























When we click on the "I'm not a robot" button, the altcha widget sends request to provide a math challange or puzzle.

















If we check the response we can the JSON object returned by the GateCHA server








{

    "algorithm": "SHA-256",

    "challenge": "ce66813a276e9c91f8cbd2e798e2f7f1b5d8092af43f21d770f8797be4ae0735",

    "maxNumber": 100000,

    "salt": "1caa882d1b86cf565bead696?expires=1787662905\u0026",

    "signature": "a7853808cb78de93f5218d78c040d8b59598a4e284cc2352fb43c4aab446b985"

}


The JSON block is a hidden math puzzle send to the browser by the GateCHA server.

Here is what each piece of the puzzle means:

  1. algorithm: "SHA-256": This tells our browser to use SHA-256 hashing function to solve the puzzle.
  2. maxNumber (The Guessing Limit): 100000: To solve the puzzle, our browser has to guess a secret number. This field tells the browser: "The secret number is somewhere between 0 and 100,000." The browser will rapidly count up and check each number until it finds the right one.
  3. salt (The Unique Puzzle Piece & Timer): "1caa88...expires=1787662905&": This is a random string of characters combined with an expiration timestamp. It guarantees that every single puzzle generated is unique and can't be solved ahead of time. Once the timestamp passes, the puzzle is useless.
  4. challenge (The Target Answer): "ce66813a...": This long string of characters is the final "target.". The browser's job is to take the salt, attach its guessed number to it, and run it through the algorithm (SHA-256). If the result matches this exact challenge string, the browser has found the correct number.
  5. signature (The Anti-Cheat Seal): "a7853808...": This is a digital seal created by the server. It prevents hackers from cheating.



How the system remains cryptographically secure

The API Key: It simply tells the GateCHA server which configuration (difficulty level, expiration time, etc.) to use to generate the math puzzle. To prevent someone from taking our site's API key and using it on their own website, GateCHA allows us to configure Domain Restrictions for an API key. Once we restrict the key to our production domain (e.g., myapp.com), the backend will reject any verification requests originating from unauthorized domains.


Puzzle Metadata: GateCHA relies on mathematical Proof-of-Work.To solve a math puzzle, our browser needs the instructions. 

The server generates a random secret number (let's call it n). It sends the browser the salt and the resulting final hash, which is the challenge.

The client-side JavaScript must run a loop to guess n using the following logic:

Hash(salt + guess) = challenge

The client guesses 0, 1, 2... up to the maxNumber until the hashes match. 

Because there is no shortcut in cryptography to reverse a hash, the bot is forced to spend time and CPU cycles doing the math.

Once found, it sends the solved number, along with the original challenge, salt, and signature, back to the backend server.


The Signature: If the attacker can see the instructions, what stops them from changing the maxNumber to 1 so the puzzle solves instantly?

The security relies entirely on the signature field. When the GateCHA server generates the puzzle, it signs the salt and challenge using a Private Secret Key that never leaves the server. This creates the signature. 

If a bot tampers with the maxNumber, modifies the salt, or alters the challenge, the signature will instantly become invalid. When our backend sends this tampered payload to the /verify endpoint of GateCHA, the GateCHA server will check the signature, realize it was forged, and return a 403 Forbidden.


How the Signature is Generated:

When the GateCHA server receives a request for a new challenge, it performs the following cryptographic operations:

  • Generate the Puzzle: The server creates a random salt (which includes the expiration timestamp) and selects a random secret number n between 0 and the maxNumber.
  • Compute the Challenge: The server hashes the salt and the secret number together to create the target challenge:
    challenge = SHA-256(salt + n)
  • Sign the Challenge: To prevent tampering, the server uses our private HMAC Secret (let's call it K) to sign the newly created challenge using a Hash-based Message Authentication Code (HMAC):
    signature = HMAC-SHA-256(K, challenge)

    The HMAC Secret generated during the API key creation is the private cryptographic key used to generate that exact signature field.
The server then packages the salt, challenge, maxNumber, and signature and sends them to the client. It discards the secret number n entirely.


GateCHA server verification: When our backend sends this data to GateCHA, GateCHA immediately takes the submitted challenge and re-runs it through the HMAC equation using its private HMAC Secret (K).

If an attacker tried to create their own easy challenge, or alter the salt to remove the expiration date, the signature GateCHA calculates will not match the signature the attacker sent. 

GateCHA immediately drops the request with a 403 Forbidden error without even looking at the math.

Only if the signature is perfectly valid GateCHA perfrom the accuracy check and verify the math:

SHA-256(submitted_salt + submitted_number) == submitted_challenge


Expiration and Replay Protection: In the JSON object returned by GateCHA server, we can see that there is a  timestamp inside the salt (expires=1787560709).

  • Time-to-Live: The server mathematically verifies this timestamp upon submission to ensure the puzzle hasn't expired.
  • Single Use: Once a bot solves the puzzle and submits it, the server logs the transaction. If the bot tries to reuse that exact same solved payload for a second form submission (a replay attack), the server recognizes the duplicate and rejects it.


I hope, I am able to explain how GateCHA works and how to easily implement it in your websites. 

The sample code used in this blog is shared in this Github repository so that you can easily refer:

https://github.com/pranabsharma/gatecha-demo 

Saturday, 1 June 2024

Revoking a JWT (JSON Web Token) before its expiry

JWTs are designed to be stateless and are valid until they expire, which can pose a security risk if they are leaked or stolen. Revoking a JWT (JSON Web Token) before its expiry can be necessary in several scenarios, mostly revolving around security concerns or changes in user status.

Here are some common examples where revoking a JWT is crucial:

1) User logout: When a user logs out, it's a security best practice to ensure that the JWT the user was using is immediately invalidated to prevent further use. This helps enforce that logout effectively ends access, rather than allowing the token to remain valid until it naturally expires.

2) Change of user permission or role: If a user's roles or permissions are changed, you might need to revoke any existing tokens. This ensures that any new requests from the user adhere to their updated permissions, preventing access based on outdated privileges.

3) Security Breaches: If you detect that a user's credentials have been compromised, revoking their active JWTs can help mitigate unauthorized access. This is particularly important if you suspect that tokens have been stolen or exposed to third parties.

4) Suspension or Deletion of Accounts: If a user's account is suspended or deleted, all associated JWTs should be revoked to prevent any further activity.

5) Password Changes: Following a password change, particularly if the change was prompted by security concerns (like a potential breach), it's sensible to revoke any existing tokens. This prevents the old tokens from being used by anyone who might have gained unauthorized access before the password was updated.

6) Anomalies in User Behavior: If abnormal activity is detected in a user’s account, such as logging in from an unusual location or multiple failed attempts to access restricted resources, it might be wise to revoke their JWTs until the activity can be reviewed. This could prevent ongoing or escalating security issues.


In each of these above example scenarios, revoking a JWT is about ensuring that the system's current state aligns with the security and operational policies of the application. Revoking a JWT (JSON Web Token) before its expiry can be crucial for maintaining the security of the application, especially in cases where a token might be compromised.

Here are some strategies that we can employ to effectively manage and revoke JWTs before their expiration:

1. Use a Token Revocation List: A common method is to maintain a token revocation list on your server. Whenever you need to revoke a token, you add its unique identifier to this list. Each time a token is presented to the server, you check if it's on this revocation list. If it is, you treat the token as invalid, even if it's not expired. 2. Have Short Expiry Times: Another approach is to use very short expiry times for your tokens and require frequent re-authentication or token refreshing. This limits the window in which a stolen or leaked token can be used. During the refresh process, you can perform additional checks and refuse to issue a new token if the user's credentials have been revoked. 3. Implement a Blacklist with Cache: Similar to using a revocation list, you can implement a blacklist service, potentially using a fast, in-memory data store like Redis. This approach can be particularly effective in environments with high scalability requirements, where checking a database might introduce too much latency. 4. Change the Secret Key: In some extreme cases, such as a breach where multiple tokens are compromised, you can invalidate all issued tokens by changing the secret key used to sign the JWTs. This approach requires issuing new tokens for all active users, which can be disruptive but is highly effective in mitigating damage from a wide-scale token compromise. 5. Use Stateful Tokens: If feasible, consider using stateful JWTs. This involves storing token metadata in a database or another storage mechanism. When a token needs to be revoked, you can simply mark it as invalid in the storage system. This approach combines the benefits of token-based authentication with the revocability of session-based authentication.


Regularly rotate your secrets and update your token validation logic to keep up with potential vulnerabilities.

Thursday, 20 January 2022

Bootstrapping your own Kubernetes clusters for testing and development

 In this document, I am going to show you the simplest & quickest way to ready your own Kubernetes cluster that you can use for testing, learning and development purposes. It is not recommended for production scenarios. 

I will use one master node and two worker nodes for this demonstration. I am using Virtualbox VMs with all the nodes running Ubuntu 20.04 and the scripts that I am going to use are also for Ubuntu only. 


Prerequisites:

  • Minimum RAM per node should be 2 GB

  • 2 CPU cores per node

  • Swap off on all the nodes

    • Run swapoff command on each node:
      $ sudo swapoff -a 

    • Disable any swap entry in /etc/fstab file


Recommendation:

  • The nodes should probably be in the same local subnet, they should be able to communicate with each other without any firewall.

  • If you are using VMs in some cloud provider, ensure that the VMs are in the same VCN and subnet. You can configure the security list/cloud firewall so that the VMs can interact with each other for all the ports needed in a Kubernetes cluster.


Initial Setup:

Suppose my VMs are named this way:


Node

IP

master

192.168.0.51

worker1

192.168.0.52

worker2

192.168.0.53


You can add the entries in all the VMs hosts file, so that they can communicate with each other by hostnames. So edit /etc/hosts file on each VM and add the following lines:

192.168.0.51 master

192.168.0.52 worker1

192.168.0.53 worker2


Now we are ready to start the installation



Master Node:

On the master node run the scripts step by step in the same order it is shown below:

Step 1:

 
Install container runtime containerd using the script:
https://github.com/pranabsharma/scripts/blob/master/kubernetes/installation/install_containerd.sh 

Download the script and run it
ubuntu@master:~$ ./install_containerd.sh


Step 2:

Install the kubectl, kubeadm and kubelet using the script:

https://github.com/pranabsharma/scripts/blob/master/kubernetes/installation/install_kubeTools.sh

Download the script and run it

ubuntu@master:~$ ./install_kubeTools.sh



Step 3: 

Download the below script and ONLY run on your master node:

https://github.com/pranabsharma/scripts/blob/master/kubernetes/installation/run_on_master.sh 


Download the script and run it

ubuntu@master:~$ ./run_on_master.sh


This script does the following tasks:

  • Run kubeadm to initialize a Kubernetes control-plane on the master node.

  • Deploy Wavenet CNI plugin to manage the kubernetes pod networking. 

  • Copy the kubeconfig file to the user's home directory location so that kubectl commands can be run without specifying the kubeconfig file.



Our master node and control-plane are ready. At this point we will get the following status of our cluster:


ubuntu@master:~$ kubectl get node

NAME     STATUS   ROLES                  AGE   VERSION

master   Ready    control-plane,master   50m   v1.23.2



ubuntu@master:~$ kubectl get pod -n kube-system

NAME                                             READY       STATUS    RESTARTS      AGE

coredns-64897985d-fvnhj              1/1             Running           0                     51m

coredns-64897985d-wq6z5           1/1             Running           0                     51m

etcd-master                                   1/1             Running           0                     51m

kube-apiserver-master                  1/1             Running           0                     51m

kube-controller-manager-master   1/1             Running           0                     51m

kube-proxy-hnk2z                         1/1             Running           0                     51m

kube-scheduler-master                 1/1             Running           0                     51m

weave-net-gjvqq                           2/2             Running           1 (50m ago)    51m





Worker Node


Installation steps on worker nodes are the same as the master, the only difference is that we are going to skip the Step3 of the master node (step3 is for setting up the control plane). Run the scripts as shown in Step1 and Step2:

Step 1:

 
Install container runtime containerd using the script:
https://github.com/pranabsharma/scripts/blob/master/kubernetes/installation/install_containerd.sh 

Download the script and run it
ubuntu@worker1:~$ ./install_containerd.sh


Step 2:

Install the kubectl, kubeadm and kubelet using the script:

https://github.com/pranabsharma/scripts/blob/master/kubernetes/installation/install_kubeTools.sh

Download the script and run it

ubuntu@worker1:~$ ./install_kubeTools.sh



Adding Worker Nodes to the cluster


At this point our required software and services for the Kubernetes cluster are ready. The final step is to add the worker nodes to the cluster. 


Step1: 


We are going to create a new token for the worker node to join the cluster.


Run the below command on master node:


ubuntu@master:~$ kubeadm token create --print-join-command


This command will output the command to join the cluster. The output will be something like this:

kubeadm join 192.168.0.51:6443 --token pk9v0f.o8valhztkblohsmu --discovery-token-ca-cert-hash sha256:9e046d3f15e49c7363ec7a762767b169a296d6af7150aad56d21d54399a2df6f


Copy the output, we will need it in the next step.


Step 2:


Run the copied output command on the worker nodes


ubuntu@worker1:~$ kubeadm join 192.168.0.51:6443 --token pk9v0f.o8valhztkblohsmu --discovery-token-ca-cert-hash sha256:9e046d3f15e49c7363ec7a762767b169a296d6af7150aad56d21d54399a2df6f



Immediately after running the above command on worker node, if we check the nodes in the cluster we may get the below output:


ubuntu@master:~$ kubectl get node

NAME      STATUS     ROLES                  AGE   VERSION

master        Ready      control-plane,master   54m       v1.23.2

worker1       NotReady       <none>                 39s       v1.23.2


After some time, the worker node will come into the ready state.


In the same way we can add the worker2 node also.


That’s it, and our kubernetes cluster is ready to rock!!! Super easy isn’t it?