Update to latest example-appstore structure with custom apps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 09:00:15 +00:00
commit 6a5ccd5c02
135 changed files with 23096 additions and 0 deletions

View File

@@ -0,0 +1,11 @@
# Required
OC_URL=https://vpn.cistech.net/Employees
OC_SERVERCERT=pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=
# Optional
OC_AUTHGROUP=
OC_SSO_ARGS=--browser-display-mode shown
VNC_PASSWORD=vpnSSO12
NOVNC_PORT=6901
PUBLISH_ADDR=0.0.0.0
SSH_KEY_PATH=/home/alexz/.ssh/id_ed25519-lenovo

42
apps/cistech-tunnel/README.md Executable file
View File

@@ -0,0 +1,42 @@
# Cistech Tunnel
OpenConnect-SSO VPN client running in a container with noVNC for browser-based access.
## Features
- **OpenConnect-SSO**: Cisco AnyConnect VPN with SSO/SAML authentication
- **TOTP Support**: Automatic 2FA via keyring integration
- **Auto-reconnect**: Automatically reconnects on disconnection
- **noVNC**: Browser-based VNC access on port 6902
- **NAT/Masquerade**: Routes traffic through VPN tunnel
- **Cloudflared**: Optional Cloudflare tunnel support
- **SSH Tunnels**: Optional SSH port forwarding
## Runtipi Installation
1. Install from the app store or custom repo
2. Configure the required environment variables
3. Start the app via Runtipi dashboard
## First-time SSO Login
1. Open noVNC at `http://<host>:6902`
2. Enter VNC password
3. Complete SSO login in the browser window
4. VPN will connect and auto-reconnect on disconnect
## Source Files
- `source/Dockerfile`: Container build file
- `source/entrypoint.sh`: Container entrypoint with auto-reconnect
## Environment Variables
| Variable | Required | Description |
|----------|----------|-------------|
| OC_URL | Yes | VPN server URL |
| OC_SERVERCERT | Yes | Server certificate pin |
| OC_USER | No | Username (enables hidden browser mode) |
| VNC_PASSWORD | Yes | noVNC access password |
| OC_TOTP_SECRET | No | TOTP secret for auto 2FA |
| NOVNC_PORT | No | noVNC port (default: 6901) |

53
apps/cistech-tunnel/config.json Executable file
View File

@@ -0,0 +1,53 @@
{
"name": "Cistech Tunnel",
"id": "cistech-tunnel",
"available": true,
"short_desc": "Cistech VPN client container with noVNC.",
"author": "alexz",
"port": 6902,
"categories": [
"utilities",
"network"
],
"description": "OpenConnect-SSO VPN running in an isolated namespace with noVNC for first-time SSO reconnects.",
"tipi_version": 1,
"version": "1.0.0",
"source": "local",
"exposable": true,
"dynamic_config": true,
"no_gui": false,
"form_fields": [
{
"label": "VPN URL",
"type": "text",
"env_variable": "OC_URL",
"required": true,
"default": "https://vpn.cistech.net/Employees"
},
{
"label": "VNC Password",
"type": "password",
"env_variable": "VNC_PASSWORD",
"required": true,
"default": "Az@83278327$$@@"
},
{
"label": "Server Certificate",
"type": "text",
"env_variable": "OC_SERVERCERT",
"required": true,
"default": "pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0="
},
{
"label": "Username",
"type": "text",
"env_variable": "OC_USER",
"required": true,
"default": "alex.zaw@cistech.net"
}
],
"supported_architectures": [
"arm64",
"amd64"
]
}

View File

@@ -0,0 +1,23 @@
{
"services": [
{
"name": "cistech-tunnel",
"image": "cistech-vpn:latest",
"isMain": true,
"internalPort": 6902,
"privileged": true,
"capAdd": ["NET_ADMIN"],
"devices": ["/dev/net/tun:/dev/net/tun"],
"environment": {
"OC_URL": "${OC_URL}",
"OC_SERVERCERT": "${OC_SERVERCERT}",
"OC_USER": "${OC_USER}",
"VNC_PASSWORD": "${VNC_PASSWORD}",
"NOVNC_PORT": "6902"
},
"volumes": [
{ "hostPath": "${APP_DATA_DIR}/data", "containerPath": "/root" }
]
}
]
}

