Introduction
Some projects are born from a simple question: "Will it work?"
This is one of them. OpenStack Keystone — the OpenStack cloud identity and authentication service — was designed, developed, and tested exclusively on Linux. The official documentation doesn't even mention Windows to advise against it. It simply doesn't exist in that context.
And yet, here we are.
What initially seemed like a curious experiment turned into a journey through the bowels of Python, the hidden assumptions of open source software, and the vast gulf separating Linux from Windows beneath the surface.
What we discovered
The first lesson was immediate: Keystone doesn't just use Python — it uses Linux. Not explicitly, not with clear warnings, but silently, through dozens of dependencies that assume it runs on a POSIX system. Modules like grp, pwd, fcntl, termios, resource — all taken for granted, all nonexistent on Windows. Every failed import was a window into how deeply open source software is ingrained in the Unix ecosystem.
The second lesson was about versions. Python 3.14 was too new — ssl.wrap_socket had already been removed. Cygwin seemed like a shortcut but proved to be a dead end when it came to compiling libraries with native dependencies like cryptography, which in recent versions requires Rust. Native Python 3.11 on Windows was the right balance.
The third lesson, perhaps the most interesting, was discovering where the line between configuration and code ends. Typically, you configure an application by editing .conf files or environment variables. Here, however, we found ourselves writing stubs of entire modules, patching third-party library sources, and completely replacing files like pipe_mutex.py because it imported eventlet.asyncio — removed in recent versions of eventlet. The line between "user" and "developer" disappeared completely.
The paradox of this project
There's something deliberately absurd about all this. Keystone is the simplest component of OpenStack — it only handles authentication and authorization. Subsequent services — Nova for compute, Neutron for networking, Cinder for storage — depend on even deeper Linux features: KVM, libvirt, Open vSwitch, network namespaces, LVM. Running them on Windows isn't difficult: it's impossible by design.
So why do it?
Because the process of attempting the impossible teaches more than the process of doing the obvious. Every mistake revealed something: about how Python handles modules, about how open-source libraries assume their environment, about how software that seems portable often isn't as soon as you scratch the surface.
And finally, Keystone runs. On Windows. With MariaDB. It responds to REST requests, issues tokens, and authenticates users. It's not production-ready, it's not supported, it's not sensible — but it works.
Sometimes that's all it takes.
Prerequisites
Before proceeding, install the following:
-
Python 3.11 — not 3.12+, not 3.14.
ssl.wrap_socketwas removed in 3.12 andeventletbreaks.- Download: https://www.python.org/ftp/python/3.11.9/python-3.11.9-amd64.exe
- ✅ Check "Add Python to PATH" during installation
MariaDB for Windows: https://mariadb.org/download/
Notepad++ (recommended for editing Python sources): https://notepad-plus-plus.org/downloads/
Step 1 — Configure MariaDB
Open C:\Program Files\MariaDB xx.x\data\my.ini and add under [mysqld]:
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 200
Restart the service:
net stop mariadb
net start mariadb
Step 2 — Create the Keystone Database
Open the MariaDB client:
cd "C:\Program Files\MariaDB xx.x\bin"
mysql -u root
Then run:
CREATE DATABASE keystone CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'keystone'@'localhost' IDENTIFIED BY 'your_password';
GRANT ALL PRIVILEGES ON keystone.* TO 'keystone'@'localhost';
FLUSH PRIVILEGES;
Step 3 — Install Python Packages
pip install pymysql cryptography waitress eventlet keystone python-openstackclient
On native Windows, cryptography installs from prebuilt wheels — no Rust needed. This is one of the key reasons to avoid Cygwin for this setup.
Step 4 — Create the Keystone Configuration
mkdir C:\keystone
mkdir C:\keystone\fernet-keys
mkdir C:\keystone\credential-keys
Create C:\keystone\keystone.conf:
[DEFAULT]
log_file = C:/keystone/keystone.log
[database]
connection = mysql+pymysql://keystone:your_password@127.0.0.1/keystone
[token]
provider = fernet
[fernet_tokens]
key_repository = C:/keystone/fernet-keys
[credential]
key_repository = C:/keystone/credential-keys
⚠️ The
key_repositoryvalues will be ignored by Keystone due to hardcoded internal defaults. We will patch this in the next step.
Step 5 — Patch Keystone for Windows
This is the heart of the guide. Keystone assumes a POSIX environment throughout its codebase. We need to fix this systematically.
Your site-packages folder is at:
C:\Users\<username>\AppData\Local\Programs\Python\Python311\Lib\site-packages\
5a. Create missing Unix module stubs
Create each of these files directly in site-packages\:
grp.py
class struct_group:
def __init__(self, name, passwd, gid, mem):
self.gr_name = name; self.gr_passwd = passwd
self.gr_gid = gid; self.gr_mem = mem
def getgrnam(name): return struct_group(name, '', 0, [])
def getgrgid(gid): return struct_group('users', '', gid, [])
def getgrall(): return [struct_group('users', '', 0, [])]
pwd.py
class struct_passwd:
def __init__(self, name, uid, gid, dir):
self.pw_name = name; self.pw_passwd = ''
self.pw_uid = uid; self.pw_gid = gid
self.pw_gecos = ''; self.pw_dir = dir; self.pw_shell = ''
def getpwnam(name):
import os
return struct_passwd(name, 0, 0, os.path.expanduser('~'))
def getpwuid(uid):
import os
return struct_passwd('username', uid, 0, os.path.expanduser('~'))
def getpwall(): return [getpwuid(0)]
fcntl.py
import struct
LOCK_SH=1; LOCK_EX=2; LOCK_NB=4; LOCK_UN=8
F_GETFD=1; F_SETFD=2; F_GETFL=3; F_SETFL=4
def fcntl(fd, cmd, arg=0): return 0
def ioctl(fd, request, arg=0, mutate_flag=True):
if isinstance(arg, bytes) and len(arg) == 8:
return struct.pack("HHHH", 24, 80, 0, 0)
return 0
def flock(fd, operation): return 0
def lockf(fd, cmd, length=0, start=0, whence=0): return 0
termios.py
TCSANOW=0; TCSADRAIN=1; TCSAFLUSH=2; TIOCGWINSZ=0x5413
def tcgetattr(fd): return [0,0,0,0,0,0,[]]
def tcsetattr(fd, when, attrs): pass
def tcdrain(fd): pass
def tcflush(fd, queue): pass
def tcflow(fd, action): pass
resource.py
import mmap
RLIMIT_NOFILE=7; RLIMIT_CPU=0; RLIMIT_AS=9; RLIM_INFINITY=float('inf')
def getrlimit(resource): return (1024, 4096)
def setrlimit(resource, limits): pass
def getpagesize(): return mmap.PAGESIZE
def getrusage(who):
return type('r',(),{k:0 for k in [
'ru_utime','ru_stime','ru_maxrss','ru_ixrss','ru_idrss','ru_isrss',
'ru_minflt','ru_majflt','ru_nswap','ru_inblock','ru_oublock',
'ru_msgsnd','ru_msgrcv','ru_nsignals','ru_nvcsw','ru_nivcsw']})()
sitecustomize.py — loaded automatically by Python at every startup:
import os
if not hasattr(os, 'geteuid'): os.geteuid = lambda: 0
if not hasattr(os, 'getuid'): os.getuid = lambda: 0
if not hasattr(os, 'getgid'): os.getgid = lambda: 0
if not hasattr(os, 'getegid'): os.getegid = lambda: 0
if not hasattr(os, 'getgroups'):os.getgroups= lambda: [0]
if not hasattr(os, 'getlogin'): os.getlogin = lambda: os.environ.get('USERNAME','admin')
if not hasattr(os, 'setegid'): os.setegid = lambda gid: None
if not hasattr(os, 'seteuid'): os.seteuid = lambda uid: None
if not hasattr(os, 'setgid'): os.setgid = lambda gid: None
if not hasattr(os, 'setuid'): os.setuid = lambda uid: None
if not hasattr(os, 'setgroups'):os.setgroups= lambda groups: None
if not hasattr(os, 'chown'): os.chown = lambda path, uid, gid: None
5b. Patch existing source files
keystone\cmd\cli.py — find and replace:
# Before
running_as_root = os.geteuid() == 0
# After
running_as_root = False
oslo_log\pipe_mutex.py — replace the entire file content:
import threading
class PipeMutex:
def __init__(self):
self._lock = threading.Lock()
self.owner = None; self.count = 0
def acquire(self, blocking=True):
if self.owner == threading.current_thread():
self.count += 1; return True
result = self._lock.acquire(blocking)
if result:
self.owner = threading.current_thread(); self.count = 1
return result
def release(self):
if self.owner != threading.current_thread():
raise RuntimeError("release() called from wrong thread")
self.count -= 1
if self.count == 0:
self.owner = None; self._lock.release()
def __enter__(self): self.acquire(); return self
def __exit__(self, *args): self.release()
def pipe_createLock(self):
self.lock = PipeMutex()
5c. Patch hardcoded /etc/keystone paths
Keystone has paths like /etc/keystone/fernet-keys/ hardcoded as defaults in its config options. Find them:
findstr /r /s "etc.keystone" C:\Users\PC\AppData\Local\Programs\Python\Python311\Lib\site-packages\keystone\*.py
In each file found, replace:
# Before
default='/etc/keystone/fernet-keys/'
default='/etc/keystone/credential-keys/'
# After
default='C:\\keystone\\fernet-keys\\'
default='C:\\keystone\\credential-keys\\'
Step 6 — Set the Config Environment Variable
Make it permanent so every new cmd session picks it up:
setx OS_KEYSTONE_CONFIG_FILES "C:\keystone\keystone.conf"
Close and reopen cmd after this command.
Step 7 — Initialize the Database
keystone-manage --config-file C:\keystone\keystone.conf db_sync
Check C:\keystone\keystone.log to confirm the migration completed successfully.
Step 8 — Generate Fernet and Credential Keys
First try the standard commands:
keystone-manage --config-file C:\keystone\keystone.conf fernet_setup --keystone-user %USERNAME% --keystone-group Users
keystone-manage --config-file C:\keystone\keystone.conf credential_setup --keystone-user %USERNAME% --keystone-group Users
If the folders remain empty (common on Windows), generate the keys directly with Python:
# C:\keystone\gen_keys.py
from cryptography.fernet import Fernet
import os
for d in ['C:/keystone/fernet-keys', 'C:/keystone/credential-keys']:
os.makedirs(d, exist_ok=True)
for i in [0, 1]:
with open(f'{d}/{i}', 'wb') as f:
f.write(Fernet.generate_key())
print(f'Created: {d}/{i}')
print('Done!')
python C:\keystone\gen_keys.py
Step 9 — Bootstrap Keystone
keystone-manage --config-file C:\keystone\keystone.conf bootstrap ^
--bootstrap-password adminpass ^
--bootstrap-admin-url http://localhost:5000/v3/ ^
--bootstrap-internal-url http://localhost:5000/v3/ ^
--bootstrap-public-url http://localhost:5000/v3/ ^
--bootstrap-region-id RegionOne
Replace adminpass with a strong password of your choice.
Step 10 — Create the Startup Script
Create C:\keystone\start_keystone.py:
import os
# Must be set BEFORE any keystone import
os.environ["OS_KEYSTONE_CONFIG_FILES"] = "C:/keystone/keystone.conf"
import eventlet
eventlet.monkey_patch(os=False)
from waitress import serve
from keystone.server.wsgi import initialize_public_application
app = initialize_public_application()
print("Keystone listening on http://0.0.0.0:5000")
serve(app, host="0.0.0.0", port=5000, threads=8)
Start it:
python C:\keystone\start_keystone.py
You should see:
Keystone listening on http://0.0.0.0:5000
Step 11 — Test Keystone
Open a new cmd window and run:
set OS_USERNAME=admin
set OS_PASSWORD=adminpass
set OS_PROJECT_NAME=admin
set OS_USER_DOMAIN_ID=default
set OS_PROJECT_DOMAIN_ID=default
set OS_AUTH_URL=http://localhost:5000/v3
set OS_IDENTITY_API_VERSION=3
openstack token issue
A successful response will return a valid token — Keystone is working.
You can also verify via browser or curl: http://localhost:5000/v3
Step 12 — Run as a Windows Service (Optional)
To start Keystone automatically at boot without running the script manually, use NSSM (Non-Sucking Service Manager).
Install NSSM
curl -L https://nssm.cc/release/nssm-2.24.zip -o C:\nssm.zip
tar -xf C:\nssm.zip -C C:\
Register the service
C:\nssm-2.24\win64\nssm.exe install KeystoneService "C:\Users\<username>\AppData\Local\Programs\Python\Python311\python.exe"
C:\nssm-2.24\win64\nssm.exe set KeystoneService AppParameters "C:\keystone\start_keystone.py"
C:\nssm-2.24\win64\nssm.exe set KeystoneService AppDirectory "C:\keystone"
C:\nssm-2.24\win64\nssm.exe set KeystoneService DisplayName "OpenStack Keystone"
C:\nssm-2.24\win64\nssm.exe set KeystoneService Description "OpenStack Identity Service"
C:\nssm-2.24\win64\nssm.exe set KeystoneService Start SERVICE_AUTO_START
C:\nssm-2.24\win64\nssm.exe set KeystoneService AppEnvironmentExtra "OS_KEYSTONE_CONFIG_FILES=C:\keystone\keystone.conf"
C:\nssm-2.24\win64\nssm.exe set KeystoneService AppStdout "C:\keystone\service.log"
C:\nssm-2.24\win64\nssm.exe set KeystoneService AppStderr "C:\keystone\service_error.log"
Manage the service
C:\nssm-2.24\win64\nssm.exe start KeystoneService
C:\nssm-2.24\win64\nssm.exe stop KeystoneService
C:\nssm-2.24\win64\nssm.exe restart KeystoneService
C:\nssm-2.24\win64\nssm.exe status KeystoneService
C:\nssm-2.24\win64\nssm.exe remove KeystoneService confirm
Summary of All Hacks Applied
| Problem | Solution |
|---|---|
Python 3.12+ removes ssl.wrap_socket
|
Use Python 3.11 |
grp, pwd don't exist on Windows |
Create Python stubs |
fcntl, termios, resource missing |
Create Python stubs |
os.geteuid, os.setegid etc. missing |
sitecustomize.py with stubs |
oslo_log.pipe_mutex imports eventlet.asyncio
|
Replace with threading-based version |
fernet_setup ignores config on Windows |
Generate keys manually with Python |
gunicorn not supported on Windows |
Use waitress instead |
mysqlclient won't compile |
Use pure-Python PyMySQL
|
Hardcoded /etc/keystone paths |
Patch source defaults directly |
cryptography requires Rust on Cygwin |
Use native Windows Python (prebuilt wheels) |
Final Thoughts
This setup works — but it's a house of cards. Every pip update, every Keystone version bump, every eventlet release could silently break one of these patches. The correct answer for running OpenStack on Windows is WSL2 or a Linux VM with DevStack.
But if you want to understand what OpenStack actually does under the hood, wrestling with it on an unsupported platform is one of the best ways to learn. You'll never take grp.getgrnam() for granted again.
Have questions or found additional patches needed? Drop them in the comments.
Top comments (0)