finish
This commit is contained in:
parent
015de8232d
commit
6573c8fbdc
106
README.md
Normal file
106
README.md
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
# Keycloak allow/ban plugin
|
||||||
|
|
||||||
|
This is a plugin for Keycloak that checks an allow-list and a ban list for
|
||||||
|
users from GitHub.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Add the plugin to your keycloak plugins list. The plugin can be obtained via
|
||||||
|
`nix build .#packages.default`.
|
||||||
|
|
||||||
|
The configuration of this plugin is in a directory of text files with the
|
||||||
|
format `github-id github-username`, allowing `#` comments.
|
||||||
|
|
||||||
|
Specify a Keycloak config file option
|
||||||
|
`spi-authenticator-allow-ban-check-authenticator-dbpath` pointing to the
|
||||||
|
directory with the configuration. Note that the error if you don't configure
|
||||||
|
this is complete garbage, and is also not printed by default (sorry)! Use
|
||||||
|
`kc.sh --verbose start` to read your `NullPointerException`.
|
||||||
|
|
||||||
|
There are three notable files in there:
|
||||||
|
- `banned-users.txt`: contains a list of GitHub IDs which will be rejected
|
||||||
|
outright on login.
|
||||||
|
|
||||||
|
If you newly ban a user, you have to kill all their sessions across all
|
||||||
|
infrastructure, including existing Keycloak sessions, since bans only apply
|
||||||
|
on login.
|
||||||
|
- `allowed-users.txt`: contains a list of GitHub IDs which will be allowed if the
|
||||||
|
allow-list is enabled.
|
||||||
|
- `use-allow-list.txt`: if present, the allow-list mechanism is used. Otherwise
|
||||||
|
it is bypassed and all logins are allowed.
|
||||||
|
|
||||||
|
The intent of the configuration is that it is synced by a cron job pulling a
|
||||||
|
git repo.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. In the GitHub Identity Provider configuration on Keycloak, set up a mapper with
|
||||||
|
type "Attribute Importer", importing the JSON field path "id" as a user
|
||||||
|
profile attribute "githubId".
|
||||||
|
|
||||||
|
2. Create an auth flow for post login on the identity provider, containing one
|
||||||
|
element "Allow/Ban check". This is necessary since it bypasses the standard
|
||||||
|
login flow if you log in via the external IdP.
|
||||||
|
|
||||||
|
3. In the identity provider, set the *Post Login Flow* to the flow just
|
||||||
|
created.
|
||||||
|
|
||||||
|
4. Add the "Allow/Ban check" action to the main login flow as a Required
|
||||||
|
element at a point *after* the username is determined.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
We are unsure if Store Tokens is necessary to set; it is not for this plugin,
|
||||||
|
but it might be a good idea to simply have them around.
|
||||||
|
|
||||||
|
We don't think there are ways to ban-evade, since this is managed by a
|
||||||
|
user-invisible profile attribute that is permanently glued to all accounts
|
||||||
|
originating from GitHub.
|
||||||
|
|
||||||
|
We have tested this on Keycloak 23 and 24.
|
||||||
|
|
||||||
|
### Test environment!
|
||||||
|
|
||||||
|
There is a test environment included with this plugin to avoid testing in prod.
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```
|
||||||
|
nix run .#
|
||||||
|
```
|
||||||
|
|
||||||
|
Then in a separate terminal:
|
||||||
|
|
||||||
|
```
|
||||||
|
sudo socat TCP-LISTEN:443,fork,reuseaddr TCP:127.0.0.1:4043
|
||||||
|
```
|
||||||
|
|
||||||
|
and add `127.0.0.1 identity.test.lix.systems` to `/etc/hosts`. Dump Firefox
|
||||||
|
DNS cache if necessary (`about:networking`), and create a GitHub OAuth app.
|
||||||
|
|
||||||
|
You can ssh into the machine on port 2022 on localhost as root, with no
|
||||||
|
password.
|
||||||
|
|
||||||
|
Then finally go to `https://identity.test.lix.systems/superadmin`, and log in
|
||||||
|
with `admin`/`Password1`.
|
||||||
|
|
||||||
|
### Attaching a debugger to Keycloak
|
||||||
|
|
||||||
|
We are so sorry.
|
||||||
|
|
||||||
|
If you are doing this to the VM here, change the last line of the keycloak
|
||||||
|
startup script to this:
|
||||||
|
|
||||||
|
`DEBUG=true DEBUG_PORT='*:1337' DEBUG_SUSPEND=y kc.sh --verbose start --optimized --debug`
|
||||||
|
|
||||||
|
To actually make that work, you want to copy the file from `systemctl cat
|
||||||
|
keycloak.service` to `/start-keycloak`, then `systemctl edit --runtime
|
||||||
|
keycloak`, with the contents:
|
||||||
|
|
||||||
|
```
|
||||||
|
[Service]
|
||||||
|
ExecStart=
|
||||||
|
ExecStart=/start-keycloak
|
||||||
|
```
|
||||||
|
|
||||||
|
Then `systemctl restart keycloak`. Next create a forward like `ssh
|
||||||
|
-L1337:localhost:1337 localvm` and attach your Java debugger to port 1337.
|
47
flake.lock
47
flake.lock
|
@ -15,13 +15,37 @@
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"microvm": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"spectrum": "spectrum"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1711159783,
|
||||||
|
"narHash": "sha256-nwl2Cygq7NrV9QcebJE/T/vXv7w+zLERD7ygHz0F5g8=",
|
||||||
|
"owner": "astro",
|
||||||
|
"repo": "microvm.nix",
|
||||||
|
"rev": "d31f7c7d3194c51372134832a3a2a256773c161a",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "astro",
|
||||||
|
"repo": "microvm.nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1700612854,
|
"lastModified": 1711163522,
|
||||||
"narHash": "sha256-yrQ8osMD+vDLGFX7pcwsY/Qr5PUd6OmDMYJZzZi0+zc=",
|
"narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "19cbff58383a4ae384dea4d1d0c823d72b49d614",
|
"rev": "44d0940ea560dee511026a53f0e2e2cde489b4d4",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -34,8 +58,25 @@
|
||||||
"root": {
|
"root": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
|
"microvm": "microvm",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"spectrum": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1708358594,
|
||||||
|
"narHash": "sha256-e71YOotu2FYA67HoC/voJDTFsiPpZNRwmiQb4f94OxQ=",
|
||||||
|
"ref": "refs/heads/main",
|
||||||
|
"rev": "6d0e73864d28794cdbd26ab7b37259ab0e1e044c",
|
||||||
|
"revCount": 614,
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://spectrum-os.org/git/spectrum"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://spectrum-os.org/git/spectrum"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|
52
flake.nix
52
flake.nix
|
@ -2,9 +2,14 @@
|
||||||
inputs = {
|
inputs = {
|
||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
microvm = {
|
||||||
|
url = "github:astro/microvm.nix";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.flake-utils.follows = "flake-utils";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
outputs = { self, nixpkgs, flake-utils }:
|
outputs = { self, nixpkgs, flake-utils, microvm }:
|
||||||
let
|
let
|
||||||
out = system:
|
out = system:
|
||||||
let
|
let
|
||||||
|
@ -14,41 +19,32 @@
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells.default = (pkgs.buildFHSEnv {
|
devShells.default = pkgs.mkShell {
|
||||||
name = "s4d-env";
|
buildInputs = with pkgs; [
|
||||||
targetPkgs = pkgs: (with pkgs; [
|
|
||||||
jdk
|
jdk
|
||||||
zlib
|
zlib
|
||||||
freetype
|
|
||||||
fontconfig
|
|
||||||
maven
|
maven
|
||||||
protobuf
|
nixos-rebuild
|
||||||
]);
|
];
|
||||||
runScript = "zsh";
|
|
||||||
}).env;
|
|
||||||
|
|
||||||
packages.default = pkgs.maven.buildMavenPackage {
|
|
||||||
pname = "keycloak-lists-plugin";
|
|
||||||
version = "1.0";
|
|
||||||
|
|
||||||
src = ./plugin;
|
|
||||||
|
|
||||||
mvnHash = "sha256-UaVCt6KIjR8i3vHVp5YWqu8zzM7mftXyrv5J2jxtw6Q=";
|
|
||||||
|
|
||||||
buildPhase = ''
|
|
||||||
mvn --offline package;
|
|
||||||
'';
|
|
||||||
|
|
||||||
installPhase = ''
|
|
||||||
mkdir -p $out/share/java
|
|
||||||
install -Dm644 target/*.jar $out/share/java
|
|
||||||
'';
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
packages.default = pkgs.callPackage ./package.nix { };
|
||||||
|
packages.run = self.nixosConfigurations.test.config.microvm.declaredRunner;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
flake-utils.lib.eachDefaultSystem out // {
|
flake-utils.lib.eachDefaultSystem out // {
|
||||||
overlays.default = final: prev: {
|
nixosConfigurations.test = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
({ ... }: {
|
||||||
|
services.keycloak.plugins = [ self.packages.x86_64-linux.default ];
|
||||||
|
})
|
||||||
|
./test-nixos.nix
|
||||||
|
microvm.nixosModules.microvm
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
overlays.default = final: prev: { };
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
18
package.nix
Normal file
18
package.nix
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{ maven }:
|
||||||
|
maven.buildMavenPackage {
|
||||||
|
pname = "keycloak-lists-plugin";
|
||||||
|
version = "1.0";
|
||||||
|
|
||||||
|
src = ./plugin;
|
||||||
|
|
||||||
|
mvnHash = "sha256-UaVCt6KIjR8i3vHVp5YWqu8zzM7mftXyrv5J2jxtw6Q=";
|
||||||
|
|
||||||
|
buildPhase = ''
|
||||||
|
mvn --offline package;
|
||||||
|
'';
|
||||||
|
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out
|
||||||
|
install -Dm644 target/*.jar $out
|
||||||
|
'';
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package lix.systems.keycloak;
|
package systems.lix.keycloak;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.Response;
|
import jakarta.ws.rs.core.Response;
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
@ -25,6 +25,9 @@ public class AllowBanCheck implements Authenticator {
|
||||||
var attr = context.getUser().getFirstAttribute("githubId");
|
var attr = context.getUser().getFirstAttribute("githubId");
|
||||||
|
|
||||||
if (attr == null) {
|
if (attr == null) {
|
||||||
|
// We don't think this should be "attempted", because this must be
|
||||||
|
// a required authenticator, and we want to pass if we don't apply.
|
||||||
|
context.success();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package lix.systems.keycloak;
|
package systems.lix.keycloak;
|
||||||
|
|
||||||
import org.keycloak.Config;
|
import org.keycloak.Config;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
|
@ -1,4 +1,4 @@
|
||||||
package lix.systems.keycloak;
|
package systems.lix.keycloak;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A database of whether users are allow-listed or banned.
|
* A database of whether users are allow-listed or banned.
|
|
@ -1,4 +1,4 @@
|
||||||
package lix.systems.keycloak;
|
package systems.lix.keycloak;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
|
@ -0,0 +1 @@
|
||||||
|
systems.lix.keycloak.AllowBanCheckFactory
|
156
test-nixos.nix
Normal file
156
test-nixos.nix
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
# Server we're hosting on.
|
||||||
|
host = "identity.test.lix.systems";
|
||||||
|
|
||||||
|
# Realm used for services.
|
||||||
|
realm = "lix-project";
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
users.users.root.password = "";
|
||||||
|
services.openssh.enable = true;
|
||||||
|
services.openssh.settings.PermitRootLogin = "yes";
|
||||||
|
|
||||||
|
services.keycloak = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
hostname = host;
|
||||||
|
|
||||||
|
# Always talk through our reverse proxy.
|
||||||
|
http-port = 9091;
|
||||||
|
proxy = "edge";
|
||||||
|
};
|
||||||
|
|
||||||
|
# This will be immediately changed, so no harm in having it here.
|
||||||
|
initialAdminPassword = "Password1";
|
||||||
|
|
||||||
|
# Automatically manage our database.
|
||||||
|
database = {
|
||||||
|
createLocally = true;
|
||||||
|
# DO NOT DO THIS IN PROD
|
||||||
|
passwordFile = builtins.toFile "bad-db-password" "Password1";
|
||||||
|
};
|
||||||
|
|
||||||
|
settings = {
|
||||||
|
log-level = "INFO";
|
||||||
|
spi-authenticator-allow-ban-check-authenticator-dbpath = "/var/keycloak-allow-bans";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Postgres server for the storage backend.
|
||||||
|
services.postgresql.enable = true;
|
||||||
|
|
||||||
|
# Create a static user, so we can set up our keys beforehand.
|
||||||
|
# This overrides the dynamic user creation in the base module config.
|
||||||
|
users.users.keycloak = {
|
||||||
|
isSystemUser = true;
|
||||||
|
group = "keycloak";
|
||||||
|
};
|
||||||
|
users.groups.keycloak = { };
|
||||||
|
|
||||||
|
# Reverse proxy our data over https.
|
||||||
|
networking.firewall.allowedTCPPorts = [ 80 443 ];
|
||||||
|
services.nginx = {
|
||||||
|
enable = true;
|
||||||
|
|
||||||
|
virtualHosts = {
|
||||||
|
"${host}" = {
|
||||||
|
forceSSL = true;
|
||||||
|
sslCertificate = "/var/lib/nginx/nc-selfsigned.crt";
|
||||||
|
sslCertificateKey = "/var/lib/nginx/nc-selfsigned.key";
|
||||||
|
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:9091";
|
||||||
|
extraConfig = ''
|
||||||
|
proxy_ssl_server_name on;
|
||||||
|
proxy_pass_header Authorization;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_protocol_addr;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
|
||||||
|
proxy_busy_buffers_size 512k;
|
||||||
|
proxy_buffers 4 512k;
|
||||||
|
proxy_buffer_size 256k;
|
||||||
|
|
||||||
|
# Allow clients with Auth hardcoded to use our base path.
|
||||||
|
#
|
||||||
|
# XXX: ok so this is horrible. For some reason gerrit explodes if
|
||||||
|
# it receives a redirect when doing auth. But we need to redirect
|
||||||
|
# the browser to reuse sessions. Thus, user agent scanning.
|
||||||
|
if ($http_user_agent ~* "^Java.*$") {
|
||||||
|
rewrite ^/auth/(.*)$ /$1 last;
|
||||||
|
}
|
||||||
|
rewrite ^/auth/(.*)$ /$1 redirect;
|
||||||
|
|
||||||
|
# Hacks to make us compatible with authenticators that expect GitLab's format.
|
||||||
|
rewrite ^/realms/${realm}/protocol/openid-connect/api/v4/user$ /realms/${realm}/protocol/openid-connect/userinfo;
|
||||||
|
rewrite ^/realms/${realm}/protocol/openid-connect/oauth/authorize$ /realms/${realm}/protocol/openid-connect/auth?scope=openid%20email%20profile;
|
||||||
|
rewrite ^/realms/${realm}/protocol/openid-connect/oauth/token$ /realms/${realm}/protocol/openid-connect/token;
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
# Forward our admin address to our default realm.
|
||||||
|
locations."= /admin".extraConfig = "return 302 https://${host}/admin/lix-project/console/;";
|
||||||
|
locations."= /superadmin".extraConfig = "return 302 https://${host}/admin/master/console/;";
|
||||||
|
|
||||||
|
# Forward our root address to the account management portal.
|
||||||
|
locations."= /".extraConfig = "return 302 https://${host}/realms/${realm}/account;";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.services.cert-setup = {
|
||||||
|
wantedBy = [ "nginx.service" ];
|
||||||
|
before = [ "nginx.service" ];
|
||||||
|
serviceConfig = {
|
||||||
|
ConditionFileExists = "!/var/lib/nginx/nc-selfsigned.crt";
|
||||||
|
ExecStart = [
|
||||||
|
"${lib.getBin pkgs.openssl}/bin/openssl req -x509 -nodes -days 365 -newkey rsa:2048 -subj /CN=identity.test.lix.systems/ -keyout /var/lib/nginx/nc-selfsigned.key -out /var/lib/nginx/nc-selfsigned.crt"
|
||||||
|
"${lib.getBin pkgs.coreutils}/bin/chown nginx:nginx /var/lib/nginx/nc-selfsigned.key /var/lib/nginx/nc-selfsigned.crt"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
systemd.tmpfiles.rules = [
|
||||||
|
"d /var/lib/nginx 755 nginx nginx -"
|
||||||
|
];
|
||||||
|
|
||||||
|
microvm = {
|
||||||
|
hypervisor = "qemu";
|
||||||
|
mem = 1024;
|
||||||
|
interfaces = [{
|
||||||
|
type = "user";
|
||||||
|
id = "microvm";
|
||||||
|
mac = "02:00:00:00:00:01";
|
||||||
|
}];
|
||||||
|
|
||||||
|
forwardPorts = [
|
||||||
|
{
|
||||||
|
from = "host";
|
||||||
|
guest.port = 443;
|
||||||
|
host.port = 4043;
|
||||||
|
proto = "tcp";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
from = "host";
|
||||||
|
guest.port = 22;
|
||||||
|
host.port = 2022;
|
||||||
|
proto = "tcp";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
volumes = [{
|
||||||
|
mountPoint = "/var";
|
||||||
|
image = "var.img";
|
||||||
|
size = 256;
|
||||||
|
}];
|
||||||
|
shares = [{
|
||||||
|
proto = "9p";
|
||||||
|
tag = "ro-store";
|
||||||
|
source = "/nix/store";
|
||||||
|
mountPoint = "/nix/.ro-store";
|
||||||
|
}];
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue