Skip to content
Snippets Groups Projects
Select Git revision
  • a2bf122c10299eb105433fcbedf482fa4a4af442
  • main default protected
2 results

cours

Project: web SSO service with K8s/KinD

A pseudo web single-sing-on (SSO) service deployed on a Kubernets cluster provisioned with KinD. This is mostly an integration project with some application development.

Introduction

An SSO service allows users of a web platform, composed of different subsystems, to log in once onto any one of the subsystems and stay authenticated for a predefined amount of time (session). During a valid session, i.e., after the first log-in, users can access other subsystems without being asked for credentials.

The idea is to implement a microservice deployed on a KinD cluster hosted by a single VM instance. The service shall be load-balanced by MetalLB and use some S3 storage for the enrollment and session data.

Architecture

The system's 3-tier architecture is microservice-based:

  1. The replicated front-end tier implements:
    • a login Web GUI composed of several views (languages: HTML5, JS) and
    • associated logic (languages: Python/Flask) which sends incoming authentication requests to the back-end and returns the results to the views. The ingress/egress point is a load balancer that exposes an external IP address.
  2. The back-end tier (languages: Python/Flask) tier receives and handles authentication requests from the front-end. All the enrollment and session logic is implemented here in a CRUD-like fashion: corresponding objects are stored in an S3-compatible storage.
  3. The storage tier is a standard S3-like object storage which can be accessed only by the back-end.

Application's architecture and deployment Application's architecture and deployment schema

Infrastructure and deployment

The front-end replicas and the back-end are deployed in separate containers hosted by a Kubernetes-based MetalLB service deployment. The service, on its turn, is hosted by a 2-node KinD cluster installed on a single VM infrastructure.

Front-end

Web portal with REST-based functions, written in HTML5, JavaScript (views) and Python/Flask (main logic). All related files are in directory frontend/.

The REST service routes are:

  • enroll: sign-up (subscribe) to the system with credentials
  • unenroll: delete subscription credentials (remove account)
  • login: authenticate with e-mail and, if needed (first login), password
  • logout: de-authenticate by removing the current session

The following files in directory views/ handle the client-side workflow.

  • index.html provides just two buttons:
    • "Login" linked to the view login.html
    • "Sign up" (enroll) linked to the view signup.html
  • signup.html provides a form with with input fields "e-mail" and "password" and is linked to the dashboard.html view
  • login.html provides a form with input fields "e-mail" and "password" and is linked to the dashboard.html view.
  • dashboard.html provides two buttons "Logout" and "Remove account" (unenroll)

The file main.py handles the REST logic by conveying all requests to the back-end.

💡 This part does not requires adaptations.

Back-end

The single file backend/main.py (Python/Flask) implements the enrollment and session management logic by handling REST requests coming from the front-end. The corresponding objects are managed in a CRUD-like fashion in/from a single S3 storage bucket.

🛠️ This part requires some development. See the details in the boilerplate backend/main.py.

Storage

This is a single S3-like buckets with two directories: one for enrollment data, one for session data.

Objects shall be written as JSON data based on the following proposed schema.

Enrollment objects