View File

@@ -0,0 +1,34 @@
services:
cistech-tunnel:
image: cistech-vpn:latest
restart: unless-stopped
networks:
cistech-tunnel_runtipi_network:
gw_priority: 0
tipi_main_network:
gw_priority: 1
environment:
OC_URL: ${OC_URL}
OC_SERVERCERT: ${OC_SERVERCERT}
OC_USER: ${OC_USER}
VNC_PASSWORD: ${VNC_PASSWORD}
NOVNC_PORT: "6902"
ports:
- ${APP_PORT}:6902
volumes:
- ${APP_DATA_DIR}/data:/root
labels:
generated: true
traefik.enable: true
traefik.docker.network: runtipi_tipi_main_network
traefik.http.middlewares.cistech-tunnel-runtipi-web-redirect.redirectscheme.scheme: https
traefik.http.services.cistech-tunnel-runtipi.loadbalancer.server.port: "6902"
traefik.http.routers.cistech-tunnel-runtipi-insecure.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.cistech-tunnel-runtipi-insecure.entrypoints: web
traefik.http.routers.cistech-tunnel-runtipi-insecure.service: cistech-tunnel-runtipi
traefik.http.routers.cistech-tunnel-runtipi-insecure.middlewares: cistech-tunnel-runtipi-web-redirect
traefik.http.routers.cistech-tunnel-runtipi.rule: Host(`${APP_DOMAIN}`)
traefik.http.routers.cistech-tunnel-runtipi.entrypoints: websecure
traefik.http.routers.cistech-tunnel-runtipi.service: cistech-tunnel-runtipi
traefik.http.routers.cistech-tunnel-runtipi.tls.certresolver: myresolver
runtipi.managed: true

View File

@@ -0,0 +1,20 @@
# Dockerized OpenConnect-SSO with noVNC and Cloudflared
## Setup
1) Copy `.env.example` to `.env` and fill values (URLs, servercert pins, VNC passwords, cloudflared tokens).
2) First-time SSO: leave `OC_SSO_ARGS_*=--browser-display-mode visible`.
3) Build and start:
docker compose build
docker compose up -d vpn_a
# Open http://localhost:6901, complete SSO.
# After success, attach app containers or start cloudflared_a.
4) Optional: switch to headless after first login:
Set `OC_SSO_ARGS_*=--browser-display-mode hidden` (or `headless`) and restart the vpn service.
## Notes
- Each VPN runs in its own net namespace; routes from one cannot affect the other or the host.
- DNS from the VPN applies within its container namespace and attached services only.
- Persisted state lives in the named volumes mounted at `/root` (Playwright cache, configs).

Binary file not shown.

After

Width:  |  Height:  |  Size: 609 KiB

View File

