From 75dd0f72cfe923e34820834a3c9d545fdc0785c3 Mon Sep 17 00:00:00 2001 From: gcaracuel <633810+gcaracuel@users.noreply.github.com> Date: Wed, 15 Jul 2026 13:00:44 +0200 Subject: [PATCH] chore: CI to build and host docker image --- .github/workflows/ci.yml | 76 ++++++++++++++++++++++++ README.md | 124 +++++++++++++++++++++++++++++++++++---- RemoteSigner/Dockerfile | 6 +- 3 files changed, 192 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b77949e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: CI + +on: + pull_request: + branches: [develop] + push: + branches: [develop] + tags: ["v*"] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + test: + name: Run tests + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: "10.0.x" + + - name: Restore dependencies + run: dotnet restore "NodeGuard Remote Signer.sln" + + - name: Build + run: dotnet build "NodeGuard Remote Signer.sln" --configuration Release --no-restore + + - name: Test + run: dotnet test "NodeGuard Remote Signer.sln" --configuration Release --no-build --verbosity normal + + build: + name: Build Docker image + needs: [test] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=raw,value=latest,enable=${{ github.ref_name == 'develop' }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./RemoteSigner + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/README.md b/README.md index 6239e7e..fdcc53d 100644 --- a/README.md +++ b/README.md @@ -1,22 +1,28 @@ # NodeGuard Remote Signer -AWS Lambda function in dotnet to allow the remote signing of transactions in NodeGuard + +AWS Lambda function in dotnet to allow the remote signing of transactions in NodeGuard. ## Trusted coordinator signing + The NodeGuard has two modes of signing transactions as as Trusted coordinator (it is mandatory for security reasons, since the FM relies on SIGHASH_NONE inputs from finance managers): + 1. Embedded (legacy) signing withing the NodeGuard, less secure but easier to manage 2. Use a remote signing function (AWS Lambda function with public URL) to sign with a AWS KMS encrypted seedphrase that only the function can decrypt. Bear in mind that the AWS KMS symmetric encryption private key is never exposed and it is managed by AWS KMS. To enable mode #1 set env var as follows `ENABLE_REMOTE_SIGNER = false`, otherwise in case you want mode #2 you need a lambda function deployed with this project with a public function URL and a AWS KMS Symmetric encryption key. Then set up the following env vars as in this example (json-like): + ``` "ENABLE_REMOTE_SIGNER": "true", "AWS_ACCESS_KEY_ID": "********", "AWS_SECRET_ACCESS_KEY": "********", "AWS_REGION": "eu-west-1", -"AWS_KMS_KEY_ID": "mrk-cec3e3ef59bc4616a6f44da60bfea0ba", +"AWS_KMS_KEY_ID": "mrk-cec3e3ef59bc4616a6f44da60bfea0ba", "REMOTE_SIGNER_ENDPOINT": "https://*.lambda-url.eu-west-1.on.aws/" ``` + They are detailed as follows: -- AWS_ACCESS_KEY_ID: IAM-based user account id used to auth against AWS lambda + +- AWS_ACCESS_KEY_ID: IAM-based user account id used to auth against AWS lambda - AWS_SECRET_ACCESS_KEY: IAM-based user secret - AWS_REGION: the region code of the AWS deployed lambda function - REMOTE_SIGNER_ENDPOINT: AWS Function url endpoint, check https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html @@ -25,8 +31,8 @@ They are detailed as follows: The key aspect when invoking the lambda function is to use the AWS API Gateway format for payloads, check [AWS Function URL invocation basics](https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html). - Example input to AWS lambda function: + ```json { "Version": null, @@ -43,7 +49,9 @@ Example input to AWS lambda function: "StageVariables": null } ``` + Request input body fields: + - Psbt: The base64-encoded PSBT to sign - EnforcedSighash: used to enforce a SIGHASH mode signing for all the inputs, 1 means SIGHASH_ALL, check NBitcoin enums to match other sighash types. - Network: Bitcoin network, this is case-insensitive as long the values are either one of the following `Mainnet, Regtest, Testnet` @@ -59,21 +67,26 @@ Example output from the lambda function: ``` Request output body fields: + - Psbt: The base64-encoded signed PSBT ### Encrypted Seedphrase generation -Right now, the easiest way to encrypt a wallet seedphrase (AKA Mnemomnic) is to use the function `EncryptSeedphrase` in the `Function.cs` class in the Remote signer by invoking a unit test to generate an encrypted seedphrase which is in the `FunctionTest.cs` named `GenerateEncryptedSeedTest`. Take into account that you must use [AWS SDK Credentials for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html) to call AWS KMS. + +Right now, the easiest way to encrypt a wallet seedphrase (AKA Mnemomnic) is to use the function `EncryptSeedphrase` in the `Function.cs` class in the Remote signer by invoking a unit test to generate an encrypted seedphrase which is in the `FunctionTest.cs` named `GenerateEncryptedSeedTest`. Take into account that you must use [AWS SDK Credentials for .NET](https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/net-dg-config-creds.html) to call AWS KMS. ### Setting the function main config + The lambda function uses environment variables as a key-value dictionary for configuration of the different wallets that can be used to sign, the dictionary keys are the master fingerprints of the different wallets while the value of the keys are the configuration of the lambda function. The environment variable key must start with a prefix as `MF_{Master Fingerprint}` (e.g. MF_ed0210c8) The configuration has two fields: -* EncryptedSeedphrase: The encrypted seedphrase as explained [above](#encrypted-seedphrase-generation) -* AwsKmsKeyId: Symmetric key generated by AWS KMS which decrypts the seedphrase + +- EncryptedSeedphrase: The encrypted seedphrase as explained above +- AwsKmsKeyId: Symmetric key generated by AWS KMS which decrypts the seedphrase Example (json-like structure of key-value, `ed0210c8` is the master fingerprint of the wallet): + ```json { "MF_ed0210c8": { @@ -82,14 +95,103 @@ Example (json-like structure of key-value, `ed0210c8` is the master fingerprint }} ``` +## CI pipeline + +The project uses GitHub Actions for continuous integration. The workflow is defined in `.github/workflows/ci.yml` and triggers on three events: + +### Pull requests to develop + +When a pull request targets the `develop` branch, the pipeline: + +1. Runs the .NET test suite (restore, build, test) +2. Builds the Docker image (no push) + +This validates that both the code and the Docker build are correct before merging. + +### Push to develop + +When code is merged into `develop`, the pipeline: + +1. Runs the .NET test suite +2. Builds the Docker image +3. Pushes the image to GHCR with two tags: + - `develop` (branch name) + - `latest` + +### Tag push (releases) + +When a tag matching `v*` is pushed (e.g. `v1.2.3`), the pipeline: + +1. Runs the .NET test suite +2. Builds the Docker image +3. Pushes the image to GHCR with the tag name as the Docker tag (e.g. `v1.2.3`) + +### Workflow diagram + +``` +Pull request (develop) Push to develop Push tag v* + | | | + run tests run tests run tests + | | | + build image build image build image + | | | + (no push) push to GHCR push to GHCR + tags: latest, develop tag: +``` + +## Using the image + +### Pull from GHCR + +```bash +docker pull ghcr.io/elenpay/nodeguard-remote-signer:latest +docker pull ghcr.io/elenpay/nodeguard-remote-signer:develop +docker pull ghcr.io/elenpay/nodeguard-remote-signer:v1.2.3 +``` + +### Push to ECR for Lambda deployment + +The image is consumed by an AWS Lambda function. To deploy it to your own AWS account, pull from GHCR, tag for ECR, and push. + +Replace the account ID and region with your own values. + +```bash +# Authenticate with your ECR registry +aws ecr get-login-password --region | \ + docker login --username AWS --password-stdin .dkr.ecr..amazonaws.com + +# Create the repository if it does not exist +aws ecr create-repository --repository-name nodeguardremotesigner --region + +# Pull from GHCR +docker pull ghcr.io/elenpay/nodeguard-remote-signer:latest + +# Tag for ECR +docker tag ghcr.io/elenpay/nodeguard-remote-signer:latest \ + .dkr.ecr..amazonaws.com/nodeguardremotesigner:latest + +# Push to ECR +docker push .dkr.ecr..amazonaws.com/nodeguardremotesigner:latest + +# Update the Lambda function +aws lambda update-function-code \ + --function-name arn:aws:lambda:::function:SignPSBT- \ + --image-uri .dkr.ecr..amazonaws.com/nodeguardremotesigner:latest \ + --publish +``` + +Replace ``, ``, and `` (stg or prod) with your actual values. + ## Deploying the lambda function ### Requirements -* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-install.html) -* [JUST](https://github.com/casey/just) -* Access to the AWS account where the lambda function will be deployed + +- AWS CLI +- Access to the AWS account where the lambda function will be deployed ### Deploy + ```bash -just deploy +# Pull the image from GHCR, tag for ECR, push, and update the Lambda function +# See instructions above for the full sequence ``` diff --git a/RemoteSigner/Dockerfile b/RemoteSigner/Dockerfile index ec41fef..5f81a73 100644 --- a/RemoteSigner/Dockerfile +++ b/RemoteSigner/Dockerfile @@ -11,11 +11,11 @@ RUN dotnet build "RemoteSigner.csproj" --configuration Release --output /app/bui FROM build AS publish RUN dotnet publish "RemoteSigner.csproj" \ - --configuration Release \ + --configuration Release \ --runtime linux-x64 \ - --self-contained false \ + --self-contained false \ --output /app/publish \ - -p:PublishReadyToRun=true + -p:PublishReadyToRun=true FROM base AS final WORKDIR /var/task