An enrollment object:

  • is named after the user e-mail
  • is unique in the system
  • is created when a new user subscribes to the system via the enroll function
  • is never updated (assume that users' passwords cannot be changed)
  • is deleted when a subscribed (== existing) user unsubscribes from the system via the unenroll function

Minimum schema (you're free to extend it):

{
	"title": "Enrollment",
	"type": "object",
	"properties": {
		"password": {
			"type": "string",
			"description": "The user's password."
		},
		"timestamp": {
			"description": "The UNIX epoch time of object's creation",
			"type": "integer",
			"minimum": 0
		}
	}
}

Example data for object named foo@bar.com:

{
	"password": "SECRET",
	"timestamp": 1733330967
}

Session objects

A session object:

  • is named after the user e-mail
  • is unique in the system -- multiple sessions are not permitted
  • is created at the first user's connection to the system via the login function
  • is never updated
  • is deleted when a logged-in user disconnects from the system via the logout function

Minimum schema (you're free to extend it):

{
	"title": "Session",
	"type": "object",
	"properties": {
		"client": {
			"description": "The client host's IPv4 address",
			"type": "string",
			"format": "ipv4"
		},
		"timestamp": {
			"description": "The UNIX epoch time of object's creation",
			"type": "integer",
			"minimum": 0
		}
	}
}

Example data for object named foo@bar.com:

{
	"client": "192.168.1.2",
	"timestamp": 1733330967
}

Workflows

The following workflows correspond to the application "routes" discussed above. Additional information is provided in the boilerplate files. All involved data are in JSON format.

Conventions:

  • KEYWORDS are capitalized.
  • Actors are in boldface.
  • 'CONSTANTS' are capitalized and single-quoted.
  • data items are lower-case and italicized.
  • all times are represented as UNIX epoch timestamps

💡 For the sake of simplicity:

  • Sessions never expire.
  • No cookies are stored at the client.
  • The e-mail address is used as authentication token.

All operations are stateless: apart from enrollment and session data, nothing else is recorded by the back-end.

Enroll

A new user subscribes to the system via the enroll function:

  1. User provides enrollment data (e-mail and password) via the the front-end's signup view.
  2. Front-end sends enrollment data to the back-end.
  3. Back-end verifies enrollment data:
    • IF user exists THEN returns 'KO:ALREADY_ENROLLED'
    • ELSE
      1. Writes enrollment data to storage
      2. Returns 'OK:ENROLLED' to the front-end
  4. Front-end receives response from the back-end and shows it to the user.

Unenroll

An enrolled user unsubscribes from the system (removes their account) with the unenroll function:

  1. User provides enrollment e-mail via the the front-end's dashboard view.
  2. Front-end sends enrollment data to the back-end.
  3. Back-end verifies enrollment data:
    • IF user does not exists THEN returns 'KO:NO_SUCH_USER'
    • ELSE
      1. Removes enrollment data and any active sessions from the storage
      2. Returns 'OK:UNENROLLED' to the front-end
  4. Front-end receives response from the back-end and shows it to the user.

Login

An enrolled user authenticates to the system with the login function:

  1. User provides e-mail via the the front-end's view login.
  2. Front-end sends e-mail to the back-end.
  3. Back-end verifies the e-mail:
    • IF user does not exists THEN returns 'KO:NO_SUCH_USER'
    • ELSE verifies sessions:
      1. IF an active session exists THEN returns 'OK:SESSION_EXISTS'
      2. ELSE returns 'OK:NEED_PASSWORD'.
  4. Front-end receives first response from the back-end:
    • IF response == 'OK:NEED_PASSWORD' THEN
      1. Prompts the user for their password.
      2. User provides password.
      3. Front-end sends e-mail and password to the back-end.
      4. Back-end verifies the password:
        • IF password matches THEN
          1. Writes a new session to storage.
          2. Returns 'OK:LOGGED_IN' to the front-end.
        • ELSE returns 'KO:WRONG_PASSWORD' to the front-end.
    • ELSE shows other response to the user and terminates.
  5. Front-end receives second response from the back-end and shows it to the user.

Logout

An enrolled user deauthenticates to the system with the logout function:

  1. User provides e-mail via the the front-end's view dashboard.
  2. Front-end sends e-mail to the back-end.
  3. Back-end verifies the e-mail:
    • IF user does not exists THEN returns 'KO:NO_SUCH_USER'
    • ELSE verifies sessions:
      • IF an active session exists THEN
        1. Removes session from storage.
        2. Returns 'OK:LOGGED_OUT'.
      • ELSE returns 'KO:NO_ACTIVE_SESSION'.
  4. Front-end receives response from the back-end and shows it to the user.

Infrastructure provisioning

The infrastructure is entirely hosted by SwitchEngines (OpenStack cloud) and is composed of

  • A single VM featuring:
    • Source image: A Debian 12 Bookworm
    • Flavor: 2 vCPUs, 4GB RAM, 40GB root disk -- no extra volume needed
    • A KinD/Kubectl installation
  • One S3 bucket.

The infrastructure (computing instance + S3 storage) shall be provisioned via Terraform.

💡 References:

Service deployment

The service shall be deployed on a 3-pods K8s microservice hosted by a two-node KinD cluster, with a single MetalLB load-balancer entry point, as done with the Lab-K8s. The front-end shall be replicated over 2 pods. The 3rd pod shall host the back-end.

The whole software stack, apart from the KinD package, shall be deployed via Ansible. Of course, instead of the dummy http-echo app, two different Docker images shall be used -- 🚧 see the project's Docker file boilerplate: one for the front-end, the other for the back-end, both hosted in the Docker Hub registry -- you shall create a personal public repository. We trust you, please, do not cheat!

The front-end image does not need to be rebuilt, unless you want to implement some client-side (HTML/JS) bonuses.

The back-end image shall be rebuilt after any modification to the application code.

The whole stack shall be redeployed whenever any of its images are updated.

Tasks

🚧 To be finalized

💡 Please, respect the file layout provided by this repository. Here is map:

├── Ansible
│   ├── keys         <= SSH keys with name "id_..." (won't be committed)
│   └── playbooks    <= recipes to be extended
│       └── files    <= KinD/K8s files
├── Application
│   ├── backend      <= main.tf to be extended
│   └── frontend
│       └── views
├── Docker
└── Terraform        <= recipes to be extended
    └── conf         <= Cloud-init files

🛠️ You shall:

  1. Fork this repository.
  2. Complete the Python back-end file Application/backend/main.py. See the section Development below.
  3. Rebuild the application back-end Docker image, and push it to your public Docker Hub repository -- (❓ TO-DO - We should provide instructions). This task shall be automated via Ansible -- see below.
  4. Complete your Terraform files from the version you developed in Lab-Terraform up to Task #8. Your recipe shall handle only the provisioning of the VM plus an S3 storage bucket -- no KinD/Kubectl package installation. Commit your recipe files and in directory Terraform/ -- Cloud-init files are already in sub-folder conf/.
  5. Complete your Ansible playbook, starting from the version you developed in Lab-Ansible Task #10, to (commit all related files in directory Ansible/):
    • expose the application portal's IP (i.e, the load-balancer's) to the Internet via socat or other mechanism of your choice;
    • rebuild and push the application images to your Docker Hub repository. These shall be local_action tasks.

💡 References:

Bonuses

You will get bonus for any of the following improvements.

  • Ask for the password when removing an account (unenroll) -- extra safety. +0.2 points.
  • Use password hashing in the back-end -- extra security. +0.1 points.
  • Handle session expiration after a configurable amount of time (in minutes). You can use an extra enrollment view's parameter expiration_time. +0.3 points.
  • Handle multiple sessions started from different browsers, e.g., private navigation tab/window. +0.4 points.
  • Support temporary disconnection via client session data stored in a Web cookie (without the password) -- this requires some sort of cryptographic "nonce". +0.5 points.

Development

Local installation

To test your application before deploying it as a K8s service, you can install and run it locally in your workstation.

🛠️ It is strongly recommended to install Python's "virtualenv":

At the top-level of your Git repo clone, get an overview of administrative commands and options:

$ make help

Then, install the application in a virtual env (automatically created)

$ make vinstall

Otherwise, plain install -- you might need to install python dependencies manually:

$ make install

Put your SwitchEngines EC2 credentials in file <sandbox>/Application/s3_credentials.env, which should be already there.

Finally, run the application in the current console:

$ make run

Or, run it in its own terminal:

$ make trun

Managing the containerized application with Docker

🛠️ Install the following Docker packages on your workstation according to your distribution's instructions: docker(.io), docker-cli, (docker-)buildx

💡 Please, peruse the Makefile to learn the real Docker commands.

After installation, switch back to your git clone directory, then run:

$ make docker-build

Then, run the containers:

$ make drun

Get useful information -- IP addresses, ports, etc.:

$ make -s dstatus

Watch logs of running containers with:

$ make -s dlogs

When ready for deployment on your VM (you'll be asked for your password/token):

$ docker login
$ make -s docker-push

You can manage your containers with the other make commands: dstop, dstart, drm, etc.

Test workflow

The following tests shall be passed by your implementation:

  • Starting with a non-provisioned infrastructure:
    1. Terraform apply shall provision a bare-bone VM and an S3 bucket
    2. Ansible-playbook shall install KinD/Kubectl and deploy your load-balanced application
    3. Your application shall be reachable on port 80 (or another of your choice) from any host outside the Cloud network.
  • Once your application is installed, you shall exercise all the branch conditions described by the above workflows:
    1. Enroll a new user: shall succeed
    2. Enroll an existing: user shall fail
    3. Unenroll a new user: shall fail
    4. Unenroll an existing user: shall succeed
    5. Login an enrolled user: shall succeed and ask for the password
      • with a valid password: shall succeed
      • with an invalid password: shall fail
    6. Login a non-enrolled user: shall fail
    7. Logout a non-enrolled user: shall fail
    8. Logout an enrolled user
      • with an active session: shall succeed
      • without an active session: shall fail