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:
- 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.
- 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.
- The storage tier is a standard S3-like object storage which can be accessed only by the back-end.
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
- "Login" linked to the view
-
signup.html
provides a form with with input fields "e-mail" and "password" and is linked to thedashboard.html
view -
login.html
provides a form with input fields "e-mail" and "password" and is linked to thedashboard.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:
-
User provides enrollment data (e-mail and password) via the the
front-end's
signup
view. - Front-end sends enrollment data to the back-end.
-
Back-end verifies enrollment data:
- IF user exists THEN returns 'KO:ALREADY_ENROLLED'
- ELSE
- Writes enrollment data to storage
- Returns 'OK:ENROLLED' to the front-end
- 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:
-
User provides enrollment e-mail via the the front-end's
dashboard
view. - Front-end sends enrollment data to the back-end.
-
Back-end verifies enrollment data:
- IF user does not exists THEN returns 'KO:NO_SUCH_USER'
- ELSE
- Removes enrollment data and any active sessions from the storage
- Returns 'OK:UNENROLLED' to the front-end
- 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:
-
User provides e-mail via the the front-end's view
login
. - Front-end sends e-mail to the back-end.
-
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 returns 'OK:SESSION_EXISTS'
- ELSE returns 'OK:NEED_PASSWORD'.
-
Front-end receives first response from the back-end:
- IF response == 'OK:NEED_PASSWORD' THEN
- Prompts the user for their password.
- User provides password.
- Front-end sends e-mail and password to the back-end.
-
Back-end verifies the password:
- IF password matches THEN
- Writes a new session to storage.
- Returns 'OK:LOGGED_IN' to the front-end.
- ELSE returns 'KO:WRONG_PASSWORD' to the front-end.
- IF password matches THEN
- ELSE shows other response to the user and terminates.
- IF response == 'OK:NEED_PASSWORD' THEN
- 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:
-
User provides e-mail via the the front-end's view
dashboard
. - Front-end sends e-mail to the back-end.
-
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
- Removes session from storage.
- Returns 'OK:LOGGED_OUT'.
- ELSE returns 'KO:NO_ACTIVE_SESSION'.
- IF an active session exists THEN
- 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:
- Fork this repository.
- Complete the Python back-end file
Application/backend/main.py
. See the section Development below. - 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.
- 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-folderconf/
. - 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.
- expose the application portal's IP (i.e, the load-balancer's) to the
Internet via
💡 References:
- Build and push Docker images: https://docs.docker.com/get-started/introduction/build-and-push-first-image/
- Ansible playbook delegation: https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_delegation.html
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":
- virtualenv, and
- virtualenvwrapper.
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:
- Terraform apply shall provision a bare-bone VM and an S3 bucket
- Ansible-playbook shall install KinD/Kubectl and deploy your load-balanced application
- 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:
- Enroll a new user: shall succeed
- Enroll an existing: user shall fail
- Unenroll a new user: shall fail
- Unenroll an existing user: shall succeed
- Login an enrolled user: shall succeed and ask for the password
- with a valid password: shall succeed
- with an invalid password: shall fail
- Login a non-enrolled user: shall fail
- Logout a non-enrolled user: shall fail
- Logout an enrolled user
- with an active session: shall succeed
- without an active session: shall fail