Skip to content

Commit

Permalink
Initial commit, V1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
izalac committed Sep 15, 2023
1 parent 28c4b00 commit bce9e3d
Show file tree
Hide file tree
Showing 15 changed files with 540 additions and 0 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
venv/
__pycache__/
.idea/
.vscode/
29 changes: 29 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
FROM python:3.11.5-alpine3.18

# System update
RUN apk update
RUN apk upgrade
RUN python3 -m pip install --upgrade pip

# Install requirements
WORKDIR /ssh-script-dashboard
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt

#adds the rest of the files
COPY . .

# Add custom CA certificate - if you need one, add it in as config/cacert.crt and uncomment:
# RUN apk --no-cache add ca-certificates
# COPY config/cacert.crt /usr/local/share/ca-certificates
# RUN update-ca-certificates
# ENV REQUESTS_CA_BUNDLE /python-docker/config/cacert.crt

# Unit tests are on - image build fails if they fail
RUN python3 -m unittest

# Networking
EXPOSE 5000

# Runs the container
CMD ["python3", "-m" , "flask", "run", "--host=0.0.0.0"]
63 changes: 63 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# SSH Script Dashboard

If you work as a GNU/Linux expert, sysadmin or devops in some capacity, you might be writing some scripts at your workplace to automate some of the tasks. Many of them can be fully automated, such as with crontab or systemd timers, but there are a few which you might need to run manually when a user requests it.

SSH Script Dashboard is intended as a solution to those issues. It allows you to set up a script dashboard for yourself, and it can be deployed as a self-service portal to allow users to trigger scripts themselves. It can run scripts remotely via SSH, or locally on any OS it runs on. It supports OpenID Connect (OIDC) for secure single sign-on and identity and access management. By default, it comes with a lightweight, flexible design, with dark and light mode themes, and can be easily customized. It works on screens of any size, including mobile.

Some configuration is required before you run it - either via environment variables, or via the included env.json config file. You can list your custom scripts in the provided commands.json config file. Dockerfile is also provided for container deployments, with commented-out section that allows you to inject your own CA certificate, if required.

## Requirements and initial setup

SSH Script Dashboard is built using the following technology:

* Python 3.11 (older versions might work too)
* Flask framework
* Jinja2 templates
* Tailwind CSS
* HTMX

To download SSH Script Dashboard, you need git and Python 3 on your system. From your terminal or command prompt, clone this repository and enter it. If it's your first time, you'll need to create a virtual environment, which you need to activate, and install the requirements in package.

### GNU/Linux instructions (should also work on Mac and most OS-es)

git clone https://github.com/izalac/ssh-script-dashboard
cd ssh-script-dashboard
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
python3 -m flask run

### Windows instructions

git clone https://github.com/izalac/ssh-script-dashboard
cd ssh-script-dashboard
py -m venv venv
venv\Scripts\activate.bat
pip3 install -r requirements.txt
py -m flask run

When this is done, you should see the application running in your browser on http://localhost:5000/

At this point, you probably want to quit running and configure it further...

## Configuration

SSH Script Dashboard requires some configuration to run properly. You'll find the documents below:

* [Environment setup](docs/environment.md)
* [Security setup](docs/security.md)
* [Command setup](commands.md)

## Legal stuff

Copyright © 2023 Ivan Žalac

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
[GNU General Public License](LICENSE) for more details.
86 changes: 86 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
'''
SSH Script Dashboard
An interface for executing scripts locally, or remotely over SSH
Copyright (C) 2023 Ivan Žalac
https://github.com/izalac/ssh-script-dashboard
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
'''

from flask import Flask, render_template, session
import os
import json
import envexecute as ex
import logging


app = Flask(__name__)
logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
version='V1.0.0'

if os.environ['AUTH_MODEL'] == 'oidc':
from flask_pyoidc.provider_configuration import ClientMetadata
from flask_pyoidc.provider_configuration import ProviderConfiguration
from flask_pyoidc.user_session import UserSession
from flask_pyoidc import OIDCAuthentication
app.config.update(OIDC_REDIRECT_URI=os.environ['LOCAL_OIDC_REDIRECT_URI'],
SECRET_KEY=os.environ['LOCAL_SECRET_KEY'])
cli_meta = ClientMetadata(client_id=os.environ['OIDC_CLIENT_ID'],
client_secret=os.environ['OIDC_CLIENT_SECRET'])
provider_config = ProviderConfiguration(issuer=os.environ['OIDC_ISSUER'],
client_metadata=cli_meta)
auth = OIDCAuthentication({'oidc-provider': provider_config})
auth.init_app(app)


