diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index e84e812f..33a46eab 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -7,9 +7,11 @@ services: volumes: - ./federator-env/${FED_ENV}/:/app/federator/config - ./federator-env/${FED_ENV}/db:/app/federator/db - # environment: - # - PM2_PUBLIC_KEY=${PM2_PUBLIC_KEY} - # - PM2_SECRET_KEY=${PM2_SECRET_KEY} + environment: + - NODEREAL_BSC_FEDERATOR_API=${NODEREAL_BSC_FEDERATOR_API:-} + - NODEREAL_ETH_FEDERATOR_API=${NODEREAL_ETH_FEDERATOR_API:-} + # - PM2_PUBLIC_KEY=${PM2_PUBLIC_KEY} + # - PM2_SECRET_KEY=${PM2_SECRET_KEY} datadog: container_name: datadog-agent diff --git a/federator-env/mainnet-BSC-RSK/bmainnet.json b/federator-env/mainnet-BSC-RSK/bmainnet.json index 979507d4..0931cb78 100644 --- a/federator-env/mainnet-BSC-RSK/bmainnet.json +++ b/federator-env/mainnet-BSC-RSK/bmainnet.json @@ -4,6 +4,6 @@ "multiSig": "0xec3fabc3517e64e07669dd1d2d673f466f93a328", "allowTokens": "0xc4b5178Cc086E764568AdfB2dacCBB0d973e8132", "erc777Converter": "0x9D46B33171eA7124aEE472bFe61B5B7084B55069", - "host": "https://bsc.sovryn.app/mainnet", + "host": "https://bsc-mainnet.nodereal.io/v1/${NODEREAL_BSC_FEDERATOR_API}", "fromBlock": 52652645 } diff --git a/federator-env/mainnet-BSC-RSK/config.js b/federator-env/mainnet-BSC-RSK/config.js index 707d52fc..adc6c019 100644 --- a/federator-env/mainnet-BSC-RSK/config.js +++ b/federator-env/mainnet-BSC-RSK/config.js @@ -1,4 +1,21 @@ const fs = require('fs'); + +function expandEnv(value) { + return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_, name) => { + const envValue = process.env[name]; + if (!envValue) throw new Error(`Missing required environment variable ${name}`); + return envValue; + }); +} + +function loadChainConfig(path) { + const chainConfig = require(path); + return { + ...chainConfig, + host: expandEnv(chainConfig.host), + }; +} + let telegramToken; try { telegramToken = fs.readFileSync(`${__dirname}/telegram.key`, 'utf8').trim(); @@ -7,8 +24,8 @@ try { telegramToken = ''; } module.exports = { - mainchain: require('./rskmainnet.json'), //the json containing the smart contract addresses in rsk - sidechain: require('./bmainnet.json'), //the json containing the smart contract addresses in eth + mainchain: loadChainConfig('./rskmainnet.json'), //the json containing the smart contract addresses in rsk + sidechain: loadChainConfig('./bmainnet.json'), //the json containing the smart contract addresses in bsc runEvery: 2, // In minutes, confirmations: 120, // Number of blocks before processing it, if working with ganache set as 0 privateKey: fs.readFileSync(`${__dirname}/federator.key`, 'utf8').trim(), @@ -1235,4 +1252,3 @@ module.exports = { } } } - diff --git a/federator-env/mainnet-ETH-RSK/config.js b/federator-env/mainnet-ETH-RSK/config.js index b1ff36d3..92d7240c 100644 --- a/federator-env/mainnet-ETH-RSK/config.js +++ b/federator-env/mainnet-ETH-RSK/config.js @@ -1,4 +1,21 @@ const fs = require('fs'); + +function expandEnv(value) { + return value.replace(/\$\{([A-Z0-9_]+)\}/g, (_, name) => { + const envValue = process.env[name]; + if (!envValue) throw new Error(`Missing required environment variable ${name}`); + return envValue; + }); +} + +function loadChainConfig(path) { + const chainConfig = require(path); + return { + ...chainConfig, + host: expandEnv(chainConfig.host), + }; +} + let telegramToken; try { telegramToken = fs.readFileSync(`${__dirname}/telegram.key`, 'utf8').trim(); @@ -15,8 +32,8 @@ try { } module.exports = { - mainchain: require('./rskmainnet.json'), //the json containing the smart contract addresses in rsk - sidechain: require('./mainnet.json'), //the json containing the smart contract addresses in eth + mainchain: loadChainConfig('./rskmainnet.json'), //the json containing the smart contract addresses in rsk + sidechain: loadChainConfig('./mainnet.json'), //the json containing the smart contract addresses in eth runEvery: 2, // In minutes, gasApiRunEvery: 5, // In Seconds, avgGasRunEvery: 10, // In Seconds, @@ -753,4 +770,3 @@ module.exports = { } } } - diff --git a/federator-env/mainnet-ETH-RSK/mainnet.json b/federator-env/mainnet-ETH-RSK/mainnet.json index 061fed64..14244b29 100644 --- a/federator-env/mainnet-ETH-RSK/mainnet.json +++ b/federator-env/mainnet-ETH-RSK/mainnet.json @@ -4,6 +4,6 @@ "multiSig": "0x062c74f9d27b1178bb76186c1756128ccb3ccd2e", "allowTokens": "0xf9A59a649859A27d664C8bDb51fA53bCb268545C", "erc777Converter": "0xC0b2A9E31f69e4F0bC24584C678C582714a4fA1b", - "host": "https://eth.sovryn.app/mainnet", + "host": "https://eth-mainnet.nodereal.io/v1/${NODEREAL_ETH_FEDERATOR_API}", "fromBlock": 22838052 } diff --git a/ops/docs/federator-nodereal-secrets.md b/ops/docs/federator-nodereal-secrets.md new file mode 100644 index 00000000..4516dc99 --- /dev/null +++ b/ops/docs/federator-nodereal-secrets.md @@ -0,0 +1,238 @@ +# Federator NodeReal Secrets + +This runbook describes how to configure private NodeReal RPC keys for federator nodes without committing keys to git. + +The federator configs use these environment variables: + +- `NODEREAL_BSC_FEDERATOR_API` +- `NODEREAL_ETH_FEDERATOR_API` + +The public JSON files contain placeholders such as: + +```json +"host": "https://bsc-mainnet.nodereal.io/v1/${NODEREAL_BSC_FEDERATOR_API}" +``` + +At runtime, `config.js` expands the placeholder from the environment. If the required variable is missing, the federator fails fast instead of falling back to the public overloaded endpoint. + +## Recommended Emergency Setup: Local `.env` + +Docker Compose automatically reads a `.env` file from the same directory where `docker-compose-prod.yml` is executed. + +On each federator node: + +```sh +cd /home/ubuntu/Bridge-SC +``` + +Create or update `.env`: + +```sh +cat > .env <<'EOF' +NODEREAL_BSC_FEDERATOR_API= +NODEREAL_ETH_FEDERATOR_API= +EOF +``` + +Lock down permissions: + +```sh +chmod 600 .env +``` + +Make sure `.env` is not committed: + +```sh +git status --short .env +``` + +If it appears in git status, add it to the local exclude file: + +```sh +echo .env >> .git/info/exclude +``` + +## Restart BSC Federator + +Restart only the federator container: + +```sh +cd /home/ubuntu/Bridge-SC +export FED_ENV=mainnet-BSC-RSK +docker-compose -f docker-compose-prod.yml up -d --force-recreate federation +``` + +Do not use `start.sh` for a simple hot restart unless you intentionally want its full reset behavior. It calls `reset.sh`, which deletes federator DB progress and performs git hard resets. + +## Restart ETH Federator + +```sh +cd /home/ubuntu/Bridge-SC +export FED_ENV=mainnet-ETH-RSK +docker-compose -f docker-compose-prod.yml up -d --force-recreate federation +``` + +## Verify The Container Has The Variables + +Do not print the actual values. Check only whether they are present: + +```sh +docker-compose -f docker-compose-prod.yml exec federation sh -lc \ + 'test -n "$NODEREAL_BSC_FEDERATOR_API" && echo BSC key present || true' +``` + +```sh +docker-compose -f docker-compose-prod.yml exec federation sh -lc \ + 'test -n "$NODEREAL_ETH_FEDERATOR_API" && echo ETH key present || true' +``` + +For a BSC-only node, only the BSC variable needs to be present. For an ETH-only node, only the ETH variable needs to be present. + +## Verify RPC Access From The Host + +BSC: + +```sh +. ./.env + +curl -sS "https://bsc-mainnet.nodereal.io/v1/$NODEREAL_BSC_FEDERATOR_API" \ + -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' | jq + +unset NODEREAL_BSC_FEDERATOR_API NODEREAL_ETH_FEDERATOR_API +``` + +Expected BSC mainnet result: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x38" +} +``` + +ETH: + +```sh +. ./.env + +curl -sS "https://eth-mainnet.nodereal.io/v1/$NODEREAL_ETH_FEDERATOR_API" \ + -H 'Content-Type: application/json' \ + --data '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}' | jq + +unset NODEREAL_BSC_FEDERATOR_API NODEREAL_ETH_FEDERATOR_API +``` + +Expected ETH mainnet result: + +```json +{ + "jsonrpc": "2.0", + "id": 1, + "result": "0x1" +} +``` + +## Check Federator Logs + +```sh +docker-compose -f docker-compose-prod.yml logs --tail=100 -f federation +``` + +The app currently logs the sidechain host as `ETH Host` even when it is BSC. The URL should show the NodeReal domain, not `bsc.sovryn.app` or `eth.sovryn.app`. + +## Optional Hardening: AWS Secrets Manager + +AWS Secrets Manager is useful for long-term operations, auditability, and controlled rotation. It is not required for emergency recovery. + +`start.sh` can load these secrets from AWS Secrets Manager if the environment variables are not already set: + +- Secret name: `NODEREAL_BSC_FEDERATOR_API` +- Secret value: raw BSC NodeReal API key +- Secret name: `NODEREAL_ETH_FEDERATOR_API` +- Secret value: raw ETH NodeReal API key + +Each AWS account that runs federator nodes must create its own secrets and IAM permissions. + +Create secrets: + +```sh +aws secretsmanager create-secret \ + --region us-east-2 \ + --name NODEREAL_BSC_FEDERATOR_API \ + --secret-string '' +``` + +```sh +aws secretsmanager create-secret \ + --region us-east-2 \ + --name NODEREAL_ETH_FEDERATOR_API \ + --secret-string '' +``` + +If the secret already exists, update it: + +```sh +aws secretsmanager put-secret-value \ + --region us-east-2 \ + --secret-id NODEREAL_BSC_FEDERATOR_API \ + --secret-string '' +``` + +```sh +aws secretsmanager put-secret-value \ + --region us-east-2 \ + --secret-id NODEREAL_ETH_FEDERATOR_API \ + --secret-string '' +``` + +Attach this inline policy to the federator EC2 instance role. Replace ``: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "ReadFederatorNodeRealSecrets", + "Effect": "Allow", + "Action": "secretsmanager:GetSecretValue", + "Resource": [ + "arn:aws:secretsmanager:us-east-2::secret:NODEREAL_BSC_FEDERATOR_API-*", + "arn:aws:secretsmanager:us-east-2::secret:NODEREAL_ETH_FEDERATOR_API-*" + ] + } + ] +} +``` + +The trailing `-*` is required because Secrets Manager appends a random suffix to secret ARNs. + +Verify from the instance: + +```sh +aws secretsmanager get-secret-value \ + --region us-east-2 \ + --secret-id NODEREAL_BSC_FEDERATOR_API \ + --query SecretString \ + --output text >/dev/null +``` + +```sh +aws secretsmanager get-secret-value \ + --region us-east-2 \ + --secret-id NODEREAL_ETH_FEDERATOR_API \ + --query SecretString \ + --output text >/dev/null +``` + +Both commands should exit with status `0`. + +## Operational Notes + +- Keep `.env` local to each federator node and never commit it. +- Use `chmod 600 .env`. +- Revoke the old leaked/public NodeReal key after federators are confirmed healthy on the new keys. +- Monitor NodeReal CU usage per key after rollout. +- Restrict NodeReal keys by source IP if NodeReal supports it for the account/plan. +- Longer term, protect `https://bsc.sovryn.app/mainnet` at the proxy layer with rate limits, auth, IP allowlists, or a capped public fallback. diff --git a/start.sh b/start.sh index 508fae4a..0cf6caa1 100755 --- a/start.sh +++ b/start.sh @@ -7,6 +7,29 @@ export FED_ID=$2 # Otherwise the errors can get uncaught and this wastes development time. set -e +load_aws_secret_if_unset() { + ENV_NAME="$1" + DEFAULT_SECRET_ID="$2" + eval CURRENT_VALUE="\${$ENV_NAME:-}" + if [ -n "$CURRENT_VALUE" ] + then + return + fi + + eval SECRET_ID_OVERRIDE="\${${ENV_NAME}_SECRET_ID:-}" + SECRET_ID="${SECRET_ID_OVERRIDE:-$DEFAULT_SECRET_ID}" + + echo "loading $ENV_NAME from AWS Secrets Manager secret: $SECRET_ID" + SECRET_STRING=`aws secretsmanager get-secret-value --secret-id "$SECRET_ID" --region us-east-2 | jq -r .SecretString` + SECRET_VALUE=`printf '%s' "$SECRET_STRING" | jq -r --arg key "$ENV_NAME" 'try (fromjson | .[$key] // empty) catch empty'` + if [ -z "$SECRET_VALUE" ] + then + SECRET_VALUE="$SECRET_STRING" + fi + + export "$ENV_NAME=$SECRET_VALUE" +} + if [ -z "$FED_ENV" ] then echo "ERROR: please choose the federator env config as first cmd arg." @@ -58,9 +81,20 @@ echo using key named: $FED_KEY_NAME cat << EOF > /home/ubuntu/Bridge-SC/federator-env/$FED_ENV/federator.key $FED_KEY EOF + +if [ "$FED_ENV" = "mainnet-BSC-RSK" ] +then + load_aws_secret_if_unset NODEREAL_BSC_FEDERATOR_API NODEREAL_BSC_FEDERATOR_API +fi + +if [ "$FED_ENV" = "mainnet-ETH-RSK" ] +then + load_aws_secret_if_unset NODEREAL_ETH_FEDERATOR_API NODEREAL_ETH_FEDERATOR_API +fi + echo "starting federator please wait... this takes" nohup 2>&1 docker-compose -f docker-compose-prod.yml up > federator.log & sleep 90 echo "federator logs: /home/ubuntu/Bridge-SC/federator.log" rm -rf /home/ubuntu/Bridge-SC/federator-env/$FED_ENV/federator.key -tail -f /home/ubuntu/Bridge-SC/federator.log \ No newline at end of file +tail -f /home/ubuntu/Bridge-SC/federator.log