├── CONTRIBUTING.md ├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── 03_documentation.md │ ├── 04_deployment-problem.md │ ├── 05_anything-else.md │ ├── 02_feature-request.md │ └── 01_bug-report.md ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md └── SUPPORT.md ├── docker-res ├── scripts │ └── clean-layer.sh ├── start_ssh.py └── ssh │ ├── authorize.sh │ ├── sshd_config │ └── update_authorized_keys.py ├── CODE_OF_CONDUCT.md ├── Dockerfile ├── README.md └── LICENSE /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | TBD -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !docker-res/ -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/03_documentation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4DA Documentation" 3 | about: Is there a mistake in the docs, is anything unclear or do you have a suggestion? 4 | title: '' 5 | labels: enhancement, docs 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe your request:** 11 | 12 | 13 | 14 | **Which page or section is this issue related to?** 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | **Describe the issue:** 8 | 9 | 10 | 11 | **Technical details:** 12 | 13 | - Image version : 14 | - Docker version : 15 | - Host Machine OS (Windows/Linux/Mac): 16 | - Command used to start the ssh-proxy : -------------------------------------------------------------------------------- /docker-res/scripts/clean-layer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This scripts should be called at the end of each RUN command 4 | # in the Dockerfiles. 5 | # 6 | # Each RUN command creates a new layer that is stored separately. 7 | # At the end of each command, we should ensure we clean up downloaded 8 | # archives and source files used to produce binary to reduce the size 9 | # of the layer. 10 | set -e 11 | set -x 12 | 13 | # Delete old downloaded archive files 14 | apt-get autoremove -y 15 | # Delete downloaded archive files 16 | apt-get clean 17 | # Delete source files used for building binaries 18 | rm -rf /usr/local/src/* 19 | # Delete cache and temp folders 20 | rm -rf /tmp/* /var/tmp/* /root/.cache/* /var/cache/apt/* 21 | # Remove apt lists 22 | rm -rf /var/lib/apt/lists/* /etc/apt/sources.list.d/* 23 | 24 | # Clean npm 25 | if [ -x "$(command -v npm)" ]; then 26 | npm cache clean --force 27 | rm -rf /root/.npm/* /root/.node-gyp/* 28 | fi 29 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | **What kind of change does this PR introduce?** 6 | 7 | 8 | - [ ] Bugfix 9 | - [ ] New Feature 10 | - [ ] Feature Improvment 11 | - [ ] Refactoring 12 | - [ ] Documentation 13 | - [ ] Other, please describe: 14 | 15 | **Description:** 16 | 17 | 18 | **Checklist:** 19 | 21 | 22 | - [ ] I have read the [CONTRIBUTING](https://github.com/ml-tooling/ssh-proxy/blob/develop/CONTRIBUTING.md) document. 23 | - [ ] My changes don't require a change to the documentation, or if they do, I've added all required information. -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/04_deployment-problem.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F433 Deployment Problem" 3 | about: Do you have problems with deployment, and none of the suggestions in the docs 4 | and other issues helped? 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | 16 | 17 | **Describe the issue:** 18 | 19 | 20 | 21 | **Technical details:** 22 | 23 | - Image version : 24 | - Docker version : 25 | - Host Machine OS (Windows/Linux/Mac): 26 | - Command used to start the ssh-proxy : 27 | - -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/05_anything-else.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F4AC Anything else?" 3 | about: For general usage questions, please consider posting on Stack Overflow or 4 | Gitter instead. 5 | title: '' 6 | labels: '' 7 | assignees: '' 8 | 9 | --- 10 | 11 | 16 | 17 | **Describe the issue:** 18 | 19 | 20 | 21 | **Technical details:** 22 | 23 | - Image version : 24 | - Docker version : 25 | - Host Machine OS (Windows/Linux/Mac): 26 | - Command used to start the ssh-proxy : 27 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/02_feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F381 Feature request" 3 | about: Do you have an idea for an improvement or a new feature? 4 | title: '' 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | 15 | 16 | **Feature description:** 17 | 18 | 23 | 24 | **Problem and motivation:** 25 | 26 | 29 | 30 | **Is this something you're interested in working on?** 31 | 32 | 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/01_bug-report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F6A8 Bug report" 3 | about: Did you come across a bug or unexpected behaviour differing from the docs? 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 17 | 18 | **Describe the bug:** 19 | 20 | 21 | 22 | **Expected behaviour:** 23 | 24 | 25 | 26 | **Steps to reproduce the issue:** 27 | 28 | 29 | 30 | 36 | 37 | **Technical details:** 38 | 39 | - Image version : 40 | - Docker version : 41 | - Host Machine OS (Windows/Linux/Mac): 42 | - Command used to start the ssh-proxy : 43 | 44 | **Possible Fix:** 45 | 46 | 47 | 48 | **Additional context:** 49 | 50 | 51 | -------------------------------------------------------------------------------- /docker-res/start_ssh.py: -------------------------------------------------------------------------------- 1 | """ 2 | Container init script 3 | """ 4 | #!/usr/bin/python 5 | 6 | from subprocess import call, Popen 7 | import os 8 | import time 9 | 10 | # Start the SSH server that will serve as a jump host to the Workspaces to allow a user to ssh into a target 11 | ENV_NAME_PERMIT_TARGET_HOST = "SSH_PERMIT_TARGET_HOST" 12 | ENV_SSH_PERMIT_TARGET_HOST = os.getenv(ENV_NAME_PERMIT_TARGET_HOST, "") 13 | 14 | ENV_NAME_MANUAL_AUTH_FILE = "MANUAL_AUTH_FILE" 15 | ENV_MANUAL_AUTH_FILE = os.getenv(ENV_NAME_MANUAL_AUTH_FILE, "false") 16 | 17 | ENV_NAME_SSH_TARGET_LABELS = "SSH_TARGET_LABELS" 18 | ENV_SSH_TARGET_LABELS = os.getenv(ENV_NAME_SSH_TARGET_LABELS, "") 19 | 20 | if ENV_SSH_PERMIT_TARGET_HOST == "": 21 | print("The environment variable {} must be set.".format(ENV_NAME_PERMIT_TARGET_HOST)) 22 | exit(1) 23 | 24 | ENV_NAME_PERMIT_TARGET_PORT = "SSH_PERMIT_TARGET_PORT" 25 | ENV_SSH_PERMIT_TARGET_PORT = os.getenv(ENV_NAME_PERMIT_TARGET_PORT, "22") 26 | SSHD_CONFIG = "/etc/ssh/sshd_config" 27 | 28 | # replace the PermitOpen directive placeholders in the sshd_config 29 | call("sed -i 's/{" + ENV_NAME_PERMIT_TARGET_HOST + "}/" + ENV_SSH_PERMIT_TARGET_HOST + "/g' " + SSHD_CONFIG, shell=True) 30 | call("sed -i 's/{" + ENV_NAME_PERMIT_TARGET_PORT + "}/" + ENV_SSH_PERMIT_TARGET_PORT + "/g' " + SSHD_CONFIG, shell=True) 31 | 32 | # export environment variables to a file which sshd can read to preserve their values in the ssh session 33 | call("echo 'export {}={}' >> {}".format(ENV_NAME_PERMIT_TARGET_HOST, ENV_SSH_PERMIT_TARGET_HOST, os.getenv("SSHD_ENVIRONMENT_VARIABLES")), shell=True) 34 | call("echo 'export {}={}' >> {}".format(ENV_NAME_MANUAL_AUTH_FILE, ENV_MANUAL_AUTH_FILE, os.getenv("SSHD_ENVIRONMENT_VARIABLES")), shell=True) 35 | call("echo 'export {}={}' >> {}".format(ENV_NAME_SSH_TARGET_LABELS, ENV_SSH_TARGET_LABELS, os.getenv("SSHD_ENVIRONMENT_VARIABLES")), shell=True) 36 | 37 | call("/usr/local/sbin/sshd -D -f " + SSHD_CONFIG, shell=True) 38 | -------------------------------------------------------------------------------- /docker-res/ssh/authorize.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Command to be called by the OpenSSH Server AuthorizedKeysCommand to check whether a valid Runtime container exists 3 | 4 | public_key=$1 5 | echo "$(date) $public_key" >> /etc/ssh/access.log 6 | 7 | # If MANUAL_AUTH_FILE is true, another mechanism is supposed to fill the /etc/ssh/authorized_keys_cache file (e.g. a mounted file) 8 | if [ {MANUAL_AUTH_FILE} = true ]; then 9 | cat /etc/ssh/authorized_keys_cache; 10 | exit 11 | fi 12 | 13 | # Read environment variables from file to access general environment variables 14 | # This script is run in an SSH session and, thus, the environment variables do not exist otherwise 15 | source $SSHD_ENVIRONMENT_VARIABLES 16 | 17 | CACHE_TIME=$((60 * 15)) 18 | #[ ! -f /etc/ssh/authorized_keys_cache ] && python /etc/ssh/update_authorized_keys.py 19 | current_date=$(date +%s) 20 | cache_last_modified=$(date -r /etc/ssh/authorized_keys_cache +%s) 21 | time_difference=$(($current_date - $cache_last_modified)) 22 | 23 | # check if the key is in the authorized_keys_cache file. 24 | # If not, perform a synchronous update (the user has to wait) and return the content then. 25 | # Use still a shorter chache validation time to prevent brute-force try (when a user offers a key that does not match 26 | # any runtime key then the test will always fail and a costly key update is triggered) 27 | # Check for " $public_key " with spaces before and after to prevent matching parts of a key 28 | SHORTER_CACHE_TIME=$((60 * 2)) 29 | if [ ! -f etc/ssh/authorized_keys_cache ]; then 30 | python /etc/ssh/update_authorized_keys.py full 31 | elif ! grep -q " $public_key " /etc/ssh/authorized_keys_cache; then 32 | if [ $time_difference -ge $SHORTER_CACHE_TIME ]; then 33 | python /etc/ssh/update_authorized_keys.py 34 | fi 35 | elif [ $time_difference -ge $CACHE_TIME ]; then 36 | # Run the python script to update the cache in the background so the command can directly return 37 | nohup python /etc/ssh/update_authorized_keys.py full > /dev/null 2>&1 & 38 | fi 39 | 40 | # Return the collected keys to the OpenSSH server 41 | cat /etc/ssh/authorized_keys_cache 42 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | The SSH Proxy project is maintained by [@raethlein](https://twitter.com/raethlein) and [@LukasMasuch](https://twitter.com/LukasMasuch). Please understand that we won't be able 4 | to provide individual support via email. We also believe that help is much more 5 | valuable if it's shared publicly so that more people can benefit from it. 6 | 7 | | Type | Channel | 8 | | ------------------------ | ------------------------------------------------------ | 9 | | 🚨 **Bug Reports** | | 10 | | 🎁 **Feature Requests** | | 11 | | 👩‍💻 **Usage Questions** | | 12 | | 🗯 **General Discussion** | | 13 | 14 | ## Contribution 15 | 16 | - Pull requests are encouraged and always welcome. Read [`CONTRIBUTING.md`](https://github.com/ml-tooling/ssh-proxy/tree/master/CONTRIBUTING.md) and check out [help-wanted](https://github.com/ml-tooling/ssh-proxy/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A"help+wanted"+sort%3Areactions-%2B1-desc+) issues. 17 | - Submit github issues for any [feature enhancements](https://github.com/ml-tooling/ml-workspace/issues/new?assignees=&labels=feature-request&template=02_feature-request.md&title=), [bugs](https://github.com/ml-tooling/ssh-proxy/issues/new?assignees=&labels=bug&template=01_bug-report.md&title=), or [documentation](https://github.com/ml-tooling/ssh-proxy/issues/new?assignees=&labels=enhancement%2C+docs&template=03_documentation.md&title=) problems. 18 | - By participating in this project you agree to abide by its [Code of Conduct](https://github.com/ml-tooling/ssh-proxy/tree/master/CODE_OF_CONDUCT.md). -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at mltooling.team@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | # Basics 4 | ENV _RESOURCES_PATH="/resources" 5 | 6 | # Layer cleanup script 7 | COPY docker-res/scripts/clean-layer.sh /usr/bin/clean-layer.sh 8 | 9 | RUN \ 10 | mkdir $_RESOURCES_PATH && \ 11 | chmod ug+rwx $_RESOURCES_PATH && \ 12 | chmod a+rwx /usr/bin/clean-layer.sh 13 | 14 | RUN \ 15 | apt-get update && \ 16 | apt-get install -y \ 17 | wget \ 18 | python3 \ 19 | python3-pip && \ 20 | ln -s /usr/bin/pip3 /usr/bin/pip && \ 21 | ln -s /usr/bin/python3 /usr/bin/python && \ 22 | # Cleanup 23 | clean-layer.sh 24 | 25 | # SSH Server 26 | ## Install & Prepare SSH 27 | RUN \ 28 | mkdir /var/run/sshd && \ 29 | mkdir /root/.ssh && \ 30 | mkdir /var/lib/sshd && \ 31 | chmod -R 700 /var/lib/sshd/ && \ 32 | chown -R root:sys /var/lib/sshd/ && \ 33 | useradd -r -U -d /var/lib/sshd/ -c "sshd privsep" -s /bin/false sshd && \ 34 | apt-get update && \ 35 | apt-get install -y build-essential libssl-dev zlib1g-dev && \ 36 | cd $_RESOURCES_PATH && \ 37 | wget "https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-8.0p1.tar.gz" && \ 38 | tar xfz openssh-8.0p1.tar.gz && \ 39 | cd $_RESOURCES_PATH/openssh-8.0p1/ && \ 40 | # modify the code where the 'PermitOpen' host is checked so that it supports regexes 41 | sed -i "s@strcmp(allowed_open->host_to_connect, requestedhost) != 0@strcmp(allowed_open->host_to_connect, requestedhost) != 0 \&\& match_hostname(requestedhost, allowed_open->host_to_connect) == 0@g" ./channels.c && \ 42 | ./configure && \ 43 | make && \ 44 | make install && \ 45 | # filelock is needed for our custom AuthorizedKeysCommand script in the OpenSSH server 46 | apt-get install -y python3-setuptools && \ 47 | pip install filelock && \ 48 | # Python kubernetes client is needed for caching the authorized keys in Kubernetes mode 49 | pip install kubernetes && \ 50 | pip install docker && \ 51 | # Cleanup 52 | clean-layer.sh 53 | 54 | ## Create user with restricted permissions for ssh 55 | # https://gist.github.com/smoser/3e9430c51e23e0c0d16c359a2ca668ae 56 | # https://www.tecmint.com/restrict-ssh-user-to-directory-using-chrooted-jail/ 57 | # http://www.ab-weblog.com/en/creating-a-restricted-ssh-user-for-ssh-tunneling-only/ 58 | RUN useradd -d /home/limited-user -m -s /bin/true --gid nogroup --skel /dev/null --create-home limited-user && \ 59 | #chmod 755 /home/limited-user && \ 60 | #chmod g+rwx /home/limited-user && \ 61 | echo 'PATH=""' >> /home/limited-user/.profile && \ 62 | echo 'limited-user:limited' |chpasswd && \ 63 | chmod 555 /home/limited-user/ && \ 64 | cd /home/limited-user/ && \ 65 | # .bash_logout .bashrc 66 | chmod 444 .profile && \ 67 | chown root:root /home/limited-user/ 68 | 69 | COPY docker-res/start_ssh.py $_RESOURCES_PATH/start_ssh.py 70 | COPY docker-res/ssh/* /etc/ssh/ 71 | 72 | # Set default configuration 73 | ENV SSH_PERMIT_TARGET_HOST="*" \ 74 | SSH_PERMIT_TARGET_PORT="*" \ 75 | SSH_TARGET_KEY_PATH="~/.ssh/id_ed25519.pub" \ 76 | MANUAL_AUTH_FILE="false" \ 77 | SSHD_ENVIRONMENT_VARIABLES="${_RESOURCES_PATH}/sshd_environment" \ 78 | SSH_TARGET_PUBLICKEY_API_PORT=8080 \ 79 | ENV_NAME_SSH_TARGET_LABELS="" 80 | 81 | RUN \ 82 | chmod -R ug+rwx $_RESOURCES_PATH && \ 83 | touch $SSHD_ENVIRONMENT_VARIABLES && \ 84 | chmod a+r $SSHD_ENVIRONMENT_VARIABLES && \ 85 | # Replace the environment variable in the script directly here, since the script is executed from sshd shell and cannot 86 | # access the environment variable directly 87 | sed -i 's@$SSHD_ENVIRONMENT_VARIABLES@'"$SSHD_ENVIRONMENT_VARIABLES"'@g' /etc/ssh/authorize.sh 88 | 89 | ENTRYPOINT python $_RESOURCES_PATH/start_ssh.py 90 | -------------------------------------------------------------------------------- /docker-res/ssh/sshd_config: -------------------------------------------------------------------------------- 1 | # See the sshd_config(5) manpage for details: https://www.freebsd.org/cgi/man.cgi?sshd_config(5) 2 | 3 | # What ports, IPs and protocols we listen for 4 | Port 22 5 | # Use these options to restrict which interfaces/protocols sshd will bind to 6 | #ListenAddress :: 7 | #ListenAddress 0.0.0.0 8 | Protocol 2 9 | # HostKeys for protocol version 2 10 | # HostKey /etc/ssh/ssh_host_rsa_key 11 | # HostKey /etc/ssh/ssh_host_dsa_key 12 | # HostKey /etc/ssh/ssh_host_ecdsa_key 13 | # HostKey /etc/ssh/ssh_host_ed25519_key 14 | # Privilege Separation is turned on for security 15 | # UsePrivilegeSeparation yes 16 | 17 | # Lifetime and size of ephemeral version 1 server key 18 | # deprecated KeyRegenerationInterval 3600 19 | # deprecated ServerKeyBits 1024 20 | 21 | # Logging 22 | SyslogFacility AUTH 23 | LogLevel INFO 24 | 25 | # Authentication: 26 | LoginGraceTime 120 27 | StrictModes yes 28 | 29 | # deprecated RSAAuthentication yes 30 | PubkeyAuthentication yes 31 | #AuthorizedKeysFile %h/.ssh/authorized_keys 32 | 33 | # Don't read the user's ~/.rhosts and ~/.shosts files 34 | IgnoreRhosts yes 35 | # For this to work you will also need host keys in /etc/ssh_known_hosts 36 | # deprecated RhostsRSAAuthentication no 37 | # similar for protocol version 2 38 | HostbasedAuthentication no 39 | # Uncomment if you don't trust ~/.ssh/known_hosts for RhostsRSAAuthentication 40 | #IgnoreUserKnownHosts yes 41 | 42 | # To enable empty passwords, change to yes (NOT RECOMMENDED) 43 | PermitEmptyPasswords no 44 | 45 | # Change to yes to enable challenge-response passwords (beware issues with 46 | # some PAM modules and threads) 47 | ChallengeResponseAuthentication no 48 | 49 | # X11Forwarding yes 50 | X11DisplayOffset 10 51 | PrintMotd no 52 | PrintLastLog yes 53 | TCPKeepAlive yes 54 | #UseLogin no 55 | 56 | #MaxStartups 10:30:60 57 | #Banner /etc/issue.net 58 | 59 | # Allow client to pass locale environment variables 60 | AcceptEnv LANG LC_* 61 | 62 | # Do not activate sftp? 63 | # Subsystem sftp /usr/lib/openssh/sftp-server 64 | 65 | PermitUserEnvironment yes 66 | ClientAliveInterval 60 67 | ClientAliveCountMax 10 68 | 69 | # https://askubuntu.com/questions/48129/how-to-create-a-restricted-ssh-user-for-port-forwarding 70 | # https://unix.stackexchange.com/questions/208960/how-to-restrict-a-user-to-one-folder-and-not-allow-them-to-move-out-his-folder 71 | 72 | UseDNS yes 73 | 74 | # Only allow limited-user 75 | PermitRootLogin no 76 | DenyUsers root 77 | AllowUsers limited-user 78 | 79 | AllowTcpForwarding no 80 | PermitListen none 81 | PermitOpen none 82 | 83 | # Change to no to disable tunnelled clear text passwords 84 | PasswordAuthentication no 85 | 86 | Match User limited-user 87 | GatewayPorts no 88 | AllowTcpForwarding yes 89 | # placeholder are replaced by run.py 90 | PermitOpen {SSH_PERMIT_TARGET_HOST}:{SSH_PERMIT_TARGET_PORT} 91 | PermitListen none 92 | PermitTTY no 93 | PermitTunnel no 94 | X11Forwarding no 95 | AllowAgentForwarding no 96 | AllowStreamLocalForwarding no 97 | AuthorizedKeysCommand /bin/bash /etc/ssh/authorize.sh %k 98 | AuthorizedKeysCommandUser root 99 | ForceCommand echo 'This account can only be used for tunneling.' 100 | 101 | # /bin/false Vs /bin/true 102 | 103 | Match Host localhost 104 | PermitOpen none 105 | X11Forwarding no 106 | AllowAgentForwarding no 107 | AllowTcpForwarding no 108 | ForceCommand echo 'Dont try to connect to local!.' 109 | 110 | Match address 127.0.0.1 111 | PermitOpen none 112 | X11Forwarding no 113 | AllowAgentForwarding no 114 | AllowTcpForwarding no 115 | ForceCommand echo 'Dont try to connect to 127.0.0.1!.' 116 | 117 | Match address ::1 118 | PermitOpen none 119 | X11Forwarding no 120 | AllowAgentForwarding no 121 | AllowTcpForwarding no 122 | ForceCommand echo 'Dont try to connect to ::1!.' 123 | 124 | Match LocalAddress 127.0.0.1,::1 125 | PermitOpen none 126 | X11Forwarding no 127 | AllowAgentForwarding no 128 | AllowTcpForwarding no 129 | ForceCommand echo 'Dont try to connect to localhost (via localaddress)!.' 130 | -------------------------------------------------------------------------------- /docker-res/ssh/update_authorized_keys.py: -------------------------------------------------------------------------------- 1 | """Used for sshd's AuthorizedKeysCommand to fetch a list of authorized keys. 2 | The script will create a file in ssh's authorized_keys format at `/etc/ssh/authorized_keys_cache` containing 3 | authorized_keys from all ssh target containers / pods (by exec'ing into them and fetching the public keys). 4 | The script will cache the containers / pods it already got the keys from to reduce runtime and will only exec 5 | into those it did not fetch the keys from in a previous run. This behavior can be changed with the first argument arg1. 6 | 7 | Args: 8 | arg1 (str): If value of arg1 is 'full', then the cache files are not considered 9 | 10 | """ 11 | 12 | import docker 13 | from kubernetes import client, config, stream 14 | import os 15 | import sys 16 | from filelock import FileLock, Timeout 17 | from subprocess import getoutput 18 | import re 19 | import requests 20 | 21 | SSH_PERMIT_TARGET_HOST = os.getenv("SSH_PERMIT_TARGET_HOST", "*") 22 | SSH_TARGET_KEY_PATH = os.getenv("SSH_TARGET_KEY_PATH", "~/.ssh/id_ed25519.pub") 23 | SSH_TARGET_PUBLICKEY_API_PORT = os.getenv("SSH_TARGET_PUBLICKEY_API_PORT", 8080) 24 | ENV_SSH_TARGET_LABELS = os.getenv("SSH_TARGET_LABELS", "") 25 | 26 | authorized_keys_cache_file = "/etc/ssh/authorized_keys_cache" 27 | authorized_keys_cache_file_lock = "cache_files.lock" 28 | query_cache_file = "/etc/ssh/query_cache" 29 | 30 | container_client = None 31 | CONTAINER_CLIENT_KUBERNETES = "kubernetes" 32 | CONTAINER_CLIENT_DOCKER = "docker" 33 | 34 | PRINT_KEY_COMMAND = '/bin/bash -c "cat {SSH_TARGET_KEY_PATH}"'.format(SSH_TARGET_KEY_PATH=SSH_TARGET_KEY_PATH) 35 | 36 | SSH_PERMIT_TARGET_HOST_REGEX = SSH_PERMIT_TARGET_HOST.replace("*", ".*") 37 | SSH_PERMIT_TARGET_HOST_REGEX = re.compile(SSH_PERMIT_TARGET_HOST_REGEX) 38 | 39 | 40 | # First try to find Kubernetes client. If Kubernetes client or the config is not there, use the Docker client 41 | try: 42 | try: 43 | # incluster config is the config given by a service account and it's role permissions 44 | config.load_incluster_config() 45 | except config.ConfigException: 46 | config.load_kube_config() 47 | kubernetes_client = client.CoreV1Api() 48 | container_client = CONTAINER_CLIENT_KUBERNETES 49 | 50 | # at this path the namespace the container is in is stored in Kubernetes deployment (see https://stackoverflow.com/questions/31557932/how-to-get-the-namespace-from-inside-a-pod-in-openshift) 51 | NAMESPACE = getoutput( 52 | "cat /var/run/secrets/kubernetes.io/serviceaccount/namespace") 53 | except (FileNotFoundError, TypeError): 54 | try: 55 | docker_client = docker.from_env() 56 | docker_client.ping() 57 | container_client = CONTAINER_CLIENT_DOCKER 58 | except FileNotFoundError: 59 | pass 60 | 61 | if container_client is None: 62 | print("Could neither initialize Kubernetes nor Docker client. Stopping execution.") 63 | exit(1) 64 | 65 | 66 | def get_authorized_keys_kubernetes(query_cache: list = []) -> (list, list): 67 | """Execs into all Kubernetes pods where the name complies to `SSH_PERMIT_TARGET_HOST` and returns it's public key. 68 | 69 | Note: 70 | This method can be quite slow. For big setups / clusters, think about rewriting it to fetch public keys from a REST API or so. 71 | 72 | Args: 73 | query_cache (list[str]): contains Pod names which are skipped 74 | 75 | Returns: 76 | list[str]: newly fetched public keys 77 | list[str]: name of all pods (previously cached ones and newly exec'd ones) 78 | 79 | """ 80 | 81 | pod_list = kubernetes_client.list_namespaced_pod( 82 | NAMESPACE, field_selector="status.phase=Running", label_selector=ENV_SSH_TARGET_LABELS) 83 | authorized_keys = [] 84 | new_query_cache = [] 85 | for pod in pod_list.items: 86 | name = pod.metadata.name 87 | pod_ip = pod.status.pod_ip 88 | 89 | if SSH_PERMIT_TARGET_HOST_REGEX.match(name) is None: 90 | continue 91 | elif name in query_cache: 92 | new_query_cache.append(name) 93 | continue 94 | 95 | key = None 96 | # Try to get the public key via an API call first 97 | publickey_url = "http://{}:{}/publickey".format(pod_ip, str(SSH_TARGET_PUBLICKEY_API_PORT)) 98 | timeout_seconds = 10 99 | try: 100 | request = requests.request("GET", publickey_url, timeout=timeout_seconds) 101 | if request.status_code == 200: 102 | key = request.text 103 | except requests.exceptions.ConnectTimeout: 104 | print("Connection to {ip} timed out after {timeout} seconds. Will try to exec into the pod to retrieve the key.".format(ip=pod_ip, timeout=str(timeout_seconds))) 105 | 106 | # If the API call did not work, try to exec into the pod. 107 | # Make sure that the executing process has permission to exec into the target pod (e.g. when Kubernetes roles are used) 108 | if key is None: 109 | try: 110 | exec_result = stream.stream(kubernetes_client.connect_get_namespaced_pod_exec, name, 111 | NAMESPACE, command=PRINT_KEY_COMMAND, stderr=True, stdin=False, stdout=True, tty=False) 112 | key = exec_result 113 | except: 114 | # This can happen when the pod is in a false state such as Terminating, as status.phase is 'Running' but pod cannot be reached anymore 115 | print("Could not reach pod {}".format(name)) 116 | 117 | if key is not None: 118 | authorized_keys.append(key) 119 | new_query_cache.append(name) 120 | 121 | return authorized_keys, new_query_cache 122 | 123 | 124 | def get_authorized_keys_docker(query_cache: list = []) -> (list, list): 125 | """Execs into all Docker containers where the name starts with `SSH_PERMIT_TARGET_HOST` and returns it's public key. 126 | 127 | Note: 128 | This method can be quite slow. For big setups / clusters, think about rewriting it to fetch public keys from a REST API or so. 129 | 130 | Args: 131 | query_cache (list[str]): contains container ids which are skipped 132 | 133 | Returns: 134 | list[str] newly fetched public keys 135 | list[str]: ids of all containers (previously cached ones and newly exec'd ones) 136 | 137 | """ 138 | 139 | filters = {"status": "running"} 140 | if ENV_SSH_TARGET_LABELS != "": 141 | SSH_TARGET_LABELS = ENV_SSH_TARGET_LABELS.split(",") 142 | filters.update({"label": SSH_TARGET_LABELS}) 143 | 144 | containers = docker_client.containers.list(filters=filters) 145 | authorized_keys = [] 146 | new_query_cache = [] 147 | for container in containers: 148 | 149 | if SSH_PERMIT_TARGET_HOST_REGEX.match(container.name) is None: 150 | continue 151 | elif container.id in query_cache: 152 | new_query_cache.append(container.id) 153 | continue 154 | 155 | key = None 156 | # Try to get the public key via an API call first 157 | publickey_url = "http://{}:{}/publickey".format(container.name, str(SSH_TARGET_PUBLICKEY_API_PORT)) 158 | timeout_seconds = 10 159 | try: 160 | request = requests.request("GET", publickey_url, timeout=timeout_seconds) 161 | if request.status_code == 200: 162 | key = request.text 163 | except (requests.exceptions.ConnectionError, requests.exceptions.ConnectTimeout): 164 | print("Connection to {ip} timed out after {timeout} seconds. Will try to exec into the pod to retrieve the key.".format(ip=container.id, timeout=str(timeout_seconds))) 165 | 166 | if key is None: 167 | exec_result = container.exec_run(PRINT_KEY_COMMAND) 168 | key = exec_result[1].decode("utf-8") 169 | 170 | if key is not None: 171 | authorized_keys.append(key) 172 | new_query_cache.append(container.id) 173 | 174 | return authorized_keys, new_query_cache 175 | 176 | 177 | def update_cache_file(): 178 | # make sure only a single script execution can update authorized_keys file 179 | lock = FileLock(authorized_keys_cache_file_lock, timeout=0) 180 | try: 181 | with lock: 182 | write_mode = 'a' 183 | # Delete query_cache file in case it is a 'full' run 184 | if len(sys.argv) == 2 and sys.argv[1] == "full": 185 | if os.path.isfile(query_cache_file): 186 | os.remove(query_cache_file) 187 | write_mode = 'w' 188 | 189 | query_cache = [] 190 | if os.path.isfile(query_cache_file): 191 | with open(query_cache_file, 'r') as cache_file: 192 | for line in cache_file.readlines(): 193 | # the strip will remove the newline character at the end of each line 194 | query_cache.append(line.strip()) 195 | 196 | if container_client == CONTAINER_CLIENT_DOCKER: 197 | authorized_keys, new_query_cache = get_authorized_keys_docker( 198 | query_cache=query_cache) 199 | elif container_client == CONTAINER_CLIENT_KUBERNETES: 200 | authorized_keys, new_query_cache = get_authorized_keys_kubernetes( 201 | query_cache=query_cache) 202 | 203 | with open(authorized_keys_cache_file, write_mode) as cache_file: 204 | for authorized_key in authorized_keys: 205 | if authorized_key.startswith("ssh") == False: 206 | continue 207 | 208 | cache_file.write("{}\n".format(authorized_key)) 209 | 210 | with open(query_cache_file, 'w') as cache_file: 211 | for entry in new_query_cache: 212 | cache_file.write("{}\n".format(entry)) 213 | 214 | except Timeout: 215 | # The cache is currently updated by someone else 216 | print("The cache is currently updated by someone else") 217 | pass 218 | 219 | 220 | if __name__ == "__main__": 221 | update_cache_file() 222 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | SSH Proxy 3 |