# Loads the configured commands
try:
with open("config/commands.json") as command_file:
commands = json.load(command_file)
except Exception as e:
message=(f'Critical error in loading commands.json: {e}')
logging.critical(message)
raise Exception(message)


# Main landing page
# If using OIDC, add the following line under the @app.route line:
# @auth.oidc_auth('oidc-provider')
@app.route("/")
def index():
return render_template('index.html', commands=commands, version=version)


# Script running endpoint
# If using OIDC, add the following line under the @app.route line:
# @auth.oidc_auth('oidc-provider')
@app.route("/scripts/<string:script>")
def run_script(script):
try:
name='user'
if os.environ['AUTH_MODEL'] == 'oidc':
user_session = UserSession(session)
name = user_session.userinfo['name']
logging.info(f'{script} triggered by {name}')
result = ex.default_execute(commands[script])
return result
except Exception as e:
message=(f'Execution error: {e}')
logging.error(message)
return (f'{message} <br />')


# Runs the app
if __name__ == "__main__":
app.run()
4 changes: 4 additions & 0 deletions config/commands.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"system-id": "uname -a",
"system-usage-stats": "top -b -n 1"
}
12 changes: 12 additions & 0 deletions config/env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"REMOTESERVER": "hostname",
"REMOTEUSER": "username",
"REMOTECERT": "config/id-remote",
"LOCAL_OIDC_REDIRECT_URI": "http://localhost:5000/redirect_uri",
"LOCAL_SECRET_KEY": "qGfBF-g6f_ITH7K6EmISBXb_d7LMpblGhxkRpA1EL9Q",
"OIDC_ISSUER": "http://localhost:8080/realms/realmname",
"OIDC_CLIENT_ID": "clientname",
"OIDC_CLIENT_SECRET": "clientsecret",
"EXECUTE_MODEL": "local",
"AUTH_MODEL": "none"
}
38 changes: 38 additions & 0 deletions config/id-remote
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
-----BEGIN OPENSSH PRIVATE KEY-----
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
/ofSKc/YgFF7oXAAAAB2RlbW9rZXkBAgM=
-----END OPENSSH PRIVATE KEY-----
1 change: 1 addition & 0 deletions config/id-remote.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDIiCNgA7yh8KRfA60iCRguXWh3iAntSQf5rfY3EU2Lw0rYWp2GQV50dw7rvCKAksRsDBuJJThXPyfY5xV3w6G7hEqVyMbLEINWM01hMl6DYRuTlzLc0g4x8gJxnqRs7qgZLgikdnEqerFnKY3U8BoH4aMsZFA0ZkN0Bveggmst5GljSb7tMCWAQqeE1FXPH4L8/Nm1BJOUwaLA/VhzyCmYrlY/XwE86lC3Qrc214AKj78QEqPMAN213NQnzBK9xeRouz9e6oawS1Svngj+dgF7Ma4fRCC5q0MLIkkUfhrhE/rVlPcORwev2vmJe7a2KZQdua/jUQKt/hd/Wvgf1fQOj0icinliNzkw0Xmxq0LUUe7LzpZa1cuvympxtdzdrM+GBYsQOpHrZigXaHqXMhnvoDivL0diR4mwbuWqrWD8GXtp146xwlPwHj6TEo0JsyccZNkAz+Aot++x5yESQD2CCZZQyUrFCBn0g5KT6kh9UeT0JfrE/K5kXKQ8XvwlseE= demokey
11 changes: 11 additions & 0 deletions docs/commands.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Command setup for SSH Script Dashboard

Edit the config/commands.json file to add your commands. Commands are listed in JSON format (be mindful of the proper JSON formatting), and listed as simple key-value pairs.

The key part should be URL-safe, as it is used for both button labels on the webpage to name, and link that calls it in the backend.

The value part is the script that you wish to run. Regular commands can also be executed instead (and are provided in the examples).

Script and command paths depend on the user executing them, depending on the [EXECUTE_MODEL](environment.md) variable whether the local user who runs flask, or the configured SSH remote user and their default login environment.

[Back to Readme](../README.md)
42 changes: 42 additions & 0 deletions docs/environment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Environment setup for SSH Script Dashboard

SSH Script Dashboard depends on several environmental variables. For [security reasons](security.md), the best practice is if these are actually set up as environment variables. There are secure ways to inject this in many environments, such as a password vault, or kubernetes secrets.

App checks if **EXECUTE_MODEL** variable is set in the environment, if it's not, it injects default environment variables from env.json. These can also be modified, if you wish to run them locally.

## You must have these variables

