Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aryeh/rpc-auth #36

Merged
merged 58 commits into from
Nov 25, 2020
Merged
Show file tree
Hide file tree
Changes from 47 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
6fee28f
RPC Auth WIP
harryttd Oct 27, 2020
dec7345
WIP
harryttd Oct 27, 2020
20bc5ea
Delete nonce from redis after use
harryttd Oct 27, 2020
1b022e3
WIP Get python server running in k8s and expose through ingress
harryttd Oct 28, 2020
e438a41
Use (experimenting) devspace for hot reloading the python server
harryttd Oct 28, 2020
f65b18f
Validate url access token and passthrough to RPC endpoint
harryttd Oct 28, 2020
fb83b1b
Inject env vars into python server
harryttd Oct 29, 2020
8d2632c
Clean up client logs
harryttd Oct 29, 2020
f0d2f3b
Merge branch 'master' into aryeh/rpc-auth
harryttd Oct 29, 2020
e8687d3
tweaks
harryttd Oct 29, 2020
f1da427
Have devspace enable minikube nginx addon
harryttd Oct 29, 2020
8a604b2
Use host address that the client initiated with in the returned url
harryttd Oct 29, 2020
14b7034
Formatting using black and isort
harryttd Oct 30, 2020
c31ff23
Better naming for tezos rpc service url
harryttd Oct 30, 2020
8c3b8e1
Make /vending-machine http POST only
harryttd Oct 30, 2020
806870a
Remove create_nonce function
harryttd Oct 30, 2020
95ca56e
Use namespace fields in devspace.yaml
harryttd Nov 2, 2020
f7279aa
Update injected value for tezos rpc endpoint
harryttd Nov 2, 2020
8fe6e8c
tweak dockerfile to build smaller image
Nov 2, 2020
f9ce69f
Working nginx auth proxy
harryttd Nov 3, 2020
853cbd7
Better nginx auth configuration
harryttd Nov 4, 2020
f4ed012
Use uuid hex format for access token
harryttd Nov 4, 2020
224dc90
Use python version 3.9 in Dockerfile
harryttd Nov 4, 2020
1734ba1
Use @cache directive for chain id instead of global variable
harryttd Nov 4, 2020
de28627
pip install uwsgi
harryttd Nov 5, 2020
a1534f2
Rebuild docker image on changes + reload on k8s yaml changes
harryttd Nov 6, 2020
9f528dc
Run server in either dev or prod
harryttd Nov 10, 2020
be1272e
Merge branch 'rpc-auth-smaller-docker-image' into aryeh/rpc-auth
harryttd Nov 10, 2020
8a457be
Docker: Always run uwsgi and run it as non-root
harryttd Nov 11, 2020
0a8addc
Fix devspace not syncing files when running container as non-root
harryttd Nov 11, 2020
4b2dfff
Fix FLASK_ENV not being set in Dockerfile
harryttd Nov 12, 2020
cffc113
Store in redis public key hash instead of public key
harryttd Nov 12, 2020
cd78851
Save set of access tokens per tz address
harryttd Nov 12, 2020
6e82a3c
Validate nonce in redis transaction so that it can't be replayed
harryttd Nov 16, 2020
07a6340
Reload uwsgi after syntax error
harryttd Nov 16, 2020
074e7b5
Extract auth token in python instead of nginx
harryttd Nov 16, 2020
b416cb3
Merge branch 'master' into merge-auth-with-mkchain
harryttd Nov 16, 2020
7edf679
WIP merge rpc-auth with mkchain
harryttd Nov 16, 2020
9698c6a
Move lint.sh into scripts/
harryttd Nov 16, 2020
d509484
Merge branch 'master' into merge-auth-with-mkchain
harryttd Nov 16, 2020
ec6cbd8
WIP merge rpc authentication with mkchain
harryttd Nov 17, 2020
9ea611d
Don't expose tezos node RPC over zerotier
harryttd Nov 17, 2020
a5b8c4c
gitignore generated chain yaml files
harryttd Nov 17, 2020
d500c4a
isort + black
harryttd Nov 17, 2020
5357394
Use ip instead of localhost alias for rpc listening address
harryttd Nov 17, 2020
32c098f
Only run uwsgi touchreload in development
harryttd Nov 18, 2020
635b952
Run rpc-auth by default in prod
harryttd Nov 18, 2020
64e2de2
Merge branch 'master' into aryeh/rpc-auth
harryttd Nov 19, 2020
7fca9ae
Configure redis persistance
harryttd Nov 19, 2020
eec82f1
WIP
harryttd Nov 20, 2020
22c25d6
Configure redis persistance
harryttd Nov 19, 2020
3a0a70f
fix for devspace
harryttd Nov 20, 2020
e1361e8
Rename rpc-auth-service to just rpc-auth
harryttd Nov 20, 2020
c9a2ef8
Add FLASK_ENV var to devspace
harryttd Nov 20, 2020
38dde33
Properly handle falsey chain args
harryttd Nov 20, 2020
97d618a
Merge branch 'aryeh/rpc-auth' into devspace
harryttd Nov 23, 2020
3ca2c86
Devspace hook to increase fs.inotify.max_user_watches
harryttd Nov 24, 2020
bf55629
Merge branch 'master' into aryeh/rpc-auth
harryttd Nov 24, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ dist
*.pyc
*.egg-info
.venv*
.DS_Store
build
.devspace/
mkchain-devspace.yaml
/*_chain.yaml
/*_chain_invite.yaml
22 changes: 20 additions & 2 deletions MULTICLUSTER.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ peer-to-peer network. This tutorial assumes the use of Minikube.
* a ZeroTier network and api access token
* jq

## Installing prerequisites
## Installing prerequisites

This section varies depending on OS.

Expand All @@ -34,7 +34,7 @@ brew install jq
brew install minikube
```

### Arch Linux
### Arch Linux

```shell
pacman -Syu && pacman -S python3 jq minikube kubectl kubectx linux
Expand Down Expand Up @@ -162,3 +162,21 @@ Check that the nodes have matching heads by comparing their hashes:
``` shell
kubectl -n tqtezos exec deployment/tezos-node -c tezos-node -- /usr/local/bin/tezos-client rpc get /chains/main/blocks/head/hash
```

## RPC Authentication
You can optionally spin up an RPC authentication server allowing clients with your given permission to make RPC calls:

```shell
mkchain create $CHAIN_NAME --rpc-auth ...
```

### Current authentication flow
The client authenticates themselves and will receive a secret url that allows them to make RPC calls.
- You provide a trusted client with your cluster ip/address and your private tezos chain id.
- client runs: `scripts/rpc-auth-client.sh --cluster-address $CLUSTER_IP --tz-alias $TZ_ALIAS --chain-id $CHAIN_ID`
- TZ_ALIAS is the alias of a client's tz address secret key. The client's secret key is used to sign some data for the server to then verify.
- If the client is authenticated, the response should contain a secret url such as `http://192.168.64.51/tezos-node-rpc/ffff3eb3d7dd4f6bbff3f2fd096722ae/`
- Client can then make RPC requests:
- `curl http://192.168.64.51/tezos-node-rpc/ffff3eb3d7dd4f6bbff3f2fd096722ae/chains/main/chain_id`
- Bug in tezos client v8, so as of version `tezos/tezos:master_08d3405e_20201113152010`:
- `tezos-client --endpoint http://192.168.64.51/tezos-node-rpc/ffff3eb3d7dd4f6bbff3f2fd096722ae/ rpc get chains/main/chain_id`
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please also add a paragraph in DEVELOPMENT.md explaining how to spin up rpc-auth on minikube with devspace.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ You can modify these parameters by:
| timestamp | --timestamp | timestamp for the chain to join | |
| protocol_hash | --protocol-hash | Desired Tezos protocol hash | PsCARTHAGazKbHtnKfLzQg3kms52kSRpgnDY982a9oYsSXRLQEb |
| baker_command | --baker-command | The baker command to use, including protocol | tezos-baker-006-PsCARTHA |
| rpc_auth | --rpc-auth | Include an RPC authentication server | |

## private chain

Expand Down
39 changes: 37 additions & 2 deletions devspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,44 @@ deployments:
kubectl:
manifests:
- ./mkchain-devspace.yaml
- name: rpc-auth
namespace: tqtezos
kubectl:
manifests:
- ./tqchain/deployment/rpc-auth.yaml
images:
zerotier:
image: tezos-zerotier
dockerfile: ./docker/zerotier/Dockerfile
context: ./docker/zerotier
rpc-auth:
image: rpc-auth
dockerfile: ./docker/rpc-auth/Dockerfile
context: ./docker/rpc-auth
build:
docker:
options:
buildArgs:
FLASK_ENV: ${FLASK_ENV}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this variable also needs to be declared in vars: section below with source: env and a default value otherwise devspace build prompts for the value.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasochem I just tried with no prompt:

echo $FLASK_ENV

❯ devspace build --skip-push -b
[done] √ Done building image tezos-rpc-auth:qchX3Uf (rpc-auth)
[done] √ Done building image tezos-zerotier:KkAWwY3 (zerotier)
[done] √ Successfully built 2 images

How can I reproduce?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasochem yes, removing .devspace prompted me. thanks

dev:
logs:
images:
- rpc-auth
sync:
- imageName: zerotier
- imageName: rpc-auth
namespace: tqtezos
labelSelector:
app: tezos-bootstrap-node
app: rpc-auth
localSubPath: ./docker/rpc-auth/server
autoReload:
images:
- rpc-auth
- zerotier
deployments:
- rpc-auth
- chain
paths:
- ./tqchain/deployment/*
hooks:
- command: sh
args:
Expand All @@ -24,6 +51,14 @@ hooks:
when:
before:
deployments: all
- command: minikube
args:
- addons
- enable
- ingress
when:
before:
deployments: rpc-auth
vars:
- name: CHAIN_NAME
question: Name of the chain as passed to generate-constants
Expand Down
66 changes: 66 additions & 0 deletions docker/rpc-auth/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
FROM python:3.9-slim as builder

RUN mkdir -p /var/rpc-auth/

WORKDIR /var/rpc-auth/

# Installing pytezos deps
RUN apt-get update -y \
&& apt-get install --no-install-recommends -y \
automake \
build-essential \
libffi-dev \
libgmp-dev \
libsecp256k1-dev \
libsodium-dev \
libtool \
pkg-config \
&& echo

COPY requirements.txt .
RUN mkdir wheels \
&& pip wheel -r requirements.txt \
--wheel-dir ./wheels --no-cache-dir

FROM python:3.9-slim AS src

WORKDIR /var/rpc-auth/

# Installing pytezos deps
RUN apt-get update -y \
&& apt-get install --no-install-recommends -y \
libffi-dev \
libgmp-dev \
libsecp256k1-dev \
libsodium-dev \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*

# Installing python dependencies
COPY --from=builder /var/rpc-auth/wheels wheels
COPY requirements.txt .
RUN pip install -r requirements.txt \
--no-index --find-links ./wheels \
&& rm -rf ./wheels ./requirements.txt

RUN groupadd -g 999 appuser && \
useradd -r -u 999 -g appuser appuser

COPY --chown=appuser:appuser ./server/index.py .

ARG FLASK_ENV=production
ENV FLASK_ENV=$FLASK_ENV
ENV PYTHONUNBUFFERED=x

EXPOSE 8080

USER appuser

CMD uwsgi \
--http-socket 0.0.0.0:8080 \
--callable app \
--threads 100 \
--processes 1 \
--wsgi-file index.py \
--worker-reload-mercy 0 \
$(if [ "${FLASK_ENV}" = "development" ] ; then echo "--touch-reload index.py" ; else : ; fi)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that useful considering that devspace will rebuild and restart the container when something changes?

Copy link

@itkach itkach Nov 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nicolasochem looks like devspace is set up to sync files for rpc-auth rather than rebuild image, so this makes sense

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is true

35 changes: 35 additions & 0 deletions docker/rpc-auth/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
base58==1.0.3
bson==0.5.10
certifi==2020.6.20
cffi==1.14.3
chardet==3.0.4
click==7.1.2
fastecdsa==1.7.5
fire==0.3.1
Flask==1.1.2
idna==2.10
itsdangerous==1.1.0
Jinja2==2.11.2
loguru==0.5.3
MarkupSafe==1.1.1
mnemonic==0.19
netstruct==1.1.2
pendulum==2.1.2
ply==3.11
pyblake2==1.1.2
pycparser==2.20
pysodium==0.7.5
pytezos==2.5.11
python-dateutil==2.8.1
pytzdata==2020.1
PyYAML==5.3.1
redis==3.5.3
requests==2.24.0
secp256k1==0.13.2
simplejson==3.17.2
six==1.15.0
termcolor==1.1.0
tqdm==4.51.0
urllib3==1.25.11
uWSGI==2.0.19.1
Werkzeug==1.0.1
167 changes: 167 additions & 0 deletions docker/rpc-auth/server/index.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import os
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

putting rpc-auth under docker directory makes no sense to me; rpc-auth is a distinct component, just like mkchain. If we are not moving it out to a separate repo then rpc-auth and mkchain need to be sibling subdirs in the project, each with its own setup.py. Right now mkchain is "dominant" with its setup.py sitting in project root and rpc-auth is shoved under the rug in a weird place.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a container centric view of the environment. mkchain does not belong here because it's not part of the cluster. It's a helper script to generate helm chart values.

See also: https://github.com/midl-dev/tezos-on-gke/tree/master/docker

One of the things I am planning is to migrate generateTezosConfig.py and import_keys.sh into init containers in the docker folder.

Perhaps mkchain could move to the scripts folder that has been added in this PR?

Copy link

@itkach itkach Nov 20, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mkchain is a software component, so is rpc-auth. They have their own packaging, dependencies, documentation, possible usage and deployment scenarios. I would really like to see component based structure here, perhaps even force it via putting them in separate repos (monorepo can work too, but not like this). Containers flow from that, not the other way around.

import re
import time
from functools import cache
from urllib.parse import urljoin
from uuid import uuid4

import requests
from flask import Flask, abort, request
from pytezos.crypto import Key
from redis import StrictRedis, WatchError

TEZOS_RPC_SERVICE_URL = (
f"http://{os.getenv('TEZOS_RPC_SERVICE')}:{os.getenv('TEZOS_RPC_SERVICE_PORT')}"
)

app = Flask(__name__)
redis = StrictRedis(host=os.getenv("REDIS_HOST"), port=os.getenv("REDIS_PORT"))

## ROUTES


@app.route("/vending-machine/<chain_id>")
def get_nonce(chain_id):
try:
is_correct_chain_id = verify_chain_id(chain_id)
if not is_correct_chain_id:
abort(401)
except requests.exceptions.RequestException as e:
print("Failed to verify chain id.", e)
abort(500)

# Tezos client requires the data to be signed in hex format
nonce = uuid4().hex
redis.set(nonce, "", ex=3)
return nonce


@app.route("/vending-machine", methods=["POST"])
def generate_tezos_rpc_url():
try:
nonce, signature, public_key = [
request.values[k] for k in ("nonce", "signature", "public_key")
]
except KeyError as e:
print("Request data:", request.values)
print(e)
abort(400)

if not is_valid_nonce(nonce):
abort(401)

tezos_key_object = get_tezos_key_object(public_key)
if not is_valid_signature(tezos_key_object, signature, nonce):
abort(401)

access_token = uuid4().hex
secret_url = create_secret_url(access_token)
save_access_token(tezos_key_object.public_key_hash(), access_token)
return secret_url


@app.route("/auth")
def rpc_auth():
access_token = extract_access_token(request.headers)
if not is_valid_access_token(access_token):
abort(401)
return "OK", 200


## HELPER FUNCTIONS


def verify_chain_id(chain_id):
if chain_id != get_tezos_chain_id():
return False
return True


@cache
def get_tezos_chain_id():
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is extremely unlikely that a chain is restarted (i guess from scratch), and so the chain id would change, right?

Copy link

@itkach itkach Nov 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's a meaningful scenario where chain id would change but this service would not be updated/restarted

tezos_chain_id = os.getenv("TEZOS_CHAIN_ID")
if tezos_chain_id:
return tezos_chain_id
chain_id_response = requests.get(
urljoin(TEZOS_RPC_SERVICE_URL, "chains/main/chain_id")
)
return chain_id_response.text.strip('\n"')


def is_valid_nonce(nonce):
with redis.pipeline() as pipeline:
try:
pipeline.watch(nonce)
redis_nonce = pipeline.get(nonce)
pipeline.multi()
pipeline.delete(nonce)
pipeline.execute()
if redis_nonce != None:
return True
except WatchError:
print("Nonce was already validated.")

return False


def get_tezos_key_object(public_key):
try:
return Key.from_encoded_key(public_key)
except ValueError as e:
print("Something is wrong with the public_key provided:", e)
abort(401)


def is_valid_signature(key_object, signature, nonce):
try:
bytes_prefix = "0x05"
key_object.verify(signature, bytes_prefix + nonce)
return True
except ValueError as e:
print("Error verifying signature:", e)
return False


def create_secret_url(access_token):
return urljoin(request.url_root, f"tezos-node-rpc/{access_token}")


def create_redis_access_token_key(access_token, hash=False):
return f"access_token{':hash' if hash else ''}:{access_token}"


def save_access_token(tz_address, access_token):
access_token_key = create_redis_access_token_key(access_token)
with redis.pipeline() as pipeline:
# Create redis hash of access token with timestamp and tz address
pipeline.hset(
access_token_key, mapping={"timestamp": time.time(), "address": tz_address}
)
# Add access token to list of this tz address's tokens
pipeline.sadd(tz_address, access_token_key)
pipeline.execute()


def extract_access_token(headers):
original_url = headers.get("X-Original-Url")
regex_obj = re.search(r"tezos-node-rpc/(.*?)/", original_url)
if regex_obj:
return regex_obj.group(1)


def is_valid_access_token(access_token):
if (
access_token
and len(access_token) == 32 # Should be 32 char hex string
and redis.exists(create_redis_access_token_key(access_token)) == 1
):
return True
return False


if __name__ == "__main__":
app.run(
host="0.0.0.0",
port=8080,
debug=(True if os.getenv("FLASK_ENV") == "development" else False),
)
Loading