mirror of
https://github.com/ClusterCockpit/cc-backend
synced 2026-06-18 01:17:29 +02:00
feat(auth): replace .env/godotenv secret handling with config-based secrets
Secrets (JWT keys, LDAP sync password, OIDC client id/secret, cross-login keys) are now configured directly in config.json under the auth section where they are used. Each secret can still be supplied via its existing environment variable, which takes precedence over the config value. The godotenv dependency, the .env file, configs/env-template.txt and the loadEnvironment() bootstrap step are removed. -init now writes the demo JWT keys into config.json instead of a .env file. Closes #283 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 3a7cb814c53f
This commit is contained in:
12
CLAUDE.md
12
CLAUDE.md
@@ -161,9 +161,15 @@ applied automatically on startup. Version tracking in `version` table.
|
|||||||
- `username`: Authentication username (optional)
|
- `username`: Authentication username (optional)
|
||||||
- `password`: Authentication password (optional)
|
- `password`: Authentication password (optional)
|
||||||
- `creds-file-path`: Path to NATS credentials file (optional)
|
- `creds-file-path`: Path to NATS credentials file (optional)
|
||||||
- **.env**: Environment variables (secrets like JWT keys)
|
- **Secrets** (JWT keys, LDAP sync password, OIDC client id/secret, cross-login
|
||||||
- Copy from `configs/env-template.txt`
|
keys): configured directly in `config.json` under the `auth` section where they
|
||||||
- NEVER commit this file
|
are used (e.g. `auth.jwts.public-key`, `auth.jwts.private-key`,
|
||||||
|
`auth.ldap.sync-password`, `auth.oidc.client-id`/`client-secret`).
|
||||||
|
- Each secret may also be supplied via its environment variable
|
||||||
|
(`JWT_PUBLIC_KEY`, `JWT_PRIVATE_KEY`, `LDAP_ADMIN_PASSWORD`, `OID_CLIENT_ID`,
|
||||||
|
`OID_CLIENT_SECRET`, `CROSS_LOGIN_JWT_PUBLIC_KEY`, `CROSS_LOGIN_JWT_HS512_KEY`).
|
||||||
|
- The environment variable takes precedence over the value in `config.json`.
|
||||||
|
- The former `.env`/godotenv mechanism has been removed.
|
||||||
- **cluster.json**: Cluster topology and metric definitions (loaded from archive or config)
|
- **cluster.json**: Cluster topology and metric definitions (loaded from archive or config)
|
||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|||||||
19
README.md
19
README.md
@@ -129,12 +129,11 @@ git clone https://github.com/ClusterCockpit/cc-backend.git
|
|||||||
cd ./cc-backend/
|
cd ./cc-backend/
|
||||||
make
|
make
|
||||||
|
|
||||||
# EDIT THE .env FILE BEFORE YOU DEPLOY (Change the secrets)!
|
|
||||||
# If authentication is disabled, it can be empty.
|
|
||||||
cp configs/env-template.txt .env
|
|
||||||
vim .env
|
|
||||||
|
|
||||||
cp configs/config.json .
|
cp configs/config.json .
|
||||||
|
# EDIT config.json BEFORE YOU DEPLOY: change the secrets under "auth.jwts"
|
||||||
|
# ("public-key"/"private-key"). Each secret can also be supplied via an
|
||||||
|
# environment variable (e.g. JWT_PUBLIC_KEY), which takes precedence over the
|
||||||
|
# value in config.json.
|
||||||
vim config.json
|
vim config.json
|
||||||
|
|
||||||
#Optional: Link an existing job archive:
|
#Optional: Link an existing job archive:
|
||||||
@@ -157,8 +156,14 @@ ln -s <your-existing-job-archive> ./var/job-archive
|
|||||||
Browser sessions are stored server-side in the SQLite database (the `sessions`
|
Browser sessions are stored server-side in the SQLite database (the `sessions`
|
||||||
table) using [`alexedwards/scs`](https://github.com/alexedwards/scs); only an
|
table) using [`alexedwards/scs`](https://github.com/alexedwards/scs); only an
|
||||||
opaque random token is kept in the session cookie. No cookie-signing secret is
|
opaque random token is kept in the session cookie. No cookie-signing secret is
|
||||||
required, so the former `SESSION_KEY` environment variable is no longer used and
|
required, so the former `SESSION_KEY` environment variable is no longer used.
|
||||||
can be removed from your `.env`.
|
|
||||||
|
Secrets (JWT keys, LDAP sync password, OIDC client id/secret, cross-login keys)
|
||||||
|
are configured directly in `config.json` under the `auth` section. Each secret
|
||||||
|
may also be supplied via its environment variable (e.g. `JWT_PUBLIC_KEY`,
|
||||||
|
`JWT_PRIVATE_KEY`, `LDAP_ADMIN_PASSWORD`, `OID_CLIENT_ID`, `OID_CLIENT_SECRET`,
|
||||||
|
`CROSS_LOGIN_JWT_PUBLIC_KEY`, `CROSS_LOGIN_JWT_HS512_KEY`); the environment
|
||||||
|
variable takes precedence when set. The previous `.env` file is no longer used.
|
||||||
|
|
||||||
The session cookie's `Secure` flag is set automatically when cc-backend serves
|
The session cookie's `Secure` flag is set automatically when cc-backend serves
|
||||||
HTTPS itself (i.e. `https-cert-file` and `https-key-file` are configured in
|
HTTPS itself (i.e. `https-cert-file` and `https-key-file` are configured in
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func cliInit() {
|
func cliInit() {
|
||||||
flag.BoolVar(&flagInit, "init", false, "Setup var directory, initialize sqlite database file, config.json and .env")
|
flag.BoolVar(&flagInit, "init", false, "Setup var directory, initialize sqlite database file and config.json")
|
||||||
flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize the 'job', 'tag', and 'jobtag' tables (all running jobs will be lost!)")
|
flag.BoolVar(&flagReinitDB, "init-db", false, "Go through job-archive and re-initialize the 'job', 'tag', and 'jobtag' tables (all running jobs will be lost!)")
|
||||||
flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the 'hpc_user' table with ldap")
|
flag.BoolVar(&flagSyncLDAP, "sync-ldap", false, "Sync the 'hpc_user' table with ldap")
|
||||||
flag.BoolVar(&flagServer, "server", false, "Start a server, continues listening on port after initialization and argument handling")
|
flag.BoolVar(&flagServer, "server", false, "Start a server, continues listening on port after initialization and argument handling")
|
||||||
|
|||||||
@@ -18,13 +18,6 @@ import (
|
|||||||
"github.com/ClusterCockpit/cc-lib/v2/util"
|
"github.com/ClusterCockpit/cc-lib/v2/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
const envString = `
|
|
||||||
# Base64 encoded Ed25519 keys (DO NOT USE THESE TWO IN PRODUCTION!)
|
|
||||||
# You can generate your own keypair using the gen-keypair tool
|
|
||||||
JWT_PUBLIC_KEY="kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
|
|
||||||
JWT_PRIVATE_KEY="dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
|
|
||||||
`
|
|
||||||
|
|
||||||
const configString = `
|
const configString = `
|
||||||
{
|
{
|
||||||
"main": {
|
"main": {
|
||||||
@@ -54,7 +47,9 @@ const configString = `
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"jwts": {
|
"jwts": {
|
||||||
"max-age": "2000h"
|
"max-age": "2000h",
|
||||||
|
"public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0=",
|
||||||
|
"private-key": "dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,10 +64,6 @@ func initEnv() {
|
|||||||
cclog.Abortf("Could not write default ./config.json with permissions '0o666'. Application initialization failed, exited.\nError: %s\n", err.Error())
|
cclog.Abortf("Could not write default ./config.json with permissions '0o666'. Application initialization failed, exited.\nError: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.WriteFile(".env", []byte(envString), 0o666); err != nil {
|
|
||||||
cclog.Abortf("Could not write default ./.env file with permissions '0o666'. Application initialization failed, exited.\nError: %s\n", err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Mkdir("var", 0o777); err != nil {
|
if err := os.Mkdir("var", 0o777); err != nil {
|
||||||
cclog.Abortf("Could not create default ./var folder with permissions '0o777'. Application initialization failed, exited.\nError: %s\n", err.Error())
|
cclog.Abortf("Could not create default ./var folder with permissions '0o777'. Application initialization failed, exited.\nError: %s\n", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ import (
|
|||||||
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
||||||
"github.com/ClusterCockpit/cc-lib/v2/util"
|
"github.com/ClusterCockpit/cc-lib/v2/util"
|
||||||
"github.com/google/gops/agent"
|
"github.com/google/gops/agent"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
@@ -89,13 +88,6 @@ func initGops() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadEnvironment() error {
|
|
||||||
if err := godotenv.Load(); err != nil {
|
|
||||||
return fmt.Errorf("loading .env file: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func initConfiguration() error {
|
func initConfiguration() error {
|
||||||
ccconf.Init(flagConfigFile)
|
ccconf.Init(flagConfigFile)
|
||||||
|
|
||||||
@@ -224,7 +216,14 @@ func checkDefaultSecurityKeys() {
|
|||||||
// Default JWT public key from init.go
|
// Default JWT public key from init.go
|
||||||
defaultJWTPublic := "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
|
defaultJWTPublic := "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
|
||||||
|
|
||||||
if os.Getenv("JWT_PUBLIC_KEY") == defaultJWTPublic {
|
// Resolve the public key the same way the authenticators do: environment
|
||||||
|
// variable takes precedence over the value configured in config.json.
|
||||||
|
pubKey := os.Getenv("JWT_PUBLIC_KEY")
|
||||||
|
if pubKey == "" && auth.Keys.JwtConfig != nil {
|
||||||
|
pubKey = auth.Keys.JwtConfig.PublicKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if pubKey == defaultJWTPublic {
|
||||||
cclog.Warn("Using default JWT keys - not recommended for production environments")
|
cclog.Warn("Using default JWT keys - not recommended for production environments")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -495,7 +494,7 @@ func run() error {
|
|||||||
if flagInit {
|
if flagInit {
|
||||||
initEnv()
|
initEnv()
|
||||||
cclog.Exit("Successfully setup environment!\n" +
|
cclog.Exit("Successfully setup environment!\n" +
|
||||||
"Please review config.json and .env and adjust it to your needs.\n" +
|
"Please review config.json and adjust it to your needs.\n" +
|
||||||
"Add your job-archive at ./var/job-archive.")
|
"Add your job-archive at ./var/job-archive.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,17 +504,12 @@ func run() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize subsystems in dependency order:
|
// Initialize subsystems in dependency order:
|
||||||
// 1. Load environment variables from .env file (contains sensitive configuration)
|
// 1. Load configuration from config.json (secrets live in config; individual
|
||||||
// 2. Load configuration from config.json (may reference environment variables)
|
// secrets may be overridden via environment variables)
|
||||||
// 3. Handle database migration commands if requested
|
// 2. Handle database migration commands if requested
|
||||||
// 4. Initialize database connection (requires config for connection string)
|
// 3. Initialize database connection (requires config for connection string)
|
||||||
// 5. Handle user commands if requested (requires database and authentication config)
|
// 4. Handle user commands if requested (requires database and authentication config)
|
||||||
// 6. Initialize subsystems like archive and metrics (require config and database)
|
// 5. Initialize subsystems like archive and metrics (require config and database)
|
||||||
|
|
||||||
// Load environment and configuration
|
|
||||||
if err := loadEnvironment(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := initConfiguration(); err != nil {
|
if err := initConfiguration(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"jwts": {
|
"jwts": {
|
||||||
"max-age": "2000h"
|
"max-age": "2000h",
|
||||||
|
"public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0=",
|
||||||
|
"private-key": "dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"metric-store-external": [
|
"metric-store-external": [
|
||||||
|
|||||||
@@ -30,7 +30,9 @@
|
|||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"jwts": {
|
"jwts": {
|
||||||
"max-age": "2000h"
|
"max-age": "2000h",
|
||||||
|
"public-key": "kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0=",
|
||||||
|
"private-key": "dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"cron": {
|
"cron": {
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
# Base64 encoded Ed25519 keys (DO NOT USE THESE TWO IN PRODUCTION!)
|
|
||||||
# You can generate your own keypair using `go run tools/gen-keypair/main.go`
|
|
||||||
JWT_PUBLIC_KEY="kzfYrYy+TzpanWZHJ5qSdMj5uKUWgq74BWhQG6copP0="
|
|
||||||
JWT_PRIVATE_KEY="dtPC/6dWJFKZK7KZ78CvWuynylOmjBFyMsUWArwmodOTN9itjL5POlqdZkcnmpJ0yPm4pRaCrvgFaFAbpyik/Q=="
|
|
||||||
|
|
||||||
# Base64 encoded Ed25519 public key for accepting externally generated JWTs
|
|
||||||
# Keys in PEM format can be converted, see `tools/convert-pem-pubkey/Readme.md`
|
|
||||||
CROSS_LOGIN_JWT_PUBLIC_KEY=""
|
|
||||||
|
|
||||||
# Password for the ldap server (optional)
|
|
||||||
LDAP_ADMIN_PASSWORD="mashup"
|
|
||||||
1
go.mod
1
go.mod
@@ -28,7 +28,6 @@ require (
|
|||||||
github.com/golang-migrate/migrate/v4 v4.19.1
|
github.com/golang-migrate/migrate/v4 v4.19.1
|
||||||
github.com/google/gops v0.3.29
|
github.com/google/gops v0.3.29
|
||||||
github.com/jmoiron/sqlx v1.4.0
|
github.com/jmoiron/sqlx v1.4.0
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
github.com/mattn/go-sqlite3 v1.14.44
|
github.com/mattn/go-sqlite3 v1.14.44
|
||||||
github.com/parquet-go/parquet-go v0.30.1
|
github.com/parquet-go/parquet-go v0.30.1
|
||||||
github.com/qustavo/sqlhooks/v2 v2.1.0
|
github.com/qustavo/sqlhooks/v2 v2.1.0
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -191,8 +191,6 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
|
|||||||
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
|
||||||
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o=
|
||||||
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
|
||||||
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
|
||||||
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ specified, the application will call
|
|||||||
config file and binding to a TCP port (so it can take a privileged port), but
|
config file and binding to a TCP port (so it can take a privileged port), but
|
||||||
before it starts accepting any connections. This is good for security, but also
|
before it starts accepting any connections. This is good for security, but also
|
||||||
means that the `var/` directory must be readable and writeable by this user.
|
means that the `var/` directory must be readable and writeable by this user.
|
||||||
The `.env` and `config.json` files may contain secrets and should not be
|
The `config.json` file may contain secrets and should not be readable by this
|
||||||
readable by this user. If these files are changed, the server must be restarted.
|
user. If this file is changed, the server must be restarted.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 1. Clone this repository somewhere in your home
|
# 1. Clone this repository somewhere in your home
|
||||||
@@ -25,11 +25,9 @@ make
|
|||||||
sudo mkdir -p /opt/monitoring/cc-backend/
|
sudo mkdir -p /opt/monitoring/cc-backend/
|
||||||
cp ./cc-backend /opt/monitoring/cc-backend/
|
cp ./cc-backend /opt/monitoring/cc-backend/
|
||||||
|
|
||||||
# 3. Modify the `./config.json` and env-template.txt file from the configs directory to your liking and put it in the target directory
|
# 3. Modify the `./config.json` file from the configs directory to your liking and put it in the target directory
|
||||||
cp ./configs/config.json /opt/monitoring/config.json
|
cp ./configs/config.json /opt/monitoring/config.json
|
||||||
cp ./configs/env-template.txt /opt/monitoring/.env
|
vim /opt/monitoring/config.json # do your thing (including the secrets under "auth")...
|
||||||
vim /opt/monitoring/config.json # do your thing...
|
|
||||||
vim /opt/monitoring/.env # do your thing...
|
|
||||||
|
|
||||||
# 4. (Optional) Customization: Add your versions of the login view, legal texts, and logo image.
|
# 4. (Optional) Customization: Add your versions of the login view, legal texts, and logo image.
|
||||||
# You may use the templates in `./web/templates` as blueprint. Every overwrite separate.
|
# You may use the templates in `./web/templates` as blueprint. Every overwrite separate.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -115,6 +116,18 @@ type AuthConfig struct {
|
|||||||
// Keys holds the global authentication configuration
|
// Keys holds the global authentication configuration
|
||||||
var Keys AuthConfig
|
var Keys AuthConfig
|
||||||
|
|
||||||
|
// secretFromEnv resolves a secret from the environment or config. The
|
||||||
|
// environment variable takes precedence when set and non-empty; otherwise the
|
||||||
|
// value configured in config.json is used. This lets deployments inject secrets
|
||||||
|
// via the environment (or a secret manager) while keeping config.json
|
||||||
|
// self-contained for simple setups.
|
||||||
|
func secretFromEnv(envVar, configValue string) string {
|
||||||
|
if v := os.Getenv(envVar); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return configValue
|
||||||
|
}
|
||||||
|
|
||||||
// Authentication manages all authentication methods and session handling
|
// Authentication manages all authentication methods and session handling
|
||||||
type Authentication struct {
|
type Authentication struct {
|
||||||
sessionManager *scs.SessionManager
|
sessionManager *scs.SessionManager
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,6 +38,23 @@ type JWTAuthConfig struct {
|
|||||||
|
|
||||||
// Should an existent user be updated in the DB based on the information in the token
|
// Should an existent user be updated in the DB based on the information in the token
|
||||||
UpdateUserOnLogin bool `json:"update-user-on-login"`
|
UpdateUserOnLogin bool `json:"update-user-on-login"`
|
||||||
|
|
||||||
|
// Base64 encoded Ed25519 public key used to validate JWTs.
|
||||||
|
// Overridden by the JWT_PUBLIC_KEY environment variable when set.
|
||||||
|
PublicKey string `json:"public-key"`
|
||||||
|
|
||||||
|
// Base64 encoded Ed25519 private key used to sign JWTs.
|
||||||
|
// Overridden by the JWT_PRIVATE_KEY environment variable when set.
|
||||||
|
PrivateKey string `json:"private-key"`
|
||||||
|
|
||||||
|
// Base64 encoded Ed25519 public key for accepting externally generated JWTs.
|
||||||
|
// Overridden by the CROSS_LOGIN_JWT_PUBLIC_KEY environment variable when set.
|
||||||
|
CrossLoginPublicKey string `json:"cross-login-public-key"`
|
||||||
|
|
||||||
|
// Base64 encoded HMAC (HS256/HS512) key for accepting externally generated
|
||||||
|
// session login tokens.
|
||||||
|
// Overridden by the CROSS_LOGIN_JWT_HS512_KEY environment variable when set.
|
||||||
|
CrossLoginHS512Key string `json:"cross-login-hs512-key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type JWTAuthenticator struct {
|
type JWTAuthenticator struct {
|
||||||
@@ -47,9 +63,10 @@ type JWTAuthenticator struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (ja *JWTAuthenticator) Init() error {
|
func (ja *JWTAuthenticator) Init() error {
|
||||||
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
|
pubKey := secretFromEnv("JWT_PUBLIC_KEY", Keys.JwtConfig.PublicKey)
|
||||||
|
privKey := secretFromEnv("JWT_PRIVATE_KEY", Keys.JwtConfig.PrivateKey)
|
||||||
if pubKey == "" || privKey == "" {
|
if pubKey == "" || privKey == "" {
|
||||||
cclog.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
|
cclog.Warn("JWT public/private key not configured ('public-key'/'private-key' in config or 'JWT_PUBLIC_KEY'/'JWT_PRIVATE_KEY' env): token based authentication will not work")
|
||||||
} else {
|
} else {
|
||||||
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,7 +138,7 @@ func (ja *JWTAuthenticator) AuthViaJWT(
|
|||||||
// ProvideJWT generates a new JWT that can be used for authentication
|
// ProvideJWT generates a new JWT that can be used for authentication
|
||||||
func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
|
func (ja *JWTAuthenticator) ProvideJWT(user *schema.User) (string, error) {
|
||||||
if ja.privateKey == nil {
|
if ja.privateKey == nil {
|
||||||
return "", errors.New("environment variable 'JWT_PRIVATE_KEY' not set")
|
return "", errors.New("JWT private key not configured ('private-key' in config or 'JWT_PRIVATE_KEY' env)")
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
||||||
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
"github.com/ClusterCockpit/cc-lib/v2/schema"
|
||||||
@@ -27,10 +26,11 @@ type JWTCookieSessionAuthenticator struct {
|
|||||||
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
|
var _ Authenticator = (*JWTCookieSessionAuthenticator)(nil)
|
||||||
|
|
||||||
func (ja *JWTCookieSessionAuthenticator) Init() error {
|
func (ja *JWTCookieSessionAuthenticator) Init() error {
|
||||||
pubKey, privKey := os.Getenv("JWT_PUBLIC_KEY"), os.Getenv("JWT_PRIVATE_KEY")
|
pubKey := secretFromEnv("JWT_PUBLIC_KEY", Keys.JwtConfig.PublicKey)
|
||||||
|
privKey := secretFromEnv("JWT_PRIVATE_KEY", Keys.JwtConfig.PrivateKey)
|
||||||
if pubKey == "" || privKey == "" {
|
if pubKey == "" || privKey == "" {
|
||||||
cclog.Warn("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
|
cclog.Warn("JWT public/private key not configured ('public-key'/'private-key' in config or 'JWT_PUBLIC_KEY'/'JWT_PRIVATE_KEY' env): token based authentication will not work")
|
||||||
return errors.New("environment variables 'JWT_PUBLIC_KEY' or 'JWT_PRIVATE_KEY' not set (token based authentication will not work)")
|
return errors.New("JWT public/private key not configured: token based authentication will not work")
|
||||||
} else {
|
} else {
|
||||||
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -47,8 +47,8 @@ func (ja *JWTCookieSessionAuthenticator) Init() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Look for external public keys
|
// Look for external public keys
|
||||||
pubKeyCrossLogin, keyFound := os.LookupEnv("CROSS_LOGIN_JWT_PUBLIC_KEY")
|
pubKeyCrossLogin := secretFromEnv("CROSS_LOGIN_JWT_PUBLIC_KEY", Keys.JwtConfig.CrossLoginPublicKey)
|
||||||
if keyFound && pubKeyCrossLogin != "" {
|
if pubKeyCrossLogin != "" {
|
||||||
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
|
bytes, err := base64.StdEncoding.DecodeString(pubKeyCrossLogin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Warn("Could not decode cross login JWT public key")
|
cclog.Warn("Could not decode cross login JWT public key")
|
||||||
@@ -57,8 +57,8 @@ func (ja *JWTCookieSessionAuthenticator) Init() error {
|
|||||||
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
|
ja.publicKeyCrossLogin = ed25519.PublicKey(bytes)
|
||||||
} else {
|
} else {
|
||||||
ja.publicKeyCrossLogin = nil
|
ja.publicKeyCrossLogin = nil
|
||||||
cclog.Debug("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
|
cclog.Debug("cross login JWT public key not configured ('cross-login-public-key' in config or 'CROSS_LOGIN_JWT_PUBLIC_KEY' env): cross login token based authentication will not work")
|
||||||
return errors.New("environment variable 'CROSS_LOGIN_JWT_PUBLIC_KEY' not set (cross login token based authentication will not work)")
|
return errors.New("cross login JWT public key not configured: cross login token based authentication will not work")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn if other necessary settings are not configured
|
// Warn if other necessary settings are not configured
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
cclog "github.com/ClusterCockpit/cc-lib/v2/ccLogger"
|
||||||
@@ -25,12 +24,12 @@ type JWTSessionAuthenticator struct {
|
|||||||
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
|
var _ Authenticator = (*JWTSessionAuthenticator)(nil)
|
||||||
|
|
||||||
func (ja *JWTSessionAuthenticator) Init() error {
|
func (ja *JWTSessionAuthenticator) Init() error {
|
||||||
pubKey := os.Getenv("CROSS_LOGIN_JWT_HS512_KEY")
|
pubKey := secretFromEnv("CROSS_LOGIN_JWT_HS512_KEY", Keys.JwtConfig.CrossLoginHS512Key)
|
||||||
if pubKey == "" {
|
if pubKey == "" {
|
||||||
// Without a configured key the HMAC verification below would run against
|
// Without a configured key the HMAC verification below would run against
|
||||||
// an empty key, which lets anyone forge a valid token. Refuse to register
|
// an empty key, which lets anyone forge a valid token. Refuse to register
|
||||||
// the authenticator in that case so JWT session login is simply disabled.
|
// the authenticator in that case so JWT session login is simply disabled.
|
||||||
return errors.New("CROSS_LOGIN_JWT_HS512_KEY not set: JWT session login disabled")
|
return errors.New("cross login HS512 key not configured ('cross-login-hs512-key' in config or 'CROSS_LOGIN_JWT_HS512_KEY' env): JWT session login disabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
bytes, err := base64.StdEncoding.DecodeString(pubKey)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -33,6 +32,10 @@ type LdapConfig struct {
|
|||||||
// Should a non-existent user be added to the DB if user exists in ldap directory
|
// Should a non-existent user be added to the DB if user exists in ldap directory
|
||||||
SyncUserOnLogin bool `json:"sync-user-on-login"`
|
SyncUserOnLogin bool `json:"sync-user-on-login"`
|
||||||
UpdateUserOnLogin bool `json:"update-user-on-login"`
|
UpdateUserOnLogin bool `json:"update-user-on-login"`
|
||||||
|
|
||||||
|
// Password for the LDAP admin account used for syncing (optional).
|
||||||
|
// Overridden by the LDAP_ADMIN_PASSWORD environment variable when set.
|
||||||
|
SyncPassword string `json:"sync-password"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type LdapAuthenticator struct {
|
type LdapAuthenticator struct {
|
||||||
@@ -44,9 +47,9 @@ type LdapAuthenticator struct {
|
|||||||
var _ Authenticator = (*LdapAuthenticator)(nil)
|
var _ Authenticator = (*LdapAuthenticator)(nil)
|
||||||
|
|
||||||
func (la *LdapAuthenticator) Init() error {
|
func (la *LdapAuthenticator) Init() error {
|
||||||
la.syncPassword = os.Getenv("LDAP_ADMIN_PASSWORD")
|
la.syncPassword = secretFromEnv("LDAP_ADMIN_PASSWORD", Keys.LdapConfig.SyncPassword)
|
||||||
if la.syncPassword == "" {
|
if la.syncPassword == "" {
|
||||||
cclog.Warn("environment variable 'LDAP_ADMIN_PASSWORD' not set (ldap sync will not work)")
|
cclog.Warn("LDAP admin password not configured ('sync-password' in config or 'LDAP_ADMIN_PASSWORD' env): ldap sync will not work")
|
||||||
}
|
}
|
||||||
|
|
||||||
if Keys.LdapConfig.UserAttr != "" {
|
if Keys.LdapConfig.UserAttr != "" {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
"github.com/ClusterCockpit/cc-backend/internal/repository"
|
||||||
@@ -27,6 +26,14 @@ type OpenIDConfig struct {
|
|||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
SyncUserOnLogin bool `json:"sync-user-on-login"`
|
SyncUserOnLogin bool `json:"sync-user-on-login"`
|
||||||
UpdateUserOnLogin bool `json:"update-user-on-login"`
|
UpdateUserOnLogin bool `json:"update-user-on-login"`
|
||||||
|
|
||||||
|
// OAuth2 client ID for the OIDC provider.
|
||||||
|
// Overridden by the OID_CLIENT_ID environment variable when set.
|
||||||
|
ClientID string `json:"client-id"`
|
||||||
|
|
||||||
|
// OAuth2 client secret for the OIDC provider.
|
||||||
|
// Overridden by the OID_CLIENT_SECRET environment variable when set.
|
||||||
|
ClientSecret string `json:"client-secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type OIDC struct {
|
type OIDC struct {
|
||||||
@@ -66,13 +73,13 @@ func NewOIDC(a *Authentication) *OIDC {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
cclog.Fatal(err)
|
cclog.Fatal(err)
|
||||||
}
|
}
|
||||||
clientID := os.Getenv("OID_CLIENT_ID")
|
clientID := secretFromEnv("OID_CLIENT_ID", Keys.OpenIDConfig.ClientID)
|
||||||
if clientID == "" {
|
if clientID == "" {
|
||||||
cclog.Warn("environment variable 'OID_CLIENT_ID' not set (Open ID connect auth will not work)")
|
cclog.Warn("OIDC client ID not configured ('client-id' in config or 'OID_CLIENT_ID' env): Open ID connect auth will not work")
|
||||||
}
|
}
|
||||||
clientSecret := os.Getenv("OID_CLIENT_SECRET")
|
clientSecret := secretFromEnv("OID_CLIENT_SECRET", Keys.OpenIDConfig.ClientSecret)
|
||||||
if clientSecret == "" {
|
if clientSecret == "" {
|
||||||
cclog.Warn("environment variable 'OID_CLIENT_SECRET' not set (Open ID connect auth will not work)")
|
cclog.Warn("OIDC client secret not configured ('client-secret' in config or 'OID_CLIENT_SECRET' env): Open ID connect auth will not work")
|
||||||
}
|
}
|
||||||
|
|
||||||
client := &oauth2.Config{
|
client := &oauth2.Config{
|
||||||
|
|||||||
@@ -34,6 +34,22 @@ var configSchema = `
|
|||||||
"update-user-on-login": {
|
"update-user-on-login": {
|
||||||
"description": "Should an existent user attributes in the DB be updated at login attempt with values provided in JWT.",
|
"description": "Should an existent user attributes in the DB be updated at login attempt with values provided in JWT.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"public-key": {
|
||||||
|
"description": "Base64 encoded Ed25519 public key used to validate JWTs. Overridden by the JWT_PUBLIC_KEY environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"private-key": {
|
||||||
|
"description": "Base64 encoded Ed25519 private key used to sign JWTs. Overridden by the JWT_PRIVATE_KEY environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cross-login-public-key": {
|
||||||
|
"description": "Base64 encoded Ed25519 public key for accepting externally generated JWTs. Overridden by the CROSS_LOGIN_JWT_PUBLIC_KEY environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"cross-login-hs512-key": {
|
||||||
|
"description": "Base64 encoded HMAC (HS256/HS512) key for accepting externally generated session login tokens. Overridden by the CROSS_LOGIN_JWT_HS512_KEY environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["max-age"]
|
"required": ["max-age"]
|
||||||
@@ -52,6 +68,14 @@ var configSchema = `
|
|||||||
"update-user-on-login": {
|
"update-user-on-login": {
|
||||||
"description": "Should an existent user attributes in the DB be updated at login attempt with values provided.",
|
"description": "Should an existent user attributes in the DB be updated at login attempt with values provided.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"client-id": {
|
||||||
|
"description": "OAuth2 client ID for the OIDC provider. Overridden by the OID_CLIENT_ID environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"client-secret": {
|
||||||
|
"description": "OAuth2 client secret for the OIDC provider. Overridden by the OID_CLIENT_SECRET environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["provider"]
|
"required": ["provider"]
|
||||||
@@ -103,6 +127,10 @@ var configSchema = `
|
|||||||
"update-user-on-login": {
|
"update-user-on-login": {
|
||||||
"description": "Should an existent user attributes in the DB be updated at login attempt with values from LDAP.",
|
"description": "Should an existent user attributes in the DB be updated at login attempt with values from LDAP.",
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sync-password": {
|
||||||
|
"description": "Password for the LDAP admin account used for syncing. Overridden by the LDAP_ADMIN_PASSWORD environment variable when set.",
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["url", "user-base", "search-dn", "user-bind", "user-filter"]
|
"required": ["url", "user-base", "search-dn", "user-bind", "user-filter"]
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ type ProgramConfig struct {
|
|||||||
|
|
||||||
APISubjects *NATSConfig `json:"api-subjects"`
|
APISubjects *NATSConfig `json:"api-subjects"`
|
||||||
|
|
||||||
// Drop root permissions once .env was read and the port was taken.
|
// Drop root permissions once the config was read and the port was taken.
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
Group string `json:"group"`
|
Group string `json:"group"`
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,11 @@ var configSchema = `
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"user": {
|
"user": {
|
||||||
"description": "Drop root permissions once .env was read and the port was taken. Only applicable if using privileged port.",
|
"description": "Drop root permissions once the config was read and the port was taken. Only applicable if using privileged port.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"group": {
|
"group": {
|
||||||
"description": "Drop root permissions once .env was read and the port was taken. Only applicable if using privileged port.",
|
"description": "Drop root permissions once the config was read and the port was taken. Only applicable if using privileged port.",
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"disable-authentication": {
|
"disable-authentication": {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ MCowBQYDK2VwAyEA+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc=
|
|||||||
Unfortunately, ClusterCockpit does not handle this format (yet). You can use this tool to convert the public PEM key into a representation for CC:
|
Unfortunately, ClusterCockpit does not handle this format (yet). You can use this tool to convert the public PEM key into a representation for CC:
|
||||||
|
|
||||||
```
|
```
|
||||||
CROSS_LOGIN_JWT_PUBLIC_KEY="+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc="
|
cross-login-public-key: "+51iXX8BdLFocrppRxIw52xCOf8xFSH/eNilN5IHVGc="
|
||||||
```
|
```
|
||||||
|
|
||||||
Instructions
|
Instructions
|
||||||
@@ -19,7 +19,9 @@ Instructions
|
|||||||
- `cd tools/convert-pem-pubkey/`
|
- `cd tools/convert-pem-pubkey/`
|
||||||
- Insert your public ed25519 PEM key into `dummy.pub`
|
- Insert your public ed25519 PEM key into `dummy.pub`
|
||||||
- `go run . dummy.pub`
|
- `go run . dummy.pub`
|
||||||
- Copy the result into ClusterCockpit's `.env`
|
- Set the result as `cross-login-public-key` under `auth.jwts` in ClusterCockpit's
|
||||||
|
`config.json` (or supply it via the `CROSS_LOGIN_JWT_PUBLIC_KEY` environment
|
||||||
|
variable, which takes precedence)
|
||||||
- (Re)start ClusterCockpit backend
|
- (Re)start ClusterCockpit backend
|
||||||
|
|
||||||
Now CC can validate generated JWTs from the external provider.
|
Now CC can validate generated JWTs from the external provider.
|
||||||
|
|||||||
@@ -44,8 +44,11 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Print the value for use as auth.jwts.cross-login-public-key in config.json.
|
||||||
|
// It may alternatively be supplied via the CROSS_LOGIN_JWT_PUBLIC_KEY
|
||||||
|
// environment variable, which takes precedence.
|
||||||
fmt.Fprintf(os.Stdout,
|
fmt.Fprintf(os.Stdout,
|
||||||
"CROSS_LOGIN_JWT_PUBLIC_KEY=%#v\n",
|
"cross-login-public-key: %#v\n",
|
||||||
base64.StdEncoding.EncodeToString(pubkey))
|
base64.StdEncoding.EncodeToString(pubkey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user