* **EXECUTE_MODEL** determines how you run your scripts. It can be one of the following values:
* *local* is default, the app will run scripts on local system and display their output. Best option for quick scripts.
* *local-background* will run scripts on local system, but in a tmux detached session. Requires tmux to be installed. Output will not be displayed. Best option for long-running scripts.
* *remote* runs scripts on a remote system over SSH and display their output. Best option for quick scripts.
* *remote-background* will run scripts on a remote system over SSH, but in a tmux detached session. Requires tmux to be installed. Output will not be displayed. Best option for long-running scripts.
* **AUTH_MODEL** sets up your authentication model:
* *none* is default, but any other value not otherwise defined will function the same. No authentication or authorization, unless you set it up elsewhere.
* *oidc* sets up OIDC authentication. This will require having a SSO system that supports OIDC, setting up some additional variables, possibly adding custom CA certificates, as well as adding a line of code in app.py under every route you wish to secure. More details on the latest part is in comments in app.py

## Remote server configuration

If your **EXECUTE_MODEL** is *remote* or *remote-background*, you **must** have these variables set up to connect to remote server via ssh:

* **REMOTESERVER** - a hostname or IP address of a target system you're connecting to
* **REMOTEUSER** - username you're connecting as
* **REMOTECERT** - path to the private key of a SSH certificate

Info on how to create SSH certificates is in the [security document](security.md)

## OIDC configuration

These variables are required if your **AUTH_MODEL** is *oidc*.

* **LOCAL_OIDC_REDIRECT_URI** - redirect URI which you send to OIDC server, the default of http://localhost:5000/redirect_uri should be fine for testing, but make sure to use the hostname and port on which this app will be available, and change to https once you set it up.
* **LOCAL_SECRET_KEY** - your local secret key that will sign your session. You can change it to any random string, you should not use the default provided value. You can use the following one-liner to randomly generate a new one:

python -c "import secrets; print(secrets.token_urlsafe())"

* **OIDC_ISSUER** - base OIDC URI. The other endpoints required for OIDC will be autodiscovered from it.
* **OIDC_CLIENT_ID** - the id of your OIDC client
* **OIDC_CLIENT_SECRET** - secret value of your OIDC client; client authentication/confidential access must be set up, this is the only OIDC client configuration that will work


[Back to Readme](../README.md)
35 changes: 35 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Security setup for SSH Script Dashboard

This document will list some options for securing your installation.

## Connect to the remote server

For *remote* and *remote-background* [execution models](environment.md), you will need to set up SSH keys. This guide assumes you have your remote server and its' scripts set up.

The default key location and name is *config/id-remote*, but this can be changed in the environment. A default key is already provided for testing purposes, but you shouldn't use it for any serious work - please regenerate it at your earliest convenience. You can do this in a variety of tools, here are the instructions for openssh's ssh-keygen. Place yourself in the directory, and type the following:

ssh-keygen -f id-remote -C "ssh-script-dashboard"

Use empty passphrase. This command will re-create two files, *id-remote* (your private key) and *id-remote.pub* (your public key). Use ssh-copy-id to copy it to your remote server:

ssh-copy-id -i id-remote remoteuser@remoteserver

Alternatively, you can manually add your public key on the remote server to the end of ~/.ssh/authorized_keys file.

authorized_keys file can be further secured with options that come in front of every public key line, such as "command=..." to restrict the login to run only certain [commands or scripts](commands.md), and "from=..." to restrict connections to only certain IP addresses or hostnames. See *man sshd* for more details.

## OpenID Connect

SSH Script Dashboard has been tested against Keycloak with OIDC, but any other OIDC solution should also work fine. Keycloak also comes as a docker image, and you can [set it up and test it](https://www.keycloak.org/getting-started/getting-started-docker) within minutes.

If your OIDC solution is integrated with your corporate login, using a shared realm, or public identity providers, you might not want to grant everyone access. Implementing further restrictions is required in that case.

And remember the rule of two - if your OIDC system is on http, this app should be as well. If it's on https, this app should be as well. Certificate trust must be established, or else there might be issues with authentication. This is typically not an issue if you're using commercial TLS certificates (or letsencrypt), but there could be issues with using internal CA. Included Dockerfile provides a sample CA bundle injection (commented out by default).

## Securing deployment and scalability

Ideally, if accessed by other people, this app should be exposed through a remote proxy, load balancer, kubernetes ingress or a similar technology. TLS should be implemented either on those, or on the app itself for complete end-to-end encryption.

If you followed these instructions or used the dockerfile, SSH Script Dashboard runs in a Flask development server. This should be sufficient for solo use, or very small deployments. If you wish to make it accessible to a large number of people, you should probably use a production WSGI server, such as gunicorn.

[Back to Readme](../README.md)
Loading

0 comments on commit bce9e3d

Please sign in to comment.