diff --git a/.vscode/launch.json b/.vscode/launch.json index 8f364f5e..61edc1d8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -15,7 +15,7 @@ "PYTHONPATH": "${workspaceFolder}/src/common", "FLASK_APP": "tasks:app", "FLASK_DEBUG": "1", - "F7T_REALM_RSA_TYPE":"RS256", + "F7T_AUTH_ALGORITHMS":"RS256", "F7T_LOG_PATH":"${workspaceFolder}/logs/", "F7T_PERSIST_HOST":"localhost", "F7T_PERSIST_PORT":"6379", @@ -23,7 +23,7 @@ "F7T_TASKS_PORT":"5003", "F7T_COMPUTE_TASK_EXP_TIME":"86400", "F7T_STORAGE_TASK_EXP_TIME":"2678400", - "F7T_REALM_RSA_PUBLIC_KEY":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB" + "F7T_AUTH_PUBLIC_KEYS":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB" }, "args": [ @@ -46,7 +46,7 @@ "F7T_SYSTEMS_PUBLIC":"cluster;cluster", "F7T_STATUS_SERVICES":"certificator;utilities;compute;tasks;storage;reservations", "F7T_STATUS_SYSTEMS":"192.168.220.12:22;192.168.220.12:22", - "F7T_REALM_RSA_TYPE":"RS256", + "F7T_AUTH_ALGORITHMS":"RS256", "F7T_LOG_PATH":"${workspaceFolder}/logs/", "F7T_PERSISTENCE_IP":"localhost", "F7T_PERSIST_PORT":"6379", @@ -54,7 +54,7 @@ "F7T_TASKS_PORT":"5003", "F7T_COMPUTE_TASK_EXP_TIME":"86400", "F7T_STORAGE_TASK_EXP_TIME":"2678400", - "F7T_REALM_RSA_PUBLIC_KEY":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB" + "F7T_AUTH_PUBLIC_KEYS":"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB" }, "args": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index d195ffc3..1b8c61a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.16.1] + +### Added + +- Support for multiple JWT signature algorithms + +### Changed + +- Variable `F7T_REALM_RSA_PUBLIC_KEYS` changed to `F7T_AUTH_PUBLIC_KEYS` +- Variable `F7T_REALM_RSA_TYPE_` changed to `F7T_AUTH_ALGORITHMS` + + +### Fixed + ## [1.16.0] ### Added diff --git a/deploy/demo/common/common.env b/deploy/demo/common/common.env index 62d47f55..06647000 100644 --- a/deploy/demo/common/common.env +++ b/deploy/demo/common/common.env @@ -5,16 +5,16 @@ # Authorization: JWT token as generated by Keycloak: {"Authorization:", "Bearer fjfk..."} F7T_AUTH_HEADER_NAME=Authorization # If F7T_AUTH_HEADER_NAME = Authorization, it can also check REALM_RSA_PUBLIC_KEY: RSA key from KeyCloak Realm which signs token. -# F7T_REALM_RSA_PUBLIC_KEY="MII....QAB" +# F7T_AUTH_PUBLIC_KEYS="MII....QAB" # use 1 line without headers ("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----") -F7T_REALM_RSA_PUBLIC_KEY='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB' +F7T_AUTH_PUBLIC_KEYS='MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB' #kid: "fVc6h439Xv...." F7T_AUTH_TOKEN_ISSUER='http://localhost:8080/auth/realms/kcrealm' # specify Audience established by Keycloak, leave empty to skip verification F7T_AUTH_TOKEN_AUD='' # Keycloak scope for clients: F7T_AUTH_REQUIRED_SCOPE='firecrest' -F7T_REALM_RSA_TYPE=RS256 +F7T_AUTH_ALGORITHMS=RS256 # AUTHENTICATION ROLE for FirecREST Service Accounts F7T_AUTH_ROLE='firecrest-sa' # DEBUG FLAG diff --git a/deploy/demo/source/kong/update_kong_config.sh b/deploy/demo/source/kong/update_kong_config.sh index e4659ef4..7bbf9b59 100644 --- a/deploy/demo/source/kong/update_kong_config.sh +++ b/deploy/demo/source/kong/update_kong_config.sh @@ -19,7 +19,7 @@ echo "F7T_HTTP_SCHEMA: $F7T_HTTP_SCHEMA" # use '#' to separate string because '/' and ':' are valid on URLs sed -e 's#F7T_AUTH_TOKEN_ISSUER#'${F7T_AUTH_TOKEN_ISSUER}'#' \ - -e 's#F7T_REALM_RSA_PUBLIC_KEY#'${F7T_REALM_RSA_PUBLIC_KEY}'#' \ + -e 's#F7T_AUTH_PUBLIC_KEYS#'${F7T_AUTH_PUBLIC_KEYS}'#' \ -e 's#F7T_COMPUTE_HOST#'${F7T_COMPUTE_HOST}'#' \ -e 's#F7T_COMPUTE_PORT#'${F7T_COMPUTE_PORT}'#' \ -e 's#F7T_STATUS_HOST#'${F7T_STATUS_HOST}'#' \ diff --git a/deploy/k8s/config/templates/_helpers.tpl b/deploy/k8s/config/templates/_helpers.tpl index bc445d48..17ef4e3c 100644 --- a/deploy/k8s/config/templates/_helpers.tpl +++ b/deploy/k8s/config/templates/_helpers.tpl @@ -1,7 +1,7 @@ {{- define "list.listPubKeys" -}} {{- $map := dict }} {{- range .Values.global.auth }} -{{- $_ := set $map .F7T_AUTH_REALM_PUBKEY ""}} +{{- $_ := set $map .F7T_AUTH_PUBKEY ""}} {{- end }} {{- keys $map | join ";" }} {{- end }} @@ -9,7 +9,7 @@ {{- define "list.listPubKeyTypes" -}} {{- $map := dict }} {{- range .Values.global.auth }} -{{- $_ := set $map .F7T_AUTH_REALM_TYPE ""}} +{{- $_ := set $map .F7T_AUTH_ALGORITHM ""}} {{- end }} {{- keys $map | join ";" }} {{- end }} \ No newline at end of file diff --git a/deploy/k8s/config/templates/cm.common.yaml b/deploy/k8s/config/templates/cm.common.yaml index 060166de..9ee2600c 100644 --- a/deploy/k8s/config/templates/cm.common.yaml +++ b/deploy/k8s/config/templates/cm.common.yaml @@ -26,8 +26,8 @@ data: F7T_LOG_TYPE: "stdout" F7T_GUNICORN_LOG: "" F7T_OBJECT_STORAGE: "{{ .Values.F7T_OBJECT_STORAGE }}" - F7T_REALM_RSA_PUBLIC_KEY: '{{ include "list.listPubKeys" . }}' - F7T_REALM_RSA_TYPE: '{{ include "list.listPubKeyTypes" . }}' + F7T_AUTH_PUBLIC_KEYS: '{{ include "list.listPubKeys" . }}' + F7T_AUTH_ALGORITHMS: '{{ include "list.listPubKeyTypes" . }}' F7T_SSH_CERTIFICATE_WRAPPER_ENABLED: "{{ .Values.F7T_SSH_CERTIFICATE_WRAPPER_ENABLED }}" F7T_SSL_ENABLED: "{{ .Values.F7T_SSL_ENABLED }}" F7T_SSL_CRT: "{{ .Values.F7T_SSL_CRT }}" diff --git a/deploy/k8s/kong/templates/cm.kong.yaml b/deploy/k8s/kong/templates/cm.kong.yaml index be8fac54..074bec56 100644 --- a/deploy/k8s/kong/templates/cm.kong.yaml +++ b/deploy/k8s/kong/templates/cm.kong.yaml @@ -51,8 +51,8 @@ items: {{- range .Values.global.auth }} - jwt_secrets: - key: "{{ .F7T_AUTH_ISSUER }}" - algorithm: "{{ .F7T_AUTH_REALM_TYPE }}" - rsa_public_key: "-----BEGIN PUBLIC KEY-----\n{{ .F7T_AUTH_REALM_PUBKEY }}\n-----END PUBLIC KEY-----" + algorithm: "{{ .F7T_AUTH_ALGORITHM }}" + rsa_public_key: "-----BEGIN PUBLIC KEY-----\n{{ .F7T_AUTH_PUBKEY }}\n-----END PUBLIC KEY-----" username: "{{ .username }}" {{- end }} diff --git a/deploy/k8s/values-dev.yaml b/deploy/k8s/values-dev.yaml index 4240e981..5aa18758 100644 --- a/deploy/k8s/values-dev.yaml +++ b/deploy/k8s/values-dev.yaml @@ -99,5 +99,5 @@ global: auth: - username: kc-demo F7T_AUTH_ISSUER: "http://svc-keycloak:8080/auth/realms/kcrealm" - F7T_AUTH_REALM_PUBKEY: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB' - F7T_AUTH_REALM_TYPE: "RS256" \ No newline at end of file + F7T_AUTH_PUBKEY: 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqB44q32bQp8LbyW6dQvgsjseXESkLT1g5LQKGb+P79AC+nOAtxhn8i/kmgc6zsQH8NlUtNJruLxlzdo2/OGmlDGYZH1x6VmAwvJPJ4er0xPUrvZ8YclxYQC16PY5LFiQRNBMRyQwP5Kne1O46FpmADFVWMfoabdnaqoXexxB56b25o8tE2ulRBgfpnrRgZAvf7kWjugRCNO06FV074FVMYHA1aBk0ICyaFCDM/Tb5oaDyGr5c/ZvdrRUrw8vaiYyMgaAnnJPL75cebGoHeMJaEyZalsHA+iuhRAfeAwpSClsmhVqnfH7a7hqrqumVRo27dydqmfVgpFjU5gbFcBZ5wIDAQAB' + F7T_AUTH_ALGORITHM: "RS256" \ No newline at end of file diff --git a/deploy/test-build/environment/common.env b/deploy/test-build/environment/common.env index 19aa9232..f3374b75 100644 --- a/deploy/test-build/environment/common.env +++ b/deploy/test-build/environment/common.env @@ -7,14 +7,14 @@ F7T_AUTH_HEADER_NAME=Authorization # If AUTH_HEADER_NAME = Authorization, it can also check REALM_RSA_PUBLIC_KEY: RSA key from KeyCloak Realm which signs token. # REALM_RSA_PUBLIC_KEY="MII....QAB" # use 1 line without headers ("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----") -F7T_REALM_RSA_PUBLIC_KEY= +F7T_AUTH_PUBLIC_KEYS= #kid: "fVc6h439Xv...." F7T_AUTH_TOKEN_ISSUER='' # specify Audience established by Keycloak, leave empty to skip verification F7T_AUTH_TOKEN_AUD='' # Keycloak scope for clients: F7T_AUTH_REQUIRED_SCOPE='' -F7T_REALM_RSA_TYPE=RS256 +F7T_AUTH_ALGORITHMS=RS256 # AUTHENTICATION ROLE for FirecREST Service Accounts F7T_AUTH_ROLE='' # DEBUG FLAG diff --git a/doc/configuration.md b/doc/configuration.md index 0ec6de52..b03afdc9 100644 --- a/doc/configuration.md +++ b/doc/configuration.md @@ -34,8 +34,8 @@ The most complete way of installing is to setup 3 hosts: | **Name** | **Needs to be configured?** | **Default value** | **Definition** | **Hosts where it's used** | **Change from this version** | | ------ | ----------- | ------ | ----- | ---- | ----------------- | -|`F7T_REALM_RSA_PUBLIC_KEY` | **YES** | `''` | Value of [OIDC/OAuth2 Server Public Client Keys](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1). This is the public key used for the Identity Provider (IdP) to sign the [JWT Access Token](https://jwt.io/introduction). If there are more than one IdP, list the public keys in a semicolon separated list | `Backend`, `Certificator`, `Gateway` | | -|`F7T_REALM_RSA_TYPE` | **YES** | `''` | Value of [cryptographic algorithm used to sign JWT](https://datatracker.ietf.org/doc/html/rfc7518#section-3). Values are found in the `alg` part of the header of the JWT (`"alg": "RS256"`, `"alg": "HS256"`, etc). If there are more than one IdP, list algorithms in a semicolon separated list following the order of the keys in `F7T_REALM_RSA_PUBLIC_KEY` | `Backend`, `Certificator`, `Gateway` | | +|`F7T_AUTH_PUBLIC_KEYS` | **YES** | `''` | Value of [OIDC/OAuth2 Server Public Client Keys](https://datatracker.ietf.org/doc/html/rfc6749#section-2.1). This is the public key used for the Identity Provider (IdP) to sign the [JWT Access Token](https://jwt.io/introduction). If there are more than one IdP, list the public keys in a semicolon separated list | `Backend`, `Certificator`, `Gateway` | | +|`F7T_AUTH_ALGORITHMS` | **YES** | `'RS256'` | Value of [cryptographic algorithm used to sign JWT](https://datatracker.ietf.org/doc/html/rfc7518#section-3). Values are found in the `alg` part of the header of the JWT (`"alg": "RS256"`, `"alg": "HS256"`, etc). If there are more than one IdP, list algorithms in a semicolon separated list following the order of the keys in `F7T_AUTH_PUBLIC_KEYS` | `Backend`, `Certificator`, `Gateway` | | |`F7T_SYSTEMS_PUBLIC_NAME` | **YES** | `''` | Public name(s) of the systems/HPC clusters interfaced by FirecREST. This name is a "familiar" name for users/clients. If more than one system is interfaced by FirecREST, then set this value with a semicolon separated list of names | `Backend` | Replaces `F7T_SYSTEMS_PUBLIC` | |`F7T_SYSTEMS_INTERNAL_ADDR` | **YES** | `''` | Internal socket address (DNS or IP, and SSH port), in the form `:`, of the host used for SSH connection and command execution in relative order of `F7T_SYSTEMS_PUBLIC_NAME` (example: `192.168.220.12:22`, `cluster01.svc.com:22;cluster02.svc.com:22`). This is usually a **"login node"** of the HPC system| `Backend` | New on this version | |`F7T_STATUS_SERVICES` | **YES** | `''` | Semicolon separated list of FirecREST services to report status (example: `compute;storage;utilities`) | `Backend` | @@ -95,6 +95,7 @@ The most complete way of installing is to setup 3 hosts: |`F7T_SPANK_PLUGIN_OPTION` | only if `F7T_SPANK_PLUGIN_ENABLED=True` | `--nohome`| Name of the option to use in the workload manager command. If there is more than one system configured, there should be a semicolon separated list in relative order to `F7T_SYSTEMS_PUBLIC_NAME` values | `Backend`| |`F7T_COMPUTE_SCHEDULER` | NO | `'Slurm'`| Set to the name of the of the Workload Manager scheduler adapter class. By default it can be found in `/src/common/schedulers` | `Backend`| |`F7T_SSH_CERTIFICATE_WRAPPER_ENABLED` | NO | `False`| If set to `True` it enables FirecREST to send an SSH Certificate as command for execution. Requires a serverside [SSH ForceCommand](https://shaner.life/the-little-known-ssh-forcecommand/) wrapper | `Backend`| Replaces `F7T_SSH_CERTIFICATE_WRAPPER` | +|`F7T_CA_KEY_PATH`| NO | `'/ca-key'` | Set the absolute path in the `certificator` container where the Private Key for creating SSH certificates is stored | `Certificator`| |`F7T_PRIV_USER_KEY_PATH`| NO | `'/user-key'` | Set the absolute path in the containers on `Backend` where the user Public Key is stored in order to create SSH certificates | `Backend` | |`F7T_PUB_USER_KEY_PATH`| NO | `'/user-key.pub'` | Set the absolute path in the containers on `Backend` where the user's Private Key is stored in order to create SSH certificates | `Certificator` | |`F7T_SYSTEMS_INTERNAL_STATUS_ADDR` | NO | the value on `F7T_SYSTEMS_INTERNAL_ADDR` | Internal socket address (DNS or IP, and SSH port) of the host used for **testing system availability via SSH** in the form `:` in relative order of `F7T_SYSTEMS_PUBLIC_NAME` (example: `192.168.220.12:22`, `cluster01.svc.com:22;cluster02.svc.com:22`). **Note**: Set this variable only if you use a dedicated server for `status` microservice | `Backend` | Replaces `F7T_STATUS_SYSTEMS` | diff --git a/doc/install.md b/doc/install.md index cd85a576..040309c4 100644 --- a/doc/install.md +++ b/doc/install.md @@ -75,7 +75,7 @@ This will have to match FirecREST's environment variables: `F7T_AUTH_ROLE` and ` If the variable `F7T_AUTH_REQUIRED_SCOPE` is set, FirecREST checks the field is present on the JWT and that it matches. If empty or undefined, no check is performed. -The environment variable `F7T_REALM_RSA_PUBLIC_KEY` holds the RSA public key from the OIDC provider. +The environment variable `F7T_AUTH_PUBLIC_KEYS` holds the RSA public key from the OIDC provider. It is used to validate JWT tokens included on requests (via 'Authorization' header). If empty, no verification is made on the token, which is only useful for debugging. Additionally, if not running in debug mode (`F7T_DEBUG_MODE`) microservices will log a warning. As some systems are tricky with variables containing multiple lines, define the variable using only one line without headers (`-----BEGIN PUBLIC KEY-----`, `-----END PUBLIC KEY-----`), they will be added by F7T. For Keycloak, the signing public key and the endpoints can be retrieved from https://KEYCLOAK_URL/auth/realms/REALM_NAME/ @@ -256,7 +256,7 @@ The public key (`user-key.pub`) is only required by Certificator and must be in The following variables are not required by every microservice, but for simplicity they can be put together in the same file: - `F7T_CERTIFICATOR_HOST`, `F7T_COMPUTE_HOST`, `F7T_RESERVATIONS_HOST`, `F7T_STORAGE_HOST`, `F7T_TASKS_HOST`, `F7T_UTILITIES_HOST`: internal HostName, DNS, IP (not exposed to users) used by microservices to communicate between them. -- `F7T_REALM_RSA_PUBLIC_KEY`, if defined also requires: `F7T_REALM_RSA_TYPE` +- `F7T_AUTH_PUBLIC_KEYS`, if defined also requires: `F7T_AUTH_ALGORITHMS` - `F7T_SYSTEMS_PUBLIC_NAME`: list of systems names, as seen by users. There are additional options at `doc/configuration.md` diff --git a/src/certificator/certificator.py b/src/certificator/certificator.py index a6cc469a..a904383b 100644 --- a/src/certificator/certificator.py +++ b/src/certificator/certificator.py @@ -20,10 +20,10 @@ import threading import sys -# Checks if an environment variable injected to F7T is a valid True value -# var <- object -# returns -> boolean -def get_boolean_var(var): + +def get_boolean_var(var) -> bool: + # Checks if an environment variable injected to F7T is a valid True value + # var <- object # ensure variable to be a string var = str(var) # True, true or TRUE @@ -31,17 +31,20 @@ def get_boolean_var(var): # 1 return var.upper() == "TRUE" or var.upper() == "YES" or var == "1" -AUTH_HEADER_NAME = os.environ.get("F7T_AUTH_HEADER_NAME","Authorization") + +AUTH_HEADER_NAME = os.environ.get("F7T_AUTH_HEADER_NAME", "Authorization") AUTH_AUDIENCE = os.environ.get("F7T_AUTH_TOKEN_AUD", '').strip('\'"') -AUTH_REQUIRED_SCOPE = os.environ.get("F7T_AUTH_REQUIRED_SCOPE", '').strip('\'"') +AUTH_REQUIRED_SCOPE = os.environ.get("F7T_AUTH_REQUIRED_SCOPE", + '').strip('\'"') AUTH_ROLE = os.environ.get("F7T_AUTH_ROLE", '').strip('\'"') CERTIFICATOR_PORT = os.environ.get("F7T_CERTIFICATOR_PORT", 5000) # Fobidden chars on command certificate: avoid shell special chars -# Difference to other microservices: allow '>' for 'cat' and 'head', '&' for Storage URLs, single quotes (') for arguments +# Difference to other microservices: allow '>' for 'cat' and 'head', '&' for +# Storage URLs, single quotes (') for arguments # Commands must only use single quotes # r'...' specifies it's a regular expression with special treatment for \ FORBIDDEN_COMMAND_CHARS = r'[\|\<\;\"\\\(\)\x00-\x1F\x60]' @@ -51,35 +54,46 @@ def get_boolean_var(var): # OPA endpoint -OPA_ENABLED = get_boolean_var(os.environ.get("F7T_OPA_ENABLED",False)) -OPA_URL = os.environ.get("F7T_OPA_URL","http://localhost:8181").strip('\'"') -OPA_POLICY_PATH = os.environ.get("F7T_OPA_POLICY_PATH","v1/data/f7t/authz").strip('\'"') +OPA_ENABLED = get_boolean_var(os.environ.get("F7T_OPA_ENABLED", False)) +OPA_URL = os.environ.get("F7T_OPA_URL", "http://localhost:8181").strip('\'"') +OPA_POLICY_PATH = os.environ.get("F7T_OPA_POLICY_PATH", + "v1/data/f7t/authz").strip('\'"') -### SSL parameters +# SSL parameters SSL_ENABLED = get_boolean_var(os.environ.get("F7T_SSL_ENABLED", False)) SSL_CRT = os.environ.get("F7T_SSL_CRT", "") SSL_KEY = os.environ.get("F7T_SSL_KEY", "") -### ca-key and user-key.pub keys path +# ca-key and user-key.pub keys path CA_KEY_PATH = os.environ.get("F7T_CA_KEY_PATH", "/ca-key") PUB_USER_KEY_PATH = os.environ.get("F7T_PUB_USER_KEY_PATH", "/user-key.pub") TRACER_HEADER = "uber-trace-id" -REALM_RSA_PUBLIC_KEYS=os.environ.get("F7T_REALM_RSA_PUBLIC_KEY", '').strip('\'"').split(";") +AUTH_PUBLIC_KEYS = os.environ.get("F7T_AUTH_PUBLIC_KEYS", + "").strip('\'"').split(";") +AUTH_ALGORITHMS = os.environ.get("F7T_AUTH_ALGORITHMS", + "").strip('\'"').split(";") + +if len(AUTH_PUBLIC_KEYS) != len(AUTH_ALGORITHMS): + logging.warning("F7T_REALM_RSA_PUBLIC_KEYS and F7T_AUTH_ALGORITHMS" + + " don't have the same size") is_public_key_set = False -if len(REALM_RSA_PUBLIC_KEYS) != 0: - realm_pubkey_list = [] +if len(AUTH_PUBLIC_KEYS) != 0: + auth_pubkeys = [] is_public_key_set = True # headers are inserted here, must not be present + for i in range(len(AUTH_PUBLIC_KEYS)): + + auth_pubkey = {} - for pubkey in REALM_RSA_PUBLIC_KEYS: - realm_pubkey = f"-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----" - realm_pubkey_list.append(realm_pubkey) + auth_pubkey["pubkey"] = f"-----BEGIN PUBLIC KEY-----\n{AUTH_PUBLIC_KEYS[i]}\n-----END PUBLIC KEY-----" + auth_pubkey["alg"] = AUTH_ALGORITHMS[i] + + auth_pubkeys.append(auth_pubkey) - realm_pubkey_type = os.environ.get("F7T_REALM_RSA_TYPE").strip('\'"') DEBUG_MODE = get_boolean_var(os.environ.get("F7T_DEBUG_MODE", False)) @@ -92,37 +106,41 @@ def get_boolean_var(var): JAEGER_AGENT = os.environ.get("F7T_JAEGER_AGENT", "").strip('\'"') if JAEGER_AGENT != "": config = Config( - config={'sampler': {'type': 'const', 'param': 1 }, - 'local_agent': {'reporting_host': JAEGER_AGENT, 'reporting_port': 6831 }, - 'logging': True, - 'reporter_batch_size': 1}, - service_name = "certificator") + config={'sampler': {'type': 'const', 'param': 1}, + 'local_agent': {'reporting_host': JAEGER_AGENT, + 'reporting_port': 6831}, + 'logging': True, + 'reporter_batch_size': 1}, service_name="certificator") jaeger_tracer = config.initialize_tracer() tracing = FlaskTracing(jaeger_tracer, True, app) else: jaeger_tracer = None tracing = None -# formatter is executed for every log + class LogRequestFormatter(logging.Formatter): + # formatter is executed for every log def format(self, record): try: - # try to get TID from Flask g object, it's set on @app.before_request on each microservice + # try to get TID from Flask g object, it's set on + # @app.before_request on each microservice record.TID = g.TID - except: + except Exception: try: record.TID = threading.current_thread().name - except: + except Exception: record.TID = 'notid' return super().format(record) + def setup_logging(logging, service): logger = logging.getLogger() LOG_TYPE = os.environ.get("F7T_LOG_TYPE", "file").strip('\'"') if LOG_TYPE == "file": LOG_PATH = os.environ.get("F7T_LOG_PATH", '/var/log').strip('\'"') # timed rotation: 1 (interval) rotation per day (when="D") - logHandler = TimedRotatingFileHandler(f'{LOG_PATH}/{service}.log', when='D', interval=1) + logHandler = TimedRotatingFileHandler(f'{LOG_PATH}/{service}.log', + when='D', interval=1) elif LOG_TYPE == "stdout": logHandler = logging.StreamHandler(stream=sys.stdout) else: @@ -130,9 +148,8 @@ def setup_logging(logging, service): logger.error(msg) sys.exit(msg) - logFormatter = LogRequestFormatter('%(asctime)s,%(msecs)d %(thread)s [%(TID)s] %(levelname)-8s [%(filename)s:%(lineno)d] %(message)s', - '%Y-%m-%dT%H:%M:%S') + '%Y-%m-%dT%H:%M:%S') logHandler.setFormatter(logFormatter) # set handler to logger @@ -149,21 +166,23 @@ def setup_logging(logging, service): if OPA_ENABLED: logging.info(f"OPA: enabled, using {OPA_URL}/{OPA_POLICY_PATH}") else: - logging.info(f"OPA: disabled") + logging.info("OPA: disabled") return logger + def check_key_permission(): # check that CA private key has proper permissions: 400 (no user write, and no access for group and others) - import stat, sys try: cas = os.stat(CA_KEY_PATH).st_mode if oct(cas & 0o477) != '0o400': - msg = f"ERROR: wrong '{CA_KEY_PATH}' permissions, please set to 400. Exiting." + msg = f"ERROR: wrong '{CA_KEY_PATH}' permissions," + " please set to 400. Exiting." app.logger.error(msg) sys.exit(msg) except OSError as e: - msg = f"ERROR: couldn't stat '{CA_KEY_PATH}', message: {e.strerror} - Exiting." + msg = f"ERROR: couldn't stat '{CA_KEY_PATH}', message: {e.strerror}" + " - Exiting." app.logger.error(msg) sys.exit(msg) @@ -178,23 +197,29 @@ def check_key_permission(): # # use: # check_user_auth(username,system) -def check_user_auth(username,system): +def check_user_auth(username, system): # check if OPA is active if OPA_ENABLED: input = {"input":{"user": f"{username}", "system": f"{system}"}} try: - resp_opa = requests.post(f"{OPA_URL}/{OPA_POLICY_PATH}", json=input, verify= (SSL_CRT if SSL_ENABLED else False)) + resp_opa = requests.post(f"{OPA_URL}/{OPA_POLICY_PATH}", + json=input, + verify=(SSL_CRT if SSL_ENABLED else False)) msg = f"{resp_opa.status_code} {resp_opa.text}" logging.info(f"resp_opa: {msg}") if not resp_opa.ok: - return {"allow": False, "description":f"Server error: {msg}", "status_code": resp_opa.status_code} + return {"allow": False, + "description": f"Server error: {msg}", + "status_code": resp_opa.status_code} if resp_opa.json()["result"]["allow"]: logging.info(f"User {username} authorized by OPA") - return {"allow": True, "description":f"User {username} authorized", "status_code": 200 } + return {"allow": True, + "description": f"User {username} authorized", + "status_code": 200} else: logging.error(f"User {username} NOT authorized by OPA") return {"allow": False, "description":f"Permission denied for user {username} in {system}", "status_code": 401} @@ -229,7 +254,8 @@ def check_header(header): decoded = jwt.decode(token, options={"verify_signature": False}) decoding_result = True - # only check for expired signature or general exception for this case + # only check for expired signature or general exception for this + # case except jwt.exceptions.ExpiredSignatureError: decoding_reason = "JWT token has expired" logging.error(decoding_reason, exc_info=True) @@ -238,16 +264,21 @@ def check_header(header): logging.error(decoding_reason, exc_info=True) else: # iterates over the list of public keys - for realm_pubkey in realm_pubkey_list: + for auth_pubkey in auth_pubkeys: if DEBUG_MODE: - logging.debug(f"Trying decoding with [...{realm_pubkey[71:81]}...] public key...") + logging.debug(f"Trying decoding with Public Key ({i})" + + f"[...{auth_pubkey['pubkey'][71:81]}...] ...") try: if AUTH_AUDIENCE == '': - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], options={'verify_aud': False}) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + options={'verify_aud': False}) else: - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], audience=AUTH_AUDIENCE) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + audience=AUTH_AUDIENCE) if DEBUG_MODE: - logging.info(f"Correctly decoded") + logging.info("Correctly decoded") # if all passes, it means the signature is valid decoding_result = True @@ -277,7 +308,8 @@ def check_header(header): if DEBUG_MODE: logging.debug(f"Result: {decoding_result}. Reason: {decoding_reason}") - # if token was successfully decoded, then check if required scope is present + # if token was successfully decoded, then check if required scope is + # present if AUTH_REQUIRED_SCOPE != "" and decoding_result: if AUTH_REQUIRED_SCOPE not in decoded["scope"].split(): decoding_result = False @@ -287,8 +319,6 @@ def check_header(header): return {"result": decoding_result, "reason": decoding_reason} - - # receive the header, and extract the username from the token # returns username def get_username(header): @@ -317,16 +347,21 @@ def get_username(header): else: # iterates over the list of public keys - for realm_pubkey in realm_pubkey_list: + for auth_pubkey in auth_pubkeys: if DEBUG_MODE: - logging.debug(f"Trying decoding with [...{realm_pubkey[71:81]}...] public key...") + logging.debug(f"Trying decoding with Public Key ({i})" + + f"[...{auth_pubkey['pubkey'][71:81]}...] ...") try: if AUTH_AUDIENCE == '': - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], options={'verify_aud': False}) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + options={'verify_aud': False}) else: - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], audience=AUTH_AUDIENCE) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + audience=AUTH_AUDIENCE) if DEBUG_MODE: - logging.info(f"Correctly decoded") + logging.info("Correctly decoded") # if token is correctly decoded, exit the loop decoding_result = True diff --git a/src/common/common.env.template b/src/common/common.env.template index 94245505..47e5118f 100644 --- a/src/common/common.env.template +++ b/src/common/common.env.template @@ -5,15 +5,15 @@ # Authorization: JWT token as generated by Keycloak: {"Authorization:", "Bearer fjfk..."} F7T_AUTH_HEADER_NAME=Authorization # If F7T_AUTH_HEADER_NAME = Authorization, it can also check REALM_RSA_PUBLIC_KEY: RSA key from KeyCloak Realm which signs token. -# F7T_REALM_RSA_PUBLIC_KEY="MII....QAB" +# F7T_AUTH_PUBLIC_KEYS="MII....QAB" # use 1 line without headers ("-----BEGIN PUBLIC KEY-----", "-----END PUBLIC KEY-----") -F7T_REALM_RSA_PUBLIC_KEY='' +F7T_AUTH_PUBLIC_KEYS='' F7T_AUTH_TOKEN_ISSUER='' # specify Audience established by Keycloak, leave empty to skip verification F7T_AUTH_TOKEN_AUD='' # Keycloak scope for clients: F7T_AUTH_REQUIRED_SCOPE='' -F7T_REALM_RSA_TYPE=RS256 +F7T_AUTH_ALGORITHMS=RS256 # AUTHENTICATION ROLE for FirecREST Service Accounts F7T_AUTH_ROLE='' #------- diff --git a/src/common/cscs_api_common.py b/src/common/cscs_api_common.py index 92c751b3..8fcaea4f 100644 --- a/src/common/cscs_api_common.py +++ b/src/common/cscs_api_common.py @@ -49,22 +49,27 @@ def get_null_var(var): DEBUG_MODE = get_boolean_var(os.environ.get("F7T_DEBUG_MODE", False)) -AUTH_HEADER_NAME = os.environ.get("F7T_AUTH_HEADER_NAME","Authorization") +AUTH_HEADER_NAME = os.environ.get("F7T_AUTH_HEADER_NAME", "Authorization") -REALM_RSA_PUBLIC_KEYS=os.environ.get("F7T_REALM_RSA_PUBLIC_KEY", '').strip('\'"').split(";") +AUTH_PUBLIC_KEYS = os.environ.get("F7T_AUTH_PUBLIC_KEYS", + "").strip('\'"').split(";") +AUTH_ALGORITHMS = os.environ.get("F7T_AUTH_ALGORITHMS", + "RS256").strip('\'"').split(";") is_public_key_set = False -if len(REALM_RSA_PUBLIC_KEYS) != 0: - realm_pubkey_list = [] +if len(AUTH_PUBLIC_KEYS) != 0: + auth_pubkeys = [] is_public_key_set = True # headers are inserted here, must not be present + for i in range(len(AUTH_PUBLIC_KEYS)): - for pubkey in REALM_RSA_PUBLIC_KEYS: - realm_pubkey = f"-----BEGIN PUBLIC KEY-----\n{pubkey}\n-----END PUBLIC KEY-----" - realm_pubkey_list.append(realm_pubkey) + auth_pubkey = {} - realm_pubkey_type = os.environ.get("F7T_REALM_RSA_TYPE").strip('\'"') + auth_pubkey["pubkey"] = f"-----BEGIN PUBLIC KEY-----\n{AUTH_PUBLIC_KEYS[i]}\n-----END PUBLIC KEY-----" + auth_pubkey["alg"] = AUTH_ALGORITHMS[i] + + auth_pubkeys.append(auth_pubkey) AUTH_AUDIENCE = os.environ.get("F7T_AUTH_TOKEN_AUD", '').strip('\'"') AUTH_REQUIRED_SCOPE = os.environ.get("F7T_AUTH_REQUIRED_SCOPE", '').strip('\'"') @@ -116,19 +121,20 @@ def get_null_var(var): def check_header(header): # header = remove the "Bearer " string - token = header.replace("Bearer ","") + token = header.replace("Bearer ", "") decoding_result = False decoding_reason = "" if not is_public_key_set: if not DEBUG_MODE: - logging.debug("WARNING: REALM_RSA_PUBLIC_KEY is empty, JWT tokens are NOT verified, setup is not set to debug.") + logging.warning("REALM_RSA_PUBLIC_KEY is empty, JWT tokens " + + " are NOT verified, setup is not set to debug.") try: decoded = jwt.decode(token, options={"verify_signature": False}) decoding_result = True - # only check for expired signature or general exception for this case + # only check for expired signature or exceptions for this case except jwt.exceptions.ExpiredSignatureError: decoding_reason = "JWT token has expired" logging.error(decoding_reason, exc_info=True) @@ -137,18 +143,21 @@ def check_header(header): logging.error(decoding_reason, exc_info=True) else: # iterates over the list of public keys - for realm_pubkey in realm_pubkey_list: + for auth_pubkey in auth_pubkeys: if DEBUG_MODE: - logging.debug(f"Trying decoding with [...{realm_pubkey[71:81]}...] public key...") - logging.debug(f"Getting JWT from header {AUTH_HEADER_NAME}") - logging.debug(f"Value: {token}") + logging.debug(f"Trying decoding with Public Key ({i})" + + f"[...{auth_pubkey['pubkey'][71:81]}...] ...") try: if AUTH_AUDIENCE == '': - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], options={'verify_aud': False}) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + options={'verify_aud': False}) else: - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], audience=AUTH_AUDIENCE) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + audience=AUTH_AUDIENCE) if DEBUG_MODE: - logging.debug(f"Token correctly decoded") + logging.info("Correctly decoded") # if all passes, it means the signature is valid decoding_result = True @@ -178,7 +187,7 @@ def check_header(header): if DEBUG_MODE: logging.debug(f"Result: {decoding_result}. Reason: {decoding_reason}") - # if token was successfully decoded, then check if required scope is present + # if token was decoded, then check if required scope is present if AUTH_REQUIRED_SCOPE != "" and decoding_result: if AUTH_REQUIRED_SCOPE not in decoded["scope"].split(): decoding_result = False @@ -188,48 +197,53 @@ def check_header(header): return {"result": decoding_result, "reason": decoding_reason} - - -# receive the header, and extract the username from the token -# returns username def get_username(header): - + # receive the header, and extract the username from the token + # returns username # header = remove the "Bearer " string - token = header.replace("Bearer ","") + token = header.replace("Bearer ", "") decoding_result = False decoding_reason = "" # does FirecREST check the signature of the token? if not is_public_key_set: if not DEBUG_MODE: - logging.warning("WARNING: REALM_RSA_PUBLIC_KEY is empty, JWT tokens are NOT verified, setup is not set to debug.") + logging.warning("REALM_RSA_PUBLIC_KEY is empty, JWT tokens are " + + "NOT verified, setup is not set to debug.") try: decoded = jwt.decode(token, options={"verify_signature": False}) decoding_result = True - # only check for expired signature or general exception for this case + # only check for expired signature or exception for this case except jwt.exceptions.ExpiredSignatureError: logging.error("JWT token has expired", exc_info=True) - return {"result": False, "reason":"JWT token has expired", "username": None} + return {"result": False, "reason": "JWT token has expired", + "username": None} except Exception: - logging.error("Bad header or JWT, general exception raised", exc_info=True) - return {"result": False, "reason":"Bad header or JWT, general exception raised", "username": None} + logging.error("Bad header or JWT, general exception raised", + exc_info=True) + return {"result": False, + "reason": "Bad header or JWT, general exception raised", + "username": None} else: # iterates over the list of public keys - for realm_pubkey in realm_pubkey_list: + for auth_pubkey in auth_pubkeys: if DEBUG_MODE: - logging.debug(f"Trying decoding with [...{realm_pubkey[71:81]}...] public key...") - logging.debug(f"Getting JWT from header {AUTH_HEADER_NAME}") - logging.debug(f"Value: {token}") + logging.debug(f"Trying decoding with Public Key ({i})" + + f"[...{auth_pubkey['pubkey'][71:81]}...] ...") try: if AUTH_AUDIENCE == '': - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], options={'verify_aud': False}) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + options={'verify_aud': False}) else: - decoded = jwt.decode(token, realm_pubkey, algorithms=[realm_pubkey_type], audience=AUTH_AUDIENCE) + decoded = jwt.decode(token, auth_pubkey["pubkey"], + algorithms=[auth_pubkey["alg"]], + audience=AUTH_AUDIENCE) if DEBUG_MODE: - logging.debug(f"Correctly decoded") + logging.info("Correctly decoded") # if token is correctly decoded, exit the loop decoding_result = True @@ -407,7 +421,7 @@ def exec_remote_command(headers, system_name, system_addr, action, file_transfer if SSH_CERTIFICATE_WRAPPER_ENABLED: if DEBUG_MODE: - logging.debug(f"Using F7T_SSH_CERTIFICATE_WRAPPER_ENABLED option") + logging.debug("Using F7T_SSH_CERTIFICATE_WRAPPER_ENABLED option") # read cert to send it as a command to the server with open(pub_cert, 'r') as cert_file: diff --git a/src/status/status.py b/src/status/status.py index 8c823f41..bb404e5f 100644 --- a/src/status/status.py +++ b/src/status/status.py @@ -91,7 +91,7 @@ def set_services(): for servicename in SERVICES: SERVICE_HOST_ENV_VAR_NAME = f"F7T_{servicename.upper()}_HOST" SERVICE_PORT_ENV_VAR_NAME = f"F7T_{servicename.upper()}_PORT" - service_host = os.environ.get(SERVICE_HOST_ENV_VAR_NAME) + service_host = os.environ.get(SERVICE_HOST_ENV_VAR_NAME, "127.0.0.1") service_port = os.environ.get(SERVICE_PORT_ENV_VAR_NAME) if service_host and service_port: SERVICES_DICT[servicename] = f"{F7T_SCHEME_PROTOCOL}://{service_host}:{service_port}"