Skip to content
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ dist/
!.env.example

.DS_Store

# local, per-developer agent preferences: never versioned
.claude/*.local.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Who are you, and what are you doing here?"
description: "IAM (Identity & Access Management) underpins the security of almost every modern application. But what is it really, what does it do, and why is it so hard to get right? We break it all down — and introduce FerrisKey."
description: "IAM (Identity & Access Management) underpins the security of almost every modern application. But what is it really, what does it do, and why is it so hard to get right? We take it apart, and introduce FerrisKey."
short_description: "Understand IAM from scratch, and discover FerrisKey."
date: 2026-03-19
tags: [iam, identity, access-management, ferriskey, rust, open-source]
Expand All @@ -16,7 +16,7 @@ A deceptively simple question. But behind it lies an entire discipline, one that

## The problem, put simply

Picture a large company. Hundreds of employees. Dozens of applications accounting, HR, CRM, dev tooling, production servers. Every morning, people log in. Every evening, they log out. In between, they access data, trigger processes, read documents.
Picture a large company. Hundreds of employees. Dozens of applications: accounting, HR, CRM, dev tooling, production servers. Every morning, people log in. Every evening, they log out. In between, they access data, trigger processes, read documents.

Now, a few simple questions:

Expand All @@ -43,15 +43,15 @@ It knows who exists. Every human user, every application, every machine service
:::

:::card{label="Authentication" icon="lucide:key-round"}
It verifies you are who you claim to bepassword, TOTP, hardware key, magic link. When you "Sign in with Google", Google is the IAM.
It verifies you are who you claim to be, with a password, a TOTP code, a hardware key, or a magic link. When you "Sign in with Google", Google is the IAM.
:::

:::card{label="Authorization" icon="lucide:shield-check"}
Once your identity is confirmed, it decides what you're allowed to do. Read? Write? Delete? Administer? Roles and permissions, assigned with precision.
:::

:::card{label="Standard protocols" icon="lucide:globe"}
A good IAM speaks OAuth2 and OpenID Connect (OIDC) — the universal languages of the web. Apps can delegate authentication without ever touching passwords.
A good IAM speaks OAuth2 and OpenID Connect, the two languages the whole web already understands. Apps can delegate authentication without ever touching passwords.
:::

:::card{label="Audit" icon="lucide:scroll-text"}
Expand All @@ -73,7 +73,7 @@ IAM sounds simple. It's just a `users` table with a `password_hash`, right?
Managing identities seriously means dealing with dozens of problems that stack up fast:
secret rotation, token revocation, session expiration, service accounts, multi-tenancy, MFA strategies, webhooks, tamper-proof audit logs…

And all of this has to hold under load. An IAM is a **mandatory chokepoint** — every authenticated request goes through it. If it's slow or goes down, your entire infrastructure stops.
And all of this has to hold under load. An IAM is a mandatory chokepoint: every authenticated request goes through it. If it slows down or falls over, everything behind it stops.
:::

That's why large enterprises pay fortunes for solutions like Okta, Auth0, or Azure AD. On the open source side, Keycloak has been around for a long time and does the job, but it carries significant configuration complexity and a JVM footprint that doesn't suit everyone.
Expand All @@ -87,10 +87,10 @@ FerrisKey doesn't claim to be the definitive solution. It's an alternative, with
FerrisKey is an open source IAM system, built in **Rust**, with a hexagonal architecture.

:::callout{variant="info" title="Why Rust?"}
Performance and reliability are non-negotiable for a component this critical. Rust delivers C-level speed without the pitfalls — no garbage collector, no data races, no memory surprises. For a service handling millions of authentications, that's exactly what you want.
Performance and reliability are non-negotiable for a component this critical. Rust gives you C-level speed without the traps: no garbage collector, no data races, no memory surprises. For a service handling millions of authentications, that's exactly what you want.
:::

The hexagonal architecture ensures that business logic stays pure and decoupled from infrastructure. The database can change, the HTTP framework can evolve the domain core stays stable and testable.
The hexagonal architecture ensures that business logic stays pure and decoupled from infrastructure. The database can change and the HTTP framework can evolve while the domain core stays stable and testable.

What a decoded OIDC ID token looks like when FerrisKey issues it:

Expand All @@ -112,31 +112,31 @@ In practice, FerrisKey gives you:

::::card-group{cols=2}
:::card{label="Realms" icon="lucide:layers"}
Fully isolated spaces. Each organization, project, or environment gets its own realm its own users, roles, and clients. No cross-contamination possible.
Fully isolated spaces. Each organization, project, or environment gets its own realm, with its own users, roles, and clients. Nothing crosses over.
:::

:::card{label="OAuth2 & OIDC" icon="lucide:lock-keyhole"}
The web's standards, implemented correctly, so your apps can delegate authentication without friction.
:::

:::card{label="Trident MFA" icon="lucide:shield"}
:::card{label="Trident, for MFA" icon="lucide:shield"}
FerrisKey's MFA module: TOTP, WebAuthn (hardware keys, passkeys), magic links, and recovery codes.
:::

:::card{label="SeaWatch — Audit" icon="lucide:eye"}
:::card{label="SeaWatch, for audit" icon="lucide:eye"}
A complete audit system, queryable from the console, tracing every security event across your realms.
:::

:::card{label="Webhooks" icon="lucide:webhook"}
Connect FerrisKey to your existing systems and react to lifecycle events user creation, access revocation, and more.
Connect FerrisKey to your existing systems and react to lifecycle events: a user created, an access revoked, and the rest.
:::

:::card{label="Cloud-native" icon="lucide:cloud"}
An official Helm chart, Kubernetes Operator, and Docker Compose support for frictionless deployment at any scale.
:::
::::

:::callout{variant="success" title="Apache 2.0 fully open"}
:::callout{variant="success" title="Apache 2.0, fully open"}
No paywall. No "enterprise" tier hiding critical features behind a contact form. The code is open, the community is here.
:::

Expand All @@ -146,7 +146,7 @@ No paywall. No "enterprise" tier hiding critical features behind a contact form.

::::card-group{cols=3}
:::card{label="SaaS teams" icon="lucide:rocket"}
Building a platform and want robust auth without starting from scratch or paying per monthly active user.
Building a platform and want solid auth without starting from scratch, or paying per monthly active user.
:::

:::card{label="Platform engineers" icon="lucide:server"}
Expand Down
2 changes: 1 addition & 1 deletion apps/blog/src/content/posts/en/why-rust-for-an-iam.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ For the SREs who get paged when it isn't: no garbage collector means no GC pause

## What determinism buys us next

Memory safety gets the headlines, but it's not the only property this design gives us for free. Ownership and the absence of a garbage collector also make the runtime deterministic: no GC pause, no JIT warm-up curve, no allocator quietly deciding to compact the heap under load. Feed the same input to the same binary twice and it takes the same path both times, with the same timing characteristics. That's not just a safety property, it's a performance one too: the same determinism that rules out data races is what keeps tail latency boring under load.
Memory safety gets the headlines, but it's not the only property this design gives us for free. Ownership and the absence of a garbage collector also make the runtime deterministic: no GC pause, no JIT warm-up curve, no allocator quietly deciding to compact the heap under load. Feed the same input to the same binary twice and it takes the same path both times, with the same timing characteristics. That determinism buys you more than safety. The same property that rules out data races is what keeps tail latency boring under load.

That determinism is also a foundation, not just an outcome, and it points at two things we want FerrisKey to grow into.

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Qui es-tu, et qu'est-ce que tu fais là ?"
description: "L'IAM (Identity & Access Management) est au cœur de la sécurité de presque toutes les applications modernes. Mais qu'est-ce que c'est vraiment, à quoi ça sert, et pourquoi c'est si difficile à bien faire ? On démêle tout et on vous présente FerrisKey."
description: "L'IAM (Identity & Access Management) est au cœur de la sécurité de presque toutes les applications modernes. Mais qu'est-ce que c'est vraiment, à quoi ça sert, et pourquoi c'est si difficile à bien faire ? On démêle tout, et on vous présente FerrisKey."
short_description: "Comprendre l'IAM de zéro, et découvrir FerrisKey."
date: 2026-03-19
tags: [iam, identity, access-management, ferriskey, rust, open-source]
Expand All @@ -16,7 +16,7 @@ Une question trompeusement simple. Mais derrière elle se cache toute une discip

## Le problème, en clair

Imaginez une grande entreprise. Des centaines d'employés. Des dizaines d'applications comptabilité, RH, CRM, outils de dev, serveurs de production. Chaque matin, les gens se connectent. Chaque soir, ils se déconnectent. Entre-temps, ils accèdent à des données, déclenchent des processus, lisent des documents.
Imaginez une grande entreprise. Des centaines d'employés. Des dizaines d'applications : comptabilité, RH, CRM, outils de dev, serveurs de production. Chaque matin, les gens se connectent. Chaque soir, ils se déconnectent. Entre-temps, ils accèdent à des données, déclenchent des processus, lisent des documents.

Quelques questions simples :

Expand All @@ -43,15 +43,15 @@ Il sait qui existe. Chaque utilisateur humain, chaque application, chaque servic
:::

:::card{label="Authentification" icon="lucide:key-round"}
Il vérifie que vous êtes bien qui vous prétendez être mot de passe, TOTP, clé matérielle, magic link. Quand vous faites « Se connecter avec Google », Google est l'IAM.
Il vérifie que vous êtes bien qui vous prétendez être, par mot de passe, TOTP, clé matérielle ou magic link. Quand vous faites « Se connecter avec Google », Google est l'IAM.
:::

:::card{label="Autorisation" icon="lucide:shield-check"}
Une fois votre identité confirmée, il décide ce que vous avez le droit de faire. Lire ? Écrire ? Supprimer ? Administrer ? Des rôles et permissions assignés avec précision.
:::

:::card{label="Protocoles standards" icon="lucide:globe"}
Un bon IAM parle OAuth2 et OpenID Connect (OIDC) — les langages universels du web. Les applications peuvent déléguer l'authentification sans jamais toucher aux mots de passe.
Un bon IAM parle OAuth2 et OpenID Connect, les deux langages que tout le web comprend déjà. Les applications peuvent déléguer l'authentification sans jamais toucher aux mots de passe.
:::

:::card{label="Audit" icon="lucide:scroll-text"}
Expand All @@ -73,7 +73,7 @@ L'IAM paraît simple. C'est juste une table `users` avec un `password_hash`, non
Gérer des identités sérieusement, c'est faire face à des dizaines de problèmes qui s'accumulent vite :
rotation des secrets, révocation de tokens, expiration de sessions, comptes de service, multi-tenancy, stratégies MFA, webhooks, journaux d'audit infalsifiables…

Et tout ça doit tenir sous charge. Un IAM est un **point de passage obligatoire** — chaque requête authentifiée le traverse. S'il est lent ou tombe, toute votre infrastructure s'arrête.
Et tout ça doit tenir sous charge. Un IAM est un point de passage obligatoire : chaque requête authentifiée le traverse. S'il ralentit ou tombe, tout ce qu'il y a derrière s'arrête.
:::

C'est pourquoi les grandes entreprises paient des fortunes pour des solutions comme Okta, Auth0 ou Azure AD. Côté open source, Keycloak existe depuis longtemps et fait le travail, mais il embarque une complexité de configuration significative et une empreinte JVM qui ne convient pas à tout le monde.
Expand All @@ -87,10 +87,10 @@ FerrisKey ne prétend pas être la solution définitive. C'est une alternative,
FerrisKey est un système IAM open source, construit en **Rust**, avec une architecture hexagonale.

:::callout{variant="info" title="Pourquoi Rust ?"}
La performance et la fiabilité ne sont pas négociables pour un composant aussi critique. Rust offre une vitesse comparable au C sans les pièges pas de garbage collector, pas de data races, pas de surprises mémoire. Pour un service qui gère des millions d'authentifications, c'est exactement ce qu'il faut.
La performance et la fiabilité ne sont pas négociables pour un composant aussi critique. Rust offre une vitesse comparable au C sans les pièges : pas de garbage collector, pas de data races, pas de surprises mémoire. Pour un service qui gère des millions d'authentifications, c'est exactement ce qu'il faut.
:::

L'architecture hexagonale garantit que la logique métier reste pure et découplée de l'infrastructure. La base de données peut changer, le framework HTTP peut évoluer le cœur du domaine reste stable et testable.
L'architecture hexagonale garantit que la logique métier reste pure et découplée de l'infrastructure. La base de données peut changer et le framework HTTP évoluer, le cœur du domaine reste stable et testable.

Voici à quoi ressemble un ID token OIDC décodé émis par FerrisKey :

Expand All @@ -112,31 +112,31 @@ En pratique, FerrisKey vous offre :

::::card-group{cols=2}
:::card{label="Realms" icon="lucide:layers"}
Des espaces totalement isolés. Chaque organisation, projet ou environnement dispose de son propre realm ses propres utilisateurs, rôles et clients. Zéro contamination croisée possible.
Des espaces totalement isolés. Chaque organisation, projet ou environnement dispose de son propre realm, avec ses utilisateurs, ses rôles et ses clients. Rien ne traverse.
:::

:::card{label="OAuth2 & OIDC" icon="lucide:lock-keyhole"}
Les standards du web, implémentés correctement, pour que vos applications puissent déléguer l'authentification sans friction.
:::

:::card{label="Trident MFA" icon="lucide:shield"}
:::card{label="Trident, pour le MFA" icon="lucide:shield"}
Le module MFA de FerrisKey : TOTP, WebAuthn (clés matérielles, passkeys), magic links et codes de récupération.
:::

:::card{label="SeaWatch — Audit" icon="lucide:eye"}
:::card{label="SeaWatch, pour l'audit" icon="lucide:eye"}
Un système d'audit complet, interrogeable depuis la console, traçant chaque événement de sécurité dans vos realms.
:::

:::card{label="Webhooks" icon="lucide:webhook"}
Connectez FerrisKey à vos systèmes existants et réagissez aux événements du cycle de vie création d'utilisateur, révocation d'accès, et plus encore.
Connectez FerrisKey à vos systèmes existants et réagissez aux événements du cycle de vie : création d'utilisateur, révocation d'accès, et le reste.
:::

:::card{label="Cloud-native" icon="lucide:cloud"}
Un chart Helm officiel, un Kubernetes Operator et le support Docker Compose pour un déploiement sans friction à toute échelle.
:::
::::

:::callout{variant="success" title="Apache 2.0 entièrement ouvert"}
:::callout{variant="success" title="Apache 2.0, entièrement ouvert"}
Pas de paywall. Pas de tier « enterprise » qui cache des fonctionnalités critiques derrière un formulaire de contact. Le code est ouvert, la communauté est là.
:::

Expand All @@ -146,7 +146,7 @@ Pas de paywall. Pas de tier « enterprise » qui cache des fonctionnalités crit

::::card-group{cols=3}
:::card{label="Équipes SaaS" icon="lucide:rocket"}
Vous construisez une plateforme et voulez une auth robuste sans repartir de zéro ni payer au nombre d'utilisateurs actifs mensuels.
Vous construisez une plateforme et voulez une auth solide sans repartir de zéro, ni payer au nombre d'utilisateurs actifs mensuels.
:::

:::card{label="Ingénieurs plateforme" icon="lucide:server"}
Expand Down
2 changes: 1 addition & 1 deletion apps/blog/src/content/posts/fr/why-rust-for-an-iam.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Pour les SRE qui sont réveillés quand ce n'est pas le cas : pas de garbage col

## Ce que le déterminisme nous permet ensuite

La sécurité mémoire fait les gros titres, mais ce n'est pas la seule propriété que ce design nous offre gratuitement. L'ownership et l'absence de garbage collector rendent aussi le runtime déterministe : pas de pause GC, pas de courbe de chauffe JIT, pas d'allocateur qui décide discrètement de compacter le heap sous charge. Donnez la même entrée au même binaire deux fois, il emprunte le même chemin les deux fois, avec les mêmes caractéristiques de timing. Ce n'est pas qu'une propriété de sécurité, c'est aussi une propriété de performance : le même déterminisme qui exclut les data races est ce qui garde la latence de queue prévisible sous charge.
La sécurité mémoire fait les gros titres, mais ce n'est pas la seule propriété que ce design nous offre gratuitement. L'ownership et l'absence de garbage collector rendent aussi le runtime déterministe : pas de pause GC, pas de courbe de chauffe JIT, pas d'allocateur qui décide discrètement de compacter le heap sous charge. Donnez la même entrée au même binaire deux fois, il emprunte le même chemin les deux fois, avec les mêmes caractéristiques de timing. Ce déterminisme rapporte plus que de la sûreté. La même propriété qui exclut les data races est celle qui garde la latence de queue prévisible sous charge.

Ce déterminisme est aussi une fondation, pas juste un résultat, et il pointe vers deux choses qu'on veut voir grandir chez FerrisKey.

Expand Down
4 changes: 2 additions & 2 deletions apps/blog/src/layouts/base.astro
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ interface Props {
const { title, description = 'Explainer Blog', thumbnail, locale = 'en', locales = ['en'], localeSwitchUrls = {} } = Astro.props
const t = useTranslations(locale)
const thumbnailPath = thumbnail ?? `${Astro.url.pathname.replace(/\/$/, '')}/thumbnail.png`
// og:image/twitter:image must be absolute Discord and most link-preview bots won't resolve a relative path.
// og:image/twitter:image must be absolute: Discord and most link-preview bots won't resolve a relative path.
const thumbnailUrl = Astro.site ? new URL(thumbnailPath, Astro.site).toString() : thumbnailPath

const appUrlOverrides = {
Expand All @@ -41,7 +41,7 @@ const appUrlOverrides = {
<meta name="twitter:title" content={title} />
<meta name="twitter:description" content={description} />
<meta name="twitter:image" content={thumbnailUrl} />
<title>{title} Blog</title>
<title>{title} | Blog</title>
<ClientRouter />
<script is:inline>
(function () {
Expand Down
2 changes: 1 addition & 1 deletion apps/blog/src/lib/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export function getPostLocale(post: Post): string {
}

export function getPostSlug(post: Post): string {
// id is "en/my-post.mdx" or "en/my-post" strip locale prefix and extension
// id is "en/my-post.mdx" or "en/my-post": strip locale prefix and extension
const withoutLocale = post.id.includes('/') ? post.id.split('/').slice(1).join('/') : post.id
return withoutLocale.replace(/\.mdx$/, '')
}
Expand Down
2 changes: 1 addition & 1 deletion apps/blog/src/pages/404.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Base from '../layouts/base.astro'
import { defaultLang } from '../i18n/ui'
---

<Base title="404 Page not found" locale={defaultLang}>
<Base title="404 | Page not found" locale={defaultLang}>
<section class="flex-1 flex flex-col items-center justify-center text-center">
<p class="text-primary text-sm font-semibold tracking-widest uppercase mb-4">404</p>
<h1 class="text-4xl md:text-5xl font-bold tracking-tight mb-4">Page not found</h1>
Expand Down
2 changes: 1 addition & 1 deletion apps/blog/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const tags = getAllTags(posts)
const locale = defaultLang
const t = useTranslations(locale)

// Split into featured/rest per locale, not across the mixed-locale array
// Split into featured/rest per locale, not across the mixed-locale array:
// otherwise an EN and FR copy of the same post can both land in the 2
// featured slots, and hiding the wrong-locale one leaves an empty grid gap.
const postsByLocale = locales.map((loc) => {
Expand Down
Loading
Loading