Automating Employee Onboarding to Akeyless with Terraform
Deeper DiveFor more in-depth information, check out our detailed documentation on the following topics:
Terraform Provider
Need any help?If something in this tutorial isn't working as expected, feel free to contact our support team via Slack.
Below is a text-only guide for users based on the above video
Introduction
Welcome to this Akeyless tutorial. In this guide, you will learn how to use the Akeyless Terraform Provider to automatically provision new Akeyless accounts, access roles, and user folders for new employees.
Managing application secrets and access roles across an enterprise causes IT headaches and often leads to manual bottlenecks and security risks. The Akeyless Terraform Provider fixes this by bringing Infrastructure as Code to your secrets management. It transforms scattered, ad-hoc access requests into a clean, predictable, and fully auditable workflow.
With the Akeyless Terraform Provider, platform teams can programmatically manage the entire security lifecycle. In a few lines of code, you can configure human and machine Authentication Methods, enforce granular Access Roles, organize Static, Dynamic, and Rotated Secrets, configure Gateways, manage KMS configurations, and automate PKI and Certificate Management workflows.
In this demo, we use the Akeyless Terraform Provider to automate employee onboarding through a self-service IT portal, creating a zero-touch, one-click workflow that requires no IT intervention.
Architecture Overview
The solution consists of:
-
Flask Frontend: A web form where an employee enters their corporate email address (e.g.,
[email protected]). -
Terraform Automation: A lightweight backend execution flow invoking
terraform applywith dynamic variables. -
Akeyless Platform: Creates three isolated resources per employee:
-
Auth Method (API Key):
/users/<username>/api-key -
Access Role:
/roles/<username>/role -
Personal Secrets Folder:
/personal/<username>with full item capabilities scoped exclusively to their path.
Project File Structure
Organize the repository using the following directory layout:
akeyless-onboarding-portal/
├── app.py
├── main.tf
├── static/
│ └── logo.png
└── templates/
└── index.html
-
Root Directory (
akeyless-onboarding-portal/): Contains the backend Flask application (app.py), the Terraform configuration (main.tf), and runtime files (.terraform/, state files). -
templates/: Holdsindex.html, allowing Flask'srender_templateengine to find and serve the web interface. -
static/: Houses static assets such aslogo.pngreferenced by the template.
Prerequisites
Before starting, ensure you have:
-
An active Akeyless Account with administrative permissions.
-
Akeyless Admin Access ID and Access Key.
-
Terraform CLI (v1.0.0 or later) installed.
-
Python 3.8+ with Flask installed.
Export your Akeyless admin credentials with the TF_VAR_ prefix so Terraform automatically populates the required variables:
export TF_VAR_akeyless_access_id="p-xxxxxx"
export TF_VAR_akeyless_access_key="u-xxxxxx"Terraform Configuration
Create main.tf in the root folder. This file configures the provider, extracts the username from the email address, and creates the personal folder, API Key Auth Method, and scoped Access Role:
terraform {
required_version = ">= 1.0.0"
required_providers {
akeyless = {
source = "akeyless-community/akeyless"
version = ">= 1.0.0"
}
}
}
# --- Admin Authentication Variables ---
variable "akeyless_access_id" {
type = string
description = "Akeyless Admin Access ID"
}
variable "akeyless_access_key" {
type = string
description = "Akeyless Admin Access Key"
sensitive = true
}
# --- Dynamic Input Variable (Passed from Flask app.py) ---
variable "employee_email" {
type = string
description = "The email address of the onboarding employee"
}
# --- Locals: Extract First.Last Name from Email ---
locals {
username = split("@", var.employee_email)[0] # Extract "first.last" from "[email protected]"
}
# --- Provider Configuration ---
provider "akeyless" {
api_gateway_address = "https://gw.cst.cs.akeyless.fans/api/v2"
api_key_login {
access_id = var.akeyless_access_id
access_key = var.akeyless_access_key
}
}
# 1. Create Dedicated API Key Auth Method inside /users/first.last/
resource "akeyless_auth_method_api_key" "user_api_key" {
name = "/users/${local.username}/api-key"
}
# 2. Create Access Role inside /roles/first.last/
resource "akeyless_role" "user_role" {
name = "/roles/${local.username}/role"
rules {
capability = ["create", "read", "update", "delete", "list"]
path = "/personal/${local.username}/*"
rule_type = "item-rule"
}
}
# 3. Associate Role Directly to the User's API Key Auth Method
resource "akeyless_associate_role_auth_method" "bind_user" {
role_name = akeyless_role.user_role.name
am_name = akeyless_auth_method_api_key.user_api_key.name
depends_on = [akeyless_role.user_role, akeyless_auth_method_api_key.user_api_key]
}
# 4. Create the Personal Item Folder (Uses "name" parameter for folder path)
resource "akeyless_folder" "user_folder" {
name = "/personal/${local.username}"
}
# --- Outputs Returned to Flask ---
output "access_id" {
description = "Public Access ID generated for the new user"
value = akeyless_auth_method_api_key.user_api_key.access_id
}
output "access_key" {
description = "Secret Access Key generated for the new user"
value = akeyless_auth_method_api_key.user_api_key.access_key
sensitive = true
}
output "personal_folder_path" {
description = "Path of the newly created personal items folder"
value = akeyless_folder.user_folder.name
}Initialize the provider plugin in the root directory:
terraform init
Onboarding Portal Application (Flask)
Create app.py in the root folder. This script cleans previous state to keep onboarding stateless, triggers terraform apply, parses the resulting output, and handles potential duplicate account errors:
import subprocess
import os
import json
from flask import Flask, render_template, request
app = Flask(__name__)
@app.route('/')
def home():
return render_template('index.html')
@app.route('/provision', methods=['POST'])
def provision():
email = request.form.get('employee_email')
if not email:
return render_template('index.html', error="Please provide a valid email address.")
# Clean local state before execution to keep automation stateless across requests
for state_file in ["terraform.tfstate", "terraform.tfstate.backup"]:
if os.path.exists(state_file):
os.remove(state_file)
env = os.environ.copy()
apply_cmd = [
"terraform", "apply",
f"-var=employee_email={email}",
"-input=false",
"-auto-approve"
]
try:
# Run Terraform Apply
subprocess.run(apply_cmd, capture_output=True, text=True, check=True, env=env, timeout=30)
# Retrieve structured outputs from Terraform
output_cmd = ["terraform", "output", "-json"]
output_res = subprocess.run(output_cmd, capture_output=True, text=True, check=True, env=env)
outputs = json.loads(output_res.stdout)
access_id = outputs.get("access_id", {}).get("value", "N/A")
access_key = outputs.get("access_key", {}).get("value", "N/A")
username = email.split('@')[0]
return render_template(
'index.html',
success=True,
email=email,
username=username,
access_id=access_id,
access_key=access_key
)
except subprocess.TimeoutExpired:
return render_template('index.html', error="Terraform operation timed out after 30 seconds.")
except subprocess.CalledProcessError as e:
error_output = e.stderr
# Catch duplicate resource errors from Akeyless
if "AlreadyExists" in error_output or "409 Conflict" in error_output:
username = email.split('@')[0]
friendly_msg = (
f"An Akeyless account already exists for {email}.\n"
f"Auth Method (/users/USERNAME/api-key) and Role (/roles/USERNAME/role) are already provisioned."
)
return render_template('index.html', warning=friendly_msg, email=email)
return render_template('index.html', error=error_output)
if __name__ == '__main__':
print("Starting Flask application on http://127.0.0.1:5001...")
app.run(host='127.0.0.1', port=5001, debug=True)Web UI Template
Save index.html inside the templates/ folder (templates/index.html). It renders the submission form, shows the user's scoped hierarchy, masks the Access Key with asterisks, and provides clipboard copy triggers:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Company - Akeyless Employee Onboarding Portal</title>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 40px;
background-color: #f8f9fa;
}
.card {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 600px;
margin: 0 auto;
}
.header-bar {
display: flex;
align-items: center;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 1px solid #e9ecef;
}
.logo {
height: 52px;
width: auto;
margin-right: 15px;
}
h2 {
color: #2c3e50;
margin: 0;
font-size: 22px;
}
label {
display: block;
margin-top: 15px;
font-weight: bold;
color: #34495e;
}
input[type="email"] {
width: 100%;
padding: 10px;
margin-top: 5px;
border: 1px solid #ccc;
border-radius: 4px;
box-sizing: border-box;
}
.submit-btn {
margin-top: 20px;
width: 100%;
padding: 12px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
}
.submit-btn:hover {
background: #0056b3;
}
.alert-success {
background: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.alert-warning {
background: #fff3cd;
color: #856404;
border: 1px solid #ffeeba;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.alert-warning pre {
white-space: pre-wrap;
word-break: break-all;
margin: 5px 0 0 0;
font-family: inherit;
}
.alert-error {
background: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
padding: 15px;
border-radius: 4px;
margin-bottom: 20px;
}
.alert-error pre {
white-space: pre-wrap;
word-break: break-all;
}
.cred-row {
display: flex;
align-items: center;
gap: 10px;
margin-top: 5px;
}
.cred-box {
flex-grow: 1;
background: #e9ecef;
padding: 10px 12px;
border-radius: 4px;
font-family: monospace;
font-size: 14px;
word-break: break-all;
border: 1px solid #ced4da;
}
.copy-btn {
padding: 10px 14px;
background: #6c757d;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background 0.2s ease;
white-space: nowrap;
}
.copy-btn:hover {
background: #495057;
}
.copy-btn.copied {
background: #28a745;
}
</style>
</head>
<body>
<div class="card">
<div class="header-bar">
<img src="{{ url_for('static', filename='logo.png') }}" alt="Company Logo" class="logo">
<h2>Akeyless Self-Service Portal</h2>
</div>
{% if success %}
<div class="alert-success">
<strong>✓ Account Successfully Provisioned!</strong>
<p>Akeyless credentials created for <strong>{{ email }}</strong>.</p>
<p><strong>Folder Structure:</strong></p>
<ul>
<li><strong>Auth Method:</strong> <code>/users/{{ username }}/api-key</code></li>
<li><strong>Role:</strong> <code>/roles/{{ username }}/role</code></li>
<li><strong>Access Path:</strong> <code>/personal/{{ username }}/*</code></li>
</ul>
<label>Access ID:</label>
<div class="cred-row">
<div class="cred-box">{{ access_id }}</div>
<button class="copy-btn" onclick="copyToClipboard('{{ access_id }}', this)">Copy</button>
</div>
<label>Access Key:</label>
<div class="cred-row">
<div class="cred-box">••••••••••••••••••••••••••••••••</div>
<button class="copy-btn" onclick="copyToClipboard('{{ access_key }}', this)">Copy</button>
</div>
</div>
{% endif %}
{% if warning %}
<div class="alert-warning">
<strong>⚠️ Account Already Exists</strong>
<pre>{{ warning }}</pre>
</div>
{% endif %}
{% if error %}
<div class="alert-error">
<strong>Error Executing Provisioning:</strong>
<pre>{{ error }}</pre>
</div>
{% endif %}
<form action="/provision" method="POST">
<label for="employee_email">Employee Email Address:</label>
<input type="email" id="employee_email" name="employee_email" placeholder="[email protected]" required>
<button type="submit" class="submit-btn">Provision Akeyless Account</button>
</form>
</div>
<script>
function copyToClipboard(text, btnElement) {
navigator.clipboard.writeText(text).then(function() {
const originalText = btnElement.innerText;
btnElement.innerText = "Copied!";
btnElement.classList.add("copied");
setTimeout(function() {
btnElement.innerText = originalText;
btnElement.classList.remove("copied");
}, 2000);
}).catch(function(err) {
console.error("Failed to copy text: ", err);
});
}
</script>
</body>
</html>Running the Workflow
- Install Flask:
pip install flask-
Add Your Company Logo:
Place your company or Akeyless logo file atstatic/logo.png. -
Start the Flask Server:
python app.pyThe portal runs locally on [http://127.0.0.1:5001](http://127.0.0.1:5001).
- Provision a User:
Open[http://127.0.0.1:5001](http://127.0.0.1:5001)in your browser. Enter a corporate email address (such as[email protected]) and click Provision Akeyless Account.
Terraform dynamically executes in the background, provisions the dedicated API key auth method and access role scoped strictly to /personal/john.doe/*, creates the folder, and returns the credentials directly onto the web interface with interactive copy buttons.
Verifying the Created Folder
Once the portal confirms provisioning, you can verify that the user's isolated folder exists using either the Akeyless Console or the Akeyless CLI.
Option 1: Via the Akeyless Web UI
- Log in to the Akeyless Console.
- In the left-hand navigation menu, select Items.
- Locate and click on the
/personaldirectory path. - Verify that a folder matching the employee's username (for example,
/personal/john.doe) is present. - Click on the folder to verify that it is empty and ready for the user to create their static, dynamic, or rotated secrets.
Option 2: Via the Akeyless CLI
You can verify the folder directly from your terminal using the CLI.
1. Authenticate with the Admin Profile or Admin Credentials:
akeyless auth --access-id "$TF_VAR_akeyless_access_id" --access-key "$TF_VAR_akeyless_access_key"2. List Items Inside the Personal Path:
Run akeyless list-items filtered to the target folder:
akeyless list-items --path "/personal/john.doe"If the folder was created successfully, this command returns an empty array [] (or any initial items if created) rather than an item-not-found error.
Updated about 2 hours ago