4 | 5 |

6 | Dockerized SSH bastion to proxy SSH connections to arbitrary containers. 7 |

8 | 9 |

10 | 11 | 12 | 13 | 14 | 15 | 16 |

17 | 18 |

19 | Getting Started • 20 | Highlights • 21 | Support • 22 | Report a Bug • 23 | Contribution 24 |

25 | 26 | This SSH proxy can be deployed as a standalone docker container that allows to proxy any user SSH connection to arbitrary unexposed containers. This enables users to securely access any container via SSH within a cluster only via a single exposed port and provides full SSH compatibility (e.g. port tunneling, scp, sftp, rsync, sshfs, X11). This proxy has a few security features built-in to make sure that users can only access target containers that they are allowed to. 27 | 28 | ## Highlights 29 | 30 | - 🛡 SSH access to behind-firewall clusters via a single port. 31 | - 🔐 Restrict target containers based on port and DNS pattern. 32 | - 🛠 Full SSH compatibility (port tunneling, scp, sftp, rsync, sshfs). 33 | - 📄 Basic access logging based on user logins. 34 | - 🐳 Easy to deploy via Docker and Kubernetes. 35 | - 🏗 Use it as a base image in your own Docker image to bring the ssh functionality into it (checkout the [ml-hub Dockerfile](https://github.com/ml-tooling/ml-hub/blob/1ab1c6b1b4b4b8a6fd2f321ccfb9c8f6f0e0c6eb/Dockerfile#L1) as an example) 36 | 37 | ## Getting Started 38 | 39 | ### Prerequisites 40 | 41 | The target containers must run an SSH server and provide a valid public key. The ssh-proxy container will try to get a key from a target container via a `/publickey` endpoint (e.g. `http://:8080/publickey`, whereby the port 8080 can be configured via an [environment variable]($SSH_TARGET_PUBLICKEY_API_PORT)); if this does not exist, the ssh-proxy tries to exec into the target container and search for the publickey under `$SSH_TARGET_KEY_PATH` (default: `~/.ssh/id_ed25519.pub`). 42 | 43 | > ℹ️ _The SSH proxy accepts an incoming key, if it belongs to one of the targets key, in other words the proxy/bastion server authorizes all target public keys. It is still not possible to login to the proxy directly. The authorization happens only for creating and tunneling the final connection._ 44 | 45 | Port and hostname of target containers that users are allowed to access can be restricted via environment variables (see [configuration section](#configuration)), but the restrictions can be applied only accross all targets. In Kubernetes mode, the SSH proxy and the SSH targets must be in the same namespace. 46 | 47 | We recommend to offer the public key via the `/publickey` endpoint, as the `kubectl exec` command can be slow for big clusters. You can also completely avoid those requirements by setting `$MANUAL_AUTH_FILE=true` and maintaing the proxy's `/etc/ssh/authorized_keys_cache` file yourself (e.g. by mounting a file at the same location). In this case, you don't have to mount the Docker socket / Kubernetes config into the container. The `authorized_keys_cache` file has the same format as the standard ssh authorized_keys file. 48 | 49 | ### Start SSH Proxy 50 | 51 | #### Docker 52 | 53 | ```bash 54 | docker run -d \ 55 | -p 8091:22 \ 56 | -v /var/run/docker.sock:/var/run/docker.sock \ 57 | mltooling/ssh-proxy 58 | ``` 59 | 60 | #### Kubernetes 61 | 62 | If you make a kube config available to the container, either via incluster config (Python code: `kubernetes.config.load_incluster_config()`) or by mounting it to `/root/.kube/config`, ssh-proxy also works for tunneling requests in Kubernetes. 63 | 64 | ### Connect to Target 65 | 66 | ```bash 67 | ssh \ 68 | -o "ProxyCommand=ssh -W %h:%p -p 8091 -i ~/.ssh/ limited-user@" \ 69 | -p \ 70 | -i ~/.ssh/ \ 71 | root@ 72 | ``` 73 | 74 | Doing this way, the connection from client to target is end-to-end encrypted. 75 | 76 | > ℹ️ _The "\" host can be the Docker container name or Kubernetes service name. In that case, the bastion has to be in the same Docker network or the connection must be allowed in case of existing Networkpolicies in Kubernetes, respectively._ 77 | 78 | ### Configuration 79 | 80 | The container can be configured with the following environment variables (`--env`): 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
VariableDescriptionDefault
SSH_PERMIT_TARGET_HOSTDefines which other containers can be ssh targets. The container names must start with the prefix. The ssh connection to the target can only be made for targets where the name matches the given target host. The '*' character can be used as wildcards, e.g. 'workspace-*' would allow connecting to target containers/services which names start with 'workspace-'. 91 | *
SSH_PERMIT_TARGET_PORTDefines on which port the other containers can be reached via ssh. The ssh connection to the target can only be made via this port then. The default value '*' permits any port.*
SSH_TARGET_LABELSSpecify which containers are targeted. Filters containers / pods via these labels. Must be in the form of "label1=value1,label2=value2,label3=value3". Default is empty string which disables filtering.""
SSH_TARGET_PUBLICKEY_API_PORTPort where the target container exposes the /publickey endpoint (if used).8080
SSH_TARGET_KEY_PATHThe path inside the target containers where the manager looks for a valid public key. 112 | Consider that `~` will be resolved to the target container's home. Only used when the target container does not return a public key via the /publickey endpoint.~/.ssh/id_ed25519.pub
MANUAL_AUTH_FILEDisables the bastion's public key fetching method and you have to maintain the /etc/ssh/authorized_keys_cache file yourself (e.g. by mounting a respective file there). Only used when the target container does not return a public key via the /publickey endpoint.false
121 | 122 | ## Features 123 | 124 | ### Access Logging 125 | 126 | Logins are logged at `/etc/ssh/access.log` 127 | 128 | ## Support 129 | 130 | The SSH Proxy project is maintained by [@raethlein](https://twitter.com/raethlein) and [@LukasMasuch](https://twitter.com/LukasMasuch). Please understand that we won't be able 131 | to provide individual support via email. We also believe that help is much more 132 | valuable if it's shared publicly so that more people can benefit from it. 133 | 134 | | Type | Channel | 135 | | ------------------------ | ------------------------------------------------------ | 136 | | 🚨 **Bug Reports** | | 137 | | 🎁 **Feature Requests** | | 138 | | 👩‍💻 **Usage Questions** | | 139 | | 🗯 **General Discussion** | | 140 | 141 | ## Contribution 142 | 143 | - Pull requests are encouraged and always welcome. Read [`CONTRIBUTING.md`](https://github.com/ml-tooling/ssh-proxy/tree/master/CONTRIBUTING.md) and check out [help-wanted](https://github.com/ml-tooling/ssh-proxy/issues?utf8=%E2%9C%93&q=is%3Aopen+is%3Aissue+label%3A"help+wanted"+sort%3Areactions-%2B1-desc+) issues. 144 | - Submit github issues for any [feature enhancements](https://github.com/ml-tooling/ssh-proxy/issues/new?assignees=&labels=feature-request&template=02_feature-request.md&title=), [bugs](https://github.com/ml-tooling/ssh-proxy/issues/new?assignees=&labels=bug&template=01_bug-report.md&title=), or [documentation](https://github.com/ml-tooling/ssh-proxy/issues/new?assignees=&labels=enhancement%2C+docs&template=03_documentation.md&title=) problems. 145 | - By participating in this project you agree to abide by its [Code of Conduct](https://github.com/ml-tooling/ssh-proxy/tree/master/CODE_OF_CONDUCT.md). 146 | 147 | --- 148 | 149 | Licensed **Apache 2.0**. Created and maintained with ❤️ by developers from SAP in Berlin. 150 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------