@@ -0,0 +1,46 @@
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive \
PLAYWRIGHT_BROWSERS_PATH=/ms-playwright \
VIRTUAL_ENV=/opt/venv \
PATH=/opt/venv/bin:$PATH \
QTWEBENGINE_DISABLE_SANDBOX=1 \
QTWEBENGINE_CHROMIUM_FLAGS="--no-sandbox --disable-gpu" \
OC_URL="https://vpn.cistech.net/Employees" \
OC_SERVERCERT="pin-sha256:HyHob3LiVmIp8ch9AzHJ9jMYqI43tO5N13oWeBLiZ/0=" \
OC_USER="alex.zaw@cistech.net" \
OC_TOTP_SECRET="t6ypnjqvyx2yvw2l" \
VNC_PASSWORD="Az@83278327\$\$@@"
RUN apt-get update && apt-get install -y \
openconnect iproute2 iptables ca-certificates \
python3 python3-pip python3-venv \
vpnc-scripts curl wget openssh-client \
x11vnc xvfb fluxbox novnc websockify xterm nano oathtool \
xauth libnss3 libatk1.0-0 libatk-bridge2.0-0 \
libx11-6 libx11-xcb1 libxcomposite1 libxrandr2 libgbm1 libxdamage1 \
libpango-1.0-0 fonts-liberation \
libegl1 libgl1 libopengl0 libdbus-1-3 libglib2.0-0 \
libxkbcommon0 libxkbcommon-x11-0 \
libxcb1 libxcb-cursor0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render0 libxcb-render-util0 libxcb-shm0 libxcb-xfixes0 libxcb-xinerama0 libxcb-randr0 libxcb-glx0 \
sudo && rm -rf /var/lib/apt/lists/*
RUN apt-get update && (apt-get install -y libasound2t64 || apt-get install -y libasound2) && rm -rf /var/lib/apt/lists/*
# Python venv + Playwright + openconnect-sso
RUN python3 -m venv "$VIRTUAL_ENV"
RUN pip install --no-cache-dir openconnect-sso playwright keyring keyrings.alt && \
python -m playwright install --with-deps chromium
# Cloudflared (amd64)
RUN arch=$(dpkg --print-architecture) && \
if [ "$arch" = "amd64" ]; then \
curl -fsSL https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o /tmp/cloudflared.deb && \
apt-get update && apt-get install -y /tmp/cloudflared.deb && rm -f /tmp/cloudflared.deb ; \
else \
echo "Install cloudflared manually for arch=$arch" && exit 1 ; \
fi
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 6901
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,182 @@
#!/usr/bin/env bash
set -euo pipefail
: "${OC_URL:?OC_URL required}"
: "${OC_SERVERCERT:?OC_SERVERCERT required}"
NOVNC_PORT="${NOVNC_PORT:-6901}"
VNC_PASSWORD="${VNC_PASSWORD:-changeme}"
DISPLAY_ADDR="${DISPLAY:-:1}"
OC_INTERFACE="${OC_INTERFACE:-tun0}"
OC_USER="${OC_USER:-}"
OC_TOTP_SECRET="${OC_TOTP_SECRET:-}"
# Default to hidden browser if OC_USER is set
if [[ -n "$OC_USER" ]]; then
OC_SSO_ARGS_DEFAULT="--browser-display-mode hidden -u $OC_USER"
else
OC_SSO_ARGS_DEFAULT="--browser-display-mode shown"
fi
CLOUDFLARED_MODE="${CLOUDFLARED_MODE:-off}" # off|token|config
CLOUDFLARED_TOKEN="${CLOUDFLARED_TOKEN:-}"
SSH_TUNNEL_ENABLE="${SSH_TUNNEL_ENABLE:-0}"
SSH_DEST="${SSH_DEST:-zawa@10.3.1.201}"
SSH_FORWARDS="${SSH_FORWARDS:-0.0.0.0:8090:localhost:8090}"
pids=()
# Setup keyring with TOTP secret if provided
setup_keyring() {
if [[ -n "$OC_TOTP_SECRET" && -n "$OC_USER" ]]; then
python3 -c "
import keyring
keyring.set_password('openconnect-sso', 'totp/$OC_USER', '$OC_TOTP_SECRET'.upper())
print('TOTP secret stored in keyring for $OC_USER')
"
fi
}
# Create vpn_connect command in PATH and save environment
create_vpn_command() {
# Save environment variables to a file
cat > /etc/vpn.env << ENVFILE
export OC_URL="$OC_URL"
export OC_SERVERCERT="$OC_SERVERCERT"
export OC_INTERFACE="$OC_INTERFACE"
export OC_USER="$OC_USER"
export OC_SSO_ARGS_DEFAULT="$OC_SSO_ARGS_DEFAULT"
export OC_SSO_ARGS="${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT}"
export OC_AUTHGROUP="${OC_AUTHGROUP:-}"
export OC_USERAGENT="${OC_USERAGENT:-}"
export OC_EXTRA_ARGS="${OC_EXTRA_ARGS:-}"
export OC_TOTP_SECRET="$OC_TOTP_SECRET"
export DISPLAY=":1"
ENVFILE
# Build openconnect command
OPENCONNECT_CMD="/usr/sbin/openconnect --protocol=anyconnect --servercert $OC_SERVERCERT --interface $OC_INTERFACE --script /usr/share/vpnc-scripts/vpnc-script"
[[ -n "${OC_AUTHGROUP:-}" ]] && OPENCONNECT_CMD+=" --authgroup $OC_AUTHGROUP"
[[ -n "${OC_USERAGENT:-}" ]] && OPENCONNECT_CMD+=" --useragent $OC_USERAGENT"
[[ -n "${OC_EXTRA_ARGS:-}" ]] && OPENCONNECT_CMD+=" ${OC_EXTRA_ARGS}"
echo "export OPENCONNECT_CMD=\"$OPENCONNECT_CMD\"" >> /etc/vpn.env
cat > /usr/local/bin/vpn_connect << 'VPNCMD'
#!/usr/bin/env bash
source /etc/vpn.env
echo "[$(date)] Starting VPN connection..."
# openconnect-sso reads TOTP from keyring automatically
if [[ -n "$OC_USER" ]]; then
echo "" | openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
else
openconnect-sso -s "$OC_URL" ${OC_SSO_ARGS:-$OC_SSO_ARGS_DEFAULT} -- $OPENCONNECT_CMD
fi
VPNCMD
chmod +x /usr/local/bin/vpn_connect
}
# Create VPN runner script that keeps shell open
create_vpn_script() {
cat > /tmp/vpn-runner.sh << 'VPNSCRIPT'
#!/usr/bin/env bash
cd /root
echo "============================================"
echo " Cistech VPN Container"
echo "============================================"
echo ""
echo "Commands:"
echo " vpn_connect - Start/restart VPN connection"
echo " Ctrl+C - Stop auto-reconnect and drop to shell"
echo ""
echo "Starting VPN with auto-reconnect..."
echo ""
while true; do
vpn_connect
echo ""
echo "[$(date)] VPN disconnected. Reconnecting in 10 seconds..."
echo "(Press Ctrl+C to stop auto-reconnect)"
sleep 10
done
VPNSCRIPT
chmod +x /tmp/vpn-runner.sh
}
start_gui() {
mkdir -p /root/.vnc
x11vnc -storepasswd "$VNC_PASSWORD" /root/.vnc/pass >/dev/null 2>&1 || true
rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true
Xvfb "$DISPLAY_ADDR" -screen 0 ${XVFB_WxHxD:-1280x800x24} +extension RANDR &
pids+=($!)
sleep 0.5
export DISPLAY="$DISPLAY_ADDR"
fluxbox >/tmp/fluxbox.log 2>&1 &
pids+=($!)
x11vnc -display "$DISPLAY_ADDR" -rfbauth /root/.vnc/pass -forever -shared -rfbport 5900 -quiet &
pids+=($!)
websockify --web=/usr/share/novnc/ 0.0.0.0:"$NOVNC_PORT" localhost:5900 >/tmp/websockify.log 2>&1 &
pids+=($!)
}
start_vpn_terminal() {
# Start xterm with VPN script
sleep 1
xterm -fa 'Monospace' -fs 11 -bg black -fg white -geometry 120x35+50+50 \
-T "Cistech VPN" -e /tmp/vpn-runner.sh &
pids+=($!)
}
start_cloudflared() {
case "$CLOUDFLARED_MODE" in
token)
[ -n "$CLOUDFLARED_TOKEN" ] && cloudflared tunnel run --token "$CLOUDFLARED_TOKEN" >/tmp/cloudflared.log 2>&1 &
pids+=($!)
;;
config)
cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run >/tmp/cloudflared.log 2>&1 &
pids+=($!)
;;
off|*)
;;
esac
}
start_ssh_tunnel() {
[ "$SSH_TUNNEL_ENABLE" = "1" ] || return 0
IFS=',' read -ra LINES <<< "$SSH_FORWARDS"
args=(-N -o StrictHostKeyChecking=no -o ServerAliveInterval=60)
for m in "${LINES[@]}"; do args+=(-L "$m"); done
ssh "${args[@]}" "$SSH_DEST" &
pids+=($!)
}
setup_nat() {
(
for i in {1..60}; do
if ip link show "$OC_INTERFACE" >/dev/null 2>&1; then
sysctl -w net.ipv4.ip_forward=1 >/dev/null
iptables -t nat -C POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -o "$OC_INTERFACE" -j MASQUERADE
echo "NAT enabled on $OC_INTERFACE"
break
fi
sleep 2
done
) &
}
trap 'kill 0' INT TERM
# Always start GUI now
setup_keyring
create_vpn_command
create_vpn_script
start_gui
start_vpn_terminal
setup_nat
start_cloudflared
start_ssh_tunnel
wait