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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"locked": {
|
||||
"lastModified": 1700612854,
|
||||
"narHash": "sha256-yrQ8osMD+vDLGFX7pcwsY/Qr5PUd6OmDMYJZzZi0+zc=",
|
||||
"lastModified": 1711163522,
|
||||
"narHash": "sha256-YN/Ciidm+A0fmJPWlHBGvVkcarYWSC+s3NTPk/P+q3c=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "19cbff58383a4ae384dea4d1d0c823d72b49d614",
|
||||
"rev": "44d0940ea560dee511026a53f0e2e2cde489b4d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -34,8 +58,25 @@
|
|||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"microvm": "microvm",
|
||||
"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",
|
||||
|
|
52
flake.nix
52
flake.nix
|
@ -2,9 +2,14 @@
|
|||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
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
|
||||
out = system:
|
||||
let
|
||||
|
@ -14,41 +19,32 @@
|
|||
};
|
||||
in
|
||||
{
|
||||
devShells.default = (pkgs.buildFHSEnv {
|
||||
name = "s4d-env";
|
||||
targetPkgs = pkgs: (with pkgs; [
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
jdk
|
||||
zlib
|
||||
freetype
|
||||
fontconfig
|
||||
maven
|
||||
protobuf
|
||||
]);
|
||||
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
|
||||
'';
|
||||
nixos-rebuild
|
||||
];
|
||||
};
|
||||
|
||||
packages.default = pkgs.callPackage ./package.nix { };
|
||||
packages.run = self.nixosConfigurations.test.config.microvm.declaredRunner;
|
||||
};
|
||||
in
|
||||
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 org.keycloak.authentication.AuthenticationFlowContext;
|
||||
|
@ -25,6 +25,9 @@ public class AllowBanCheck implements Authenticator {
|
|||
var attr = context.getUser().getFirstAttribute("githubId");
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package lix.systems.keycloak;
|
||||
package systems.lix.keycloak;
|
||||
|
||||
import org.keycloak.Config;
|
||||
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.
|
|
@ -1,4 +1,4 @@
|
|||
package lix.systems.keycloak;
|
||||
package systems.lix.keycloak;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
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