├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── listener.py ├── requirements.txt ├── static └── social.png └── templates └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | #.idea/ 161 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lsiobase/alpine:3.17 2 | 3 | WORKDIR /app 4 | ADD requirements.txt /app 5 | 6 | RUN \ 7 | apk add --no-cache python3 && \ 8 | python3 -m venv /lsiopy && \ 9 | 10 | # upgrade pip as OS pip is always old 11 | pip install -U --no-cache-dir pip && \ 12 | pip install -U --no-cache-dir -r /app/requirements.txt 13 | 14 | ADD . /app 15 | EXPOSE 5000 16 | 17 | CMD ["gunicorn", "--bind", "0.0.0.0:5000", "listener:app"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2023, Tailscale Community 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tailscale identity headers demo 2 | 3 | This repository is a companion to the Tailscale blog post “[Tapping into Tailscale’s identity headers with Serve](https://tailscale.dev/blog/id-headers-tailscale-serve-flask).” For more background, read that post, and then come back here to look under the hood. You can see the live demo at [https://id-headers-demo.pango-lin.ts.net](https://id-headers-demo.pango-lin.ts.net). 4 | 5 | Note that these identity headers are tied to the underlying Tailscale connection, and so they work a little differently from a cookie-based authentication model. For example, the same headers will be sent when accessing the service within private browsing sessions, from different browsers, or even on the same user's other devices on the tailnet. 6 | 7 | For the purpose of this demo, we’re keeping things simple and the entire Flask app is one function in a file. 8 | 9 | If you want to run your own copy, we recommend starting by cloning this repo and then creating a new virtual environment in its directory and installing Flask. 10 | 11 | ``` 12 | $ git clone git@github.com:tailscale-dev/id-headers-demo.git 13 | $ cd id-headers-demo 14 | $ python -m venv .venv 15 | $ source .venv/bin/activate 16 | $ python -m pip install flask 17 | ``` 18 | 19 | Then run the Flask app from the command line. Use `serve` to make it visible to your tailnet, and (optionally) Tailscale Funnel to open it to the world. 20 | 21 | ``` 22 | $ python listener.py & 23 | $ tailscale serve https / 127.0.0.1:5000 24 | $ tailscale funnel 443 on 25 | ``` 26 | 27 | The Flask server is fine for development, but not advised for production deployment. Our instance of this demo is behind [`gunicorn`](https://gunicorn.org/), but otherwise exactly as described here. 28 | 29 | The listener program will look for two environment variables when it runs, but will operate fine without them. If you want to set those variables: 30 | 31 | - `DEMO_INVITE_LINK` is an invite link URL you can generate from your Tailscale admin console 32 | - `TAILSCALE_URL` is the URL on which the demo is available. We only use it to populate the URL in the social cards of our live demo. 33 | 34 | ## docker 35 | 36 | Recently we added docker support to this image. It can now be deployed using a Linuxserver.io image and their "docker mod" concept which lets you install software at run time - in our case we install Tailscale into the container so that we can run one instance of this demo per event we attend from a single VPS VM. 37 | 38 | More details available in the devrel [infra repo](https://github.com/tailscale-dev/devrel-demo-infra). But an example compose snippet might look like this once you've built the app with `docker build -t : .`: 39 | 40 | ``` 41 | --- 42 | version: "2" 43 | services: 44 | id-demo-cleveland: 45 | image: ghcr.io/tailscale-dev/demo-id-headers:cleveland 46 | container_name: id-demo-cleveland 47 | volumes: 48 | - /mnt/zfs/appdata/2023/cleveland:/var/lib/tailscale 49 | environment: 50 | - PUID=1000 51 | - PGID=1000 52 | - TZ=America/New_York 53 | - DOCKER_MODS=ghcr.io/tailscale-dev/docker-mod:main 54 | - TAILSCALE_STATE_DIR=/var/lib/tailscale 55 | - TAILSCALE_SERVE_MODE=https 56 | - TAILSCALE_SERVE_PORT=5000 57 | - TAILSCALE_USE_SSH=0 58 | - TAILSCALE_AUTHKEY=tskey-auth-1234 59 | - TAILSCALE_HOSTNAME=hello-cleveland 60 | - TAILSCALE_FUNNEL=on 61 | restart: unless-stopped 62 | ``` 63 | 64 | To build for x86 VMs on an Apple Silicon Mac use `docker buildx build --platform linux/amd64 -t ghcr.io/tailscale-dev/demo-id-headers:tagexample .`. 65 | -------------------------------------------------------------------------------- /listener.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask, render_template, request 4 | 5 | app = Flask(__name__) 6 | 7 | @app.route('/') 8 | def greet(): 9 | tailscale_user_name = request.headers.get('Tailscale-User-Name', '') 10 | tailscale_user_login = request.headers.get('Tailscale-User-Login', '') 11 | 12 | if not (tailscale_user_name or tailscale_user_login): 13 | serve_options = {'identified' : False, 14 | 'background_color': '#f2e5bc', 15 | 'color' : '#282828', 16 | 'emoji' : '🤠', 17 | 'greeting' : 'Howdy, internet stranger!', 18 | } 19 | else: 20 | serve_options = {'identified' : True, 21 | 'background_color': '#228B22', 22 | 'color' : '#fff', 23 | 'emoji' : '👯', 24 | 'greeting' : "Now we're friends, "\ 25 | f"{tailscale_user_name} ({tailscale_user_login})!", 26 | } 27 | 28 | serve_options['invite_link'] = os.environ.get('DEMO_INVITE_LINK') 29 | serve_options['tailscale_url'] = os.environ.get('TAILSCALE_URL') 30 | serve_options['headers'] = dict(request.headers) 31 | 32 | return render_template('index.html', **serve_options) 33 | 34 | if __name__ == '__main__': 35 | app.run() 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask==3.0.0 2 | gunicorn==21.2.0 -------------------------------------------------------------------------------- /static/social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tailscale-dev/id-headers-demo/5a7ebc36d7503052ba686c0b8f109261089c7843/static/social.png -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {% if tailscale_url %} 9 | 10 | {% endif %} 11 | 12 | 13 | Tailscale identity headers demo 14 | 15 | 36 | 37 | 38 | 39 |

{{ emoji }}

40 |

{{ greeting }}

41 | 42 | {% if identified %} 43 |

Because you've connected to this machine through Tailscale, your user name and login are included with your requests. This is only the case for traffic flowing over your tailnet. You should see this machine in your Tailscale admin console; if you remove it you'll no longer send identity headers with your requests to it.

44 | 45 | {% elif not identified %} 46 |

This is a demo of Tailscale's identity headers. You can add this machine as a "shared node" to your tailnet and see how it can serve different content based on those headers. It will only be visible to you—not other users on your tailnet—and will not be able to initiate connections to any of your devices.

47 | {% if invite_link %}

To add this node to your tailnet, click this invite link and log in to Tailscale.

{% endif %} 48 | {% endif %} 49 | 50 |
See all incoming headers 51 | 54 |
55 | 56 | 57 | --------------------------------------------------------------------------------