diff --git a/admin/docker/Dockerfile b/admin/docker/Dockerfile index e8f36b2..0d13ce9 100644 --- a/admin/docker/Dockerfile +++ b/admin/docker/Dockerfile @@ -12,7 +12,7 @@ COPY admin/docker/requirements.pip3 /requirements.pip3 RUN pip3 install --no-cache-dir -r requirements.pip3 RUN apk del .build_deps -RUN apk add --no-cache curl py3-yaml yarn libpq +RUN apk add --no-cache curl py3-yaml yarn libpq openssl # SSH configuration ARG SSH_ROOT_PWD diff --git a/admin/src/admin/lib/keycloak.py b/admin/src/admin/lib/keycloak.py index 8ad0f63..2a0a278 100644 --- a/admin/src/admin/lib/keycloak.py +++ b/admin/src/admin/lib/keycloak.py @@ -215,101 +215,10 @@ class Keycloak(): return {'name':rsa_key['kid'],'certificate':rsa_key['certificate']} ## CLIENTS - def add_moodle_client(self): + def delete_client(self,clientid): self.connect() - demo={ - "id" : "a92d5417-92b6-4678-9cb9-51bc0edcee8c", - "clientId" : "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/metadata.php", - "surrogateAuthRequired" : False, - "enabled" : True, - "alwaysDisplayInConsole" : False, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+app.config['DOMAIN']+"" ], - "webOrigins" : [ "https://moodle."+app.config['DOMAIN']+"" ], - "notBefore" : 0, - "bearerOnly" : False, - "consentRequired" : False, - "standardFlowEnabled" : True, - "implicitFlowEnabled" : False, - "directAccessGrantsEnabled" : False, - "serviceAccountsEnabled" : False, - "publicClient" : False, - "frontchannelLogout" : True, - "protocol" : "saml", - "attributes" : { - "saml.force.post.binding" : True, - "saml.encrypt" : True, - "saml_assertion_consumer_url_post" : "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+app.config['DOMAIN']+"", - "saml.server.signature" : True, - "saml.server.signature.keyinfo.ext" : False, - "saml.signing.certificate" : app.config['SP_CRT'], - "saml_single_logout_service_url_redirect" : "https://moodle."+app.config['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+app.config['DOMAIN']+"", - "saml.signature.algorithm" : "RSA_SHA256", - "saml_force_name_id_format" : False, - "saml.client.signature" : True, - "saml.encryption.certificate" : app.config['SP_PEM'], - "saml.authnstatement" : True, - "saml_name_id_format" : "username", - "saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : True, - "nodeReRegistrationTimeout" : -1, - "protocolMappers" : [ { - "id" : "9296daa3-4fc4-4b80-b007-5070f546ae13", - "name" : "X500 surname", - "protocol" : "saml", - "protocolMapper" : "saml-user-property-mapper", - "consentRequired" : False, - "config" : { - "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "user.attribute" : "lastName", - "friendly.name" : "surname", - "attribute.name" : "urn:oid:2.5.4.4" - } - }, { - "id" : "ccecf6e4-d20a-4211-b67c-40200a6b2c5d", - "name" : "username", - "protocol" : "saml", - "protocolMapper" : "saml-user-property-mapper", - "consentRequired" : False, - "config" : { - "attribute.nameformat" : "Basic", - "user.attribute" : "username", - "friendly.name" : "username", - "attribute.name" : "username" - } - }, { - "id" : "53858403-eba2-4f6d-81d0-cced700b5719", - "name" : "X500 givenName", - "protocol" : "saml", - "protocolMapper" : "saml-user-property-mapper", - "consentRequired" : False, - "config" : { - "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "user.attribute" : "firstName", - "friendly.name" : "givenName", - "attribute.name" : "urn:oid:2.5.4.42" - } - }, { - "id" : "20034db5-1d0e-4e66-b815-fb0440c6d1e2", - "name" : "X500 email", - "protocol" : "saml", - "protocolMapper" : "saml-user-property-mapper", - "consentRequired" : False, - "config" : { - "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", - "user.attribute" : "email", - "friendly.name" : "email", - "attribute.name" : "urn:oid:1.2.840.113549.1.9.1" - } - } ], - "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], - "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], - "access" : { - "view" : True, - "configure" : True, - "manage" : True - } - } - return self.keycloak_admin.create_client(demo) \ No newline at end of file + return self.keycloak_admin.delete_client(clientid) + + def add_client(self,client): + self.connect() + return self.keycloak_admin.create_client(client) \ No newline at end of file diff --git a/admin/src/admin/lib/nextcloud.py b/admin/src/admin/lib/nextcloud.py index ee72b96..e018833 100644 --- a/admin/src/admin/lib/nextcloud.py +++ b/admin/src/admin/lib/nextcloud.py @@ -15,7 +15,6 @@ class Nextcloud(): url="https://nextcloud."+app.config['DOMAIN'], username=os.environ['NEXTCLOUD_ADMIN_USER'], password=os.environ['NEXTCLOUD_ADMIN_PASSWORD'], - realm='master', verify=True): self.verify_cert=verify @@ -259,6 +258,8 @@ class Nextcloud(): try: result = json.loads(self._request('GET',url)) if result['ocs']['meta']['statuscode'] == 100: return [g for g in result['ocs']['data']['groups']] + import pprint + pprint.pprint(result) raise ProviderOpError except: log.error(traceback.format_exc()) diff --git a/admin/src/moodle_saml.py b/admin/src/moodle_saml.py index c5d09ec..d8db96a 100644 --- a/admin/src/moodle_saml.py +++ b/admin/src/moodle_saml.py @@ -84,7 +84,8 @@ class MoodleSaml(): self.activate_saml_plugin() self.set_moodle_saml_plugin() - self.set_keycloak_moodle_saml_plugin() + self.delete_keycloak_moodle_saml_plugin() + self.add_keycloak_moodle_saml() def activate_saml_plugin(self): ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php @@ -104,6 +105,11 @@ class MoodleSaml(): keycloak.add_moodle_client() keycloak=None + def delete_keycloak_moodle_saml_plugin(self): + keycloak=Keycloak() + keycloak.delete_client('a92d5417-92b6-4678-9cb9-51bc0edcee8c') + keycloak=None + def set_moodle_saml_plugin(self): config={'idpmetadata': self.parse_idp_metadata(), 'certs_locked': '1', @@ -121,5 +127,107 @@ class MoodleSaml(): self.pg.update("""INSERT INTO "mdl_auth_saml2_idps" ("metadataurl", "entityid", "activeidp", "defaultidp", "adminidp", "defaultname", "displayname", "logo", "alias", "whitelist") VALUES ('xml', 'https://sso.%s/auth/realms/master', 1, 0, 0, 'Login via SAML2', '', NULL, NULL, NULL);""" % (os.environ['DOMAIN'])) + def add_keycloak_moodle_saml(self): + client={ + "id" : "a92d5417-92b6-4678-9cb9-51bc0edcee8c", + "name": "moodle", + "description": "moodle", + "clientId" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/metadata.php", + "surrogateAuthRequired" : False, + "enabled" : True, + "alwaysDisplayInConsole" : False, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"" ], + "webOrigins" : [ "https://moodle."+os.environ['DOMAIN']+"" ], + "notBefore" : 0, + "bearerOnly" : False, + "consentRequired" : False, + "standardFlowEnabled" : True, + "implicitFlowEnabled" : False, + "directAccessGrantsEnabled" : False, + "serviceAccountsEnabled" : False, + "publicClient" : False, + "frontchannelLogout" : True, + "protocol" : "saml", + "attributes" : { + "saml.force.post.binding" : True, + "saml.encrypt" : True, + "saml_assertion_consumer_url_post" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-acs.php/moodle."+os.environ['DOMAIN']+"", + "saml.server.signature" : True, + "saml.server.signature.keyinfo.ext" : False, + "saml.signing.certificate" : app['config']['SP_CRT'], + "saml_single_logout_service_url_redirect" : "https://moodle."+os.environ['DOMAIN']+"/auth/saml2/sp/saml2-logout.php/moodle."+os.environ['DOMAIN']+"", + "saml.signature.algorithm" : "RSA_SHA256", + "saml_force_name_id_format" : False, + "saml.client.signature" : False, + "saml.encryption.certificate" : app['config']['SP_PEM'], + "saml.authnstatement" : True, + "saml_name_id_format" : "username", + "saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : True, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "9296daa3-4fc4-4b80-b007-5070f546ae13", + "name" : "X500 surname", + "protocol" : "saml", + "protocolMapper" : "saml-user-property-mapper", + "consentRequired" : False, + "config" : { + "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "user.attribute" : "lastName", + "friendly.name" : "surname", + "attribute.name" : "urn:oid:2.5.4.4" + } + }, { + "id" : "ccecf6e4-d20a-4211-b67c-40200a6b2c5d", + "name" : "username", + "protocol" : "saml", + "protocolMapper" : "saml-user-property-mapper", + "consentRequired" : False, + "config" : { + "attribute.nameformat" : "Basic", + "user.attribute" : "username", + "friendly.name" : "username", + "attribute.name" : "username" + } + }, { + "id" : "53858403-eba2-4f6d-81d0-cced700b5719", + "name" : "X500 givenName", + "protocol" : "saml", + "protocolMapper" : "saml-user-property-mapper", + "consentRequired" : False, + "config" : { + "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "user.attribute" : "firstName", + "friendly.name" : "givenName", + "attribute.name" : "urn:oid:2.5.4.42" + } + }, { + "id" : "20034db5-1d0e-4e66-b815-fb0440c6d1e2", + "name" : "X500 email", + "protocol" : "saml", + "protocolMapper" : "saml-user-property-mapper", + "consentRequired" : False, + "config" : { + "attribute.nameformat" : "urn:oasis:names:tc:SAML:2.0:attrname-format:uri", + "user.attribute" : "email", + "friendly.name" : "email", + "attribute.name" : "urn:oid:1.2.840.113549.1.9.1" + } + } ], + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], + "access" : { + "view" : True, + "configure" : True, + "manage" : True + } + } + keycloak=Keycloak() + keycloak.add_client(client) + keycloak=None + m=MoodleSaml() \ No newline at end of file diff --git a/admin/src/nextcloud_saml.py b/admin/src/nextcloud_saml.py new file mode 100644 index 0000000..9a2f09f --- /dev/null +++ b/admin/src/nextcloud_saml.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python +# coding=utf-8 +import time, os +from datetime import datetime, timedelta +import pprint + +import logging as log +import traceback +import yaml, json + +import psycopg2 + +from admin.lib.postgres import Postgres +from admin.lib.keycloak import Keycloak + +import string, random + +app={} +app['config']={} + +class NextcloudSaml(): + def __init__(self): + ready=False + while not ready: + try: + self.pg=Postgres('isard-apps-postgresql','nextcloud',os.environ['NEXTCLOUD_POSTGRES_USER'],os.environ['NEXTCLOUD_POSTGRES_PASSWORD']) + ready=True + except: + log.warning('Could not connect to nextcloud database. Retrying...') + time.sleep(2) + log.info('Connected to nextcloud database.') + + ready=False + while not ready: + try: + with open(os.path.join("./saml_certs/public.cert"),"r") as crt: + app['config']['PUBLIC_CERT']=crt.read() + ready=True + except IOError: + log.warning('Could not get public certificate to be used in nextcloud. Retrying...') + log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') + time.sleep(2) + except: + log.error(traceback.format_exc()) + log.info('Got moodle srt certificate to be used in nextcloud.') + + ready=False + while not ready: + try: + with open(os.path.join("./saml_certs/private.key"),"r") as pem: + app['config']['PRIVATE_KEY']=pem.read() + ready=True + except IOError: + log.warning('Could not get private key to be used in nextcloud. Retrying...') + log.warning(' You should generate them: /admin/saml_certs # openssl req -nodes -new -x509 -keyout private.key -out public.cert') + time.sleep(2) + log.info('Got moodle pem certificate to be used in nextcloud.') + + # ## This seems related to the fact that the certificate generated the first time does'nt work. + # ## And when regenerating the certificate de privatekeypass seems not to be used and instead it + # ## will use always this code as filename: 0f635d0e0f3874fff8b581c132e6c7a7 + # ## As this bug I'm not able to solve, the process is: + # ## 1.- Bring up moodle and regenerate certificates on saml2 plugin in plugins-authentication + # ## 2.- Execute this script + # ## 3.- Cleanup all caches in moodle (Development tab) + # # with open(os.path.join("./moodledata/saml2/"+os.environ['NEXTCLOUD_SAML_PRIVATEKEYPASS'].replace("moodle."+os.environ['DOMAIN'],'')+'.idp.xml'),"w") as xml: + # # xml.write(self.parse_idp_metadata()) + # with open(os.path.join("./moodledata/saml2/0f635d0e0f3874fff8b581c132e6c7a7.idp.xml"),"w") as xml: + # xml.write(self.parse_idp_metadata()) + + try: + self.reset_saml() + except: + print('Error resetting saml on nextcloud') + + try: + self.delete_keycloak_nextcloud_saml_plugin() + except: + print('Error resetting saml on keycloak') + + try: + self.set_nextcloud_saml_plugin() + except: + print('Error adding saml on nextcloud') + + try: + self.add_keycloak_nextcloud_saml() + except: + print('Error adding saml on keycloak') + + # def activate_saml_plugin(self): + # ## After you need to purge moodle caches: /var/www/html # php admin/cli/purge_caches.php + # return self.pg.update("""UPDATE "mdl_config" SET value = 'email,saml2' WHERE "name" = 'auth'""") + + # def get_privatekey_pass(self): + # return self.pg.select("""SELECT * FROM "mdl_config" WHERE "name" = 'siteidentifier'""")[0][2] + + def parse_idp_cert(self): + keycloak=Keycloak() + rsa=keycloak.get_server_rsa_key() + keycloak=None + return rsa['certificate'] + + def set_keycloak_nextcloud_saml_plugin(self): + keycloak=Keycloak() + keycloak.add_nextcloud_client() + keycloak=None + + def delete_keycloak_nextcloud_saml_plugin(self): + keycloak=Keycloak() + keycloak.delete_client('bef873f0-2079-4876-8657-067de27d01b7') + keycloak=None + + def set_nextcloud_saml_plugin(self): + self.pg.update("""INSERT INTO "oc_appconfig" ("appid", "configkey", "configvalue") VALUES +('user_saml', 'general-uid_mapping', 'username'), +('user_saml', 'type', 'saml'), +('user_saml', 'sp-privateKey', '%s'), +('user_saml', 'saml-attribute-mapping-email_mapping', 'email'), +('user_saml', 'saml-attribute-mapping-group_mapping', 'Role'), +('user_saml', 'idp-entityId', 'https://sso.%s/auth/realms/master'), +('user_saml', 'idp-singleSignOnService.url', 'https://sso.%s/auth/realms/master/protocol/saml'), +('user_saml', 'idp-x509cert', '%s'), +('user_saml', 'security-authnRequestsSigned', '1'), +('user_saml', 'security-logoutRequestSigned', '1'), +('user_saml', 'security-logoutResponseSigned', '1'), +('user_saml', 'security-wantMessagesSigned', '1'), +('user_saml', 'security-wantAssertionsSigned', '1'), +('user_saml', 'general-idp0_display_name', 'SAML Login'), +('user_saml', 'sp-x509cert', '%s'), +('user_saml', 'idp-singleLogoutService.url', 'https://sso.%s/auth/realms/master/protocol/saml');""" % (app['config']['PRIVATE_KEY'],os.environ['DOMAIN'],os.environ['DOMAIN'],self.parse_idp_cert(),app['config']['PUBLIC_CERT'],os.environ['DOMAIN'])) + + + def reset_saml(self): + cfg_list=['general-uid_mapping', + 'sp-privateKey', + 'saml-attribute-mapping-email_mapping', + 'saml-attribute-mapping-group_mapping', + 'idp-entityId', + 'idp-singleSignOnService.url', + 'idp-x509cert', + 'security-authnRequestsSigned', + 'security-logoutRequestSigned', + 'security-logoutResponseSigned', + 'security-wantMessagesSigned', + 'security-wantAssertionsSigned', + 'general-idp0_display_name', + 'type', + 'sp-x509cert', + 'idp-singleLogoutService.url'] + for cfg in cfg_list: + self.pg.update("""DELETE FROM "oc_appconfig" WHERE appid = 'user_saml' AND configkey = '%s'""" % (cfg)) + + def add_keycloak_nextcloud_saml(self): + client={"id" : "bef873f0-2079-4876-8657-067de27d01b7", + "name": "nextcloud", + "description": "nextcloud", + "clientId" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/metadata", + "surrogateAuthRequired" : False, + "enabled" : True, + "alwaysDisplayInConsole" : False, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs" ], + "webOrigins" : [ "https://nextcloud."+os.environ['DOMAIN'] ], + "notBefore" : 0, + "bearerOnly" : False, + "consentRequired" : False, + "standardFlowEnabled" : True, + "implicitFlowEnabled" : False, + "directAccessGrantsEnabled" : False, + "serviceAccountsEnabled" : False, + "publicClient" : False, + "frontchannelLogout" : True, + "protocol" : "saml", + "attributes" : { + "saml.assertion.signature" : True, + "saml.force.post.binding" : True, + "saml_assertion_consumer_url_post" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/acs", + "saml.server.signature" : True, + "saml.server.signature.keyinfo.ext" : False, + "saml.signing.certificate" : app['config']['PUBLIC_CERT'], + "saml_single_logout_service_url_redirect" : "https://nextcloud."+os.environ['DOMAIN']+"/apps/user_saml/saml/sls", + "saml.signature.algorithm" : "RSA_SHA256", + "saml_force_name_id_format" : False, + "saml.client.signature" : False, + "saml.authnstatement" : True, + "saml_name_id_format" : "username", + "saml_signature_canonicalization_method" : "http://www.w3.org/2001/10/xml-exc-c14n#" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : True, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "e8e4acff-da2b-46aa-8bdb-ba42171671d6", + "name" : "username", + "protocol" : "saml", + "protocolMapper" : "saml-user-attribute-mapper", + "consentRequired" : False, + "config" : { + "attribute.nameformat" : "Basic", + "user.attribute" : "username", + "friendly.name" : "username", + "attribute.name" : "username" + } + }, { + "id" : "28206b59-757b-4e3c-81cb-0b6053b1fd3d", + "name" : "email", + "protocol" : "saml", + "protocolMapper" : "saml-user-property-mapper", + "consentRequired" : False, + "config" : { + "attribute.nameformat" : "Basic", + "user.attribute" : "email", + "friendly.name" : "email", + "attribute.name" : "email" + } + }, { + "id" : "e51e04b9-f71a-42de-819e-dd9285246ada", + "name" : "Roles", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : False, + "config" : { + "single" : True, + "attribute.nameformat" : "Basic", + "friendly.name" : "Roles", + "attribute.name" : "Roles" + } + } ], + "defaultClientScopes" : [ "web-origins", "role_list", "roles", "profile", "email" ], + "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ], + "access" : { + "view" : True, + "configure" : True, + "manage" : True + } + } + keycloak=Keycloak() + keycloak.add_client(client) + keycloak=None + + + +n=NextcloudSaml() \ No newline at end of file diff --git a/docker-compose-parts/admin.yml b/docker-compose-parts/admin.yml index d1293dc..55d571e 100644 --- a/docker-compose-parts/admin.yml +++ b/docker-compose-parts/admin.yml @@ -18,9 +18,10 @@ services: volumes: - /etc/localtime:/etc/localtime:ro - ${BUILD_ROOT_PATH}/admin/src:/admin # Revome in production - - ${BUILD_ROOT_PATH}/custom:/admin/custom #:ro in production - - ${DATA_FOLDER}/avatars:/admin/avatars:ro + - ${BUILD_ROOT_PATH}/../custom:/admin/custom #:ro in production + - ${DATA_FOLDER}/avatars:/admin/avatars:rw - ${DATA_FOLDER}/moodle/saml2:/admin/moodledata/saml2:rw + - ${DATA_FOLDER}/saml_certs:/admin/saml_certs:rw env_file: - .env environment: diff --git a/docker/haproxy/docker-entrypoint.sh b/docker/haproxy/docker-entrypoint.sh index d457b5f..4475cf9 100644 --- a/docker/haproxy/docker-entrypoint.sh +++ b/docker/haproxy/docker-entrypoint.sh @@ -2,7 +2,7 @@ set -e # Set debug path password -PASSWD=$(python3 -c 'import os,crypt,getpass; print(crypt.crypt(os.environ["IPA_ADMIN_PWD"], crypt.mksalt(crypt.METHOD_SHA512)))') +PASSWD=$(python3 -c 'import os,crypt,getpass; print(crypt.crypt(os.environ["ADMINAPP_PASSWORD"], crypt.mksalt(crypt.METHOD_SHA512)))') sed -i "/^ user admin password/c\ user admin password $PASSWD" /usr/local/etc/haproxy/haproxy.cfg LETSENCRYPT_DOMAIN="$DOMAIN" letsencrypt.sh diff --git a/docker/haproxy/haproxy.conf b/docker/haproxy/haproxy.conf index 7b29f20..7d8c210 100644 --- a/docker/haproxy/haproxy.conf +++ b/docker/haproxy/haproxy.conf @@ -7,6 +7,8 @@ global log 127.0.0.1 local0 tune.ssl.default-dh-param 2048 h1-case-adjust content-type Content-Type + h1-case-adjust content-encoding Content-Encoding + h1-case-adjust transfer-encoding Transfer-Encoding defaults mode http @@ -85,6 +87,8 @@ backend be_sso backend be_admin mode http + acl authorized http_auth(AuthUsers) + http-request auth realm AuthUsers unless authorized acl existing-x-forwarded-host req.hdr(X-Forwarded-Host) -m found acl existing-x-forwarded-proto req.hdr(X-Forwarded-Proto) -m found http-request add-header X-Forwarded-Host %[req.hdr(Host)] unless existing-x-forwarded-host