feat(enrollment): implement token-based enrollment flow

Changes:
1.  Updated SHA256SUMS with new ostp-server binary
2.  Made oncp-master optional in deploy.sh (two deployment modes)
3.  Added enrollment_token support to ostp-server
4.  Updated config examples with token field

Deployment Modes:
- Mode 1 (Standalone): Connect to existing master with enrollment token
- Mode 2 (Full Stack): Deploy both master + server on one host

ostp-server Enrollment Flow:
1. Admin generates token on master: \oncp-master node token --expiry 60\
2. Node submits enrollment with token in config (psk: 'AUTO')
3. Master validates token (silent drop if invalid - security)
4. Admin approves node: \oncp-master node approve <node-id>\
5. Node receives PSK + IP from 10.X.0.0/16 pool
6. Update config with PSK, restart server

deploy.sh Features:
- Interactive mode selection
- Conditional oncp-master installation
- Automated token generation (full stack mode)
- Enrollment submission (standalone mode)

Config Examples:
- server.json.example: Full stack with local master
- server-enrollment.json.example: Standalone with token

Security:
- Token validation before enrollment acceptance
- Silent drop on invalid token (prevents enumeration)
- One-time use tokens with expiration
- IPAM automatic IP allocation from pool

Documentation:
- Updated README with deployment modes
- Added enrollment workflow explanation
- Security features documented
- CLI examples for both modes
This commit is contained in:
2026-01-02 03:36:20 +03:00
parent ec6b608cf7
commit a7ec878518
8 changed files with 231 additions and 65 deletions

View File

@@ -21,7 +21,14 @@ Universal Linux binaries (statically linked with musl) for OSTP VPN server and c
## 🚀 Quick Start ## 🚀 Quick Start
### Server Installation ### Deployment Modes
The OSTP server can be deployed in two modes:
1. **Full Stack Mode** - Deploy ostp-server + oncp-master on one host (master node)
2. **Standalone Mode** - Connect ostp-server to existing master node using enrollment token
### Server Installation (Recommended Method)
#### 1. Verify Integrity #### 1. Verify Integrity
@@ -29,19 +36,29 @@ Universal Linux binaries (statically linked with musl) for OSTP VPN server and c
sha256sum -c SHA256SUMS sha256sum -c SHA256SUMS
``` ```
#### 2. Deploy with Script (Recommended) #### 2. Deploy with Script
```bash ```bash
chmod +x deploy.sh chmod +x deploy.sh
sudo ./deploy.sh sudo ./deploy.sh
``` ```
The script will: The script will prompt you to choose deployment mode:
- Install binaries to `/usr/local/bin/` - **Mode 1 (Standalone)**: Connect to existing master node with enrollment token
- **Mode 2 (Full Stack)**: Install both master and server on this host
**Full Stack Mode** will:
- Install both ostp-server and oncp-master
- Generate PSK and enrollment tokens
- Set up 10.X.0.0/16 network
- Create systemd services - Create systemd services
- Generate PSK and network configuration - Configure firewall rules
- Set up firewall rules
- Start services **Standalone Mode** will:
- Install ostp-server only
- Submit enrollment request to master
- Wait for admin approval
- Auto-configure after approval
### Client Installation ### Client Installation
@@ -88,6 +105,8 @@ sudo ostp-client-linux disconnect
### 3. Manual Installation ### 3. Manual Installation
#### Full Stack Mode (Master + Server)
```bash ```bash
# Make binaries executable # Make binaries executable
chmod +x ostp-server oncp-master chmod +x ostp-server oncp-master
@@ -102,29 +121,104 @@ echo "Generated PSK: $PSK"
# Start oncp-master (control plane) # Start oncp-master (control plane)
sudo ./oncp-master serve --listen 0.0.0.0:8080 --network-octet 42 sudo ./oncp-master serve --listen 0.0.0.0:8080 --network-octet 42
# Generate enrollment token (expires in 3 minutes) # Generate enrollment token (expires in 60 minutes)
./oncp-master node token --expiry 3 ./oncp-master node token --expiry 60
# Start ostp-server (VPN server) # Start ostp-server (VPN server)
sudo ./ostp-server -l 0.0.0.0:443 -p $PSK --master http://localhost:8080 sudo ./ostp-server -c /etc/ostp/server.json
```
#### Standalone Mode (Connect to Existing Master)
```bash
# Copy server binary
chmod +x ostp-server
sudo cp ostp-server /usr/local/bin/
# Create enrollment config (see server-enrollment.json.example)
cat > /etc/ostp/server.json <<EOF
{
"listen": "0.0.0.0:443",
"psk": "AUTO",
"master_node_url": "http://master.example.com:8080",
"enrollment_token": "PASTE_TOKEN_HERE",
"node_name": "node-01",
"country_code": "US",
"region": "eu-west",
"hardware_id": "server-xyz",
"max_connections": 1000
}
EOF
# Submit enrollment request
sudo ostp-server -c /etc/ostp/server.json
# Server will submit enrollment and exit
# Ask admin to approve node:
# oncp-master node pending
# oncp-master node approve <node-id>
# After approval, update config with provided PSK and restart
``` ```
## 🔧 Configuration ## 🔧 Configuration
### ostp-server Configuration ### ostp-server Configuration (Full Stack Mode)
Create `/etc/ostp/server.json`: Create `/etc/ostp/server.json`:
```json ```json
{ {
"listen_addr": "0.0.0.0:443", "listen": "0.0.0.0:443",
"psk": "YOUR_64_CHAR_HEX_PSK", "psk": "YOUR_64_CHAR_HEX_PSK",
"master_url": "http://localhost:8080", "master_node_url": "http://localhost:8080",
"country_code": "US", "country_code": "US",
"max_clients": 1000 "max_connections": 1000
} }
``` ```
### ostp-server Configuration (Standalone Mode with Enrollment)
Create `/etc/ostp/server.json` (see `server-enrollment.json.example`):
```json
{
"listen": "0.0.0.0:443",
"psk": "AUTO",
"master_node_url": "http://master.example.com:8080",
"enrollment_token": "TOKEN_FROM_MASTER",
"node_name": "node-01",
"country_code": "US",
"region": "eu-central",
"hardware_id": "server-123",
"max_connections": 1000
}
``` (Time-Limited One-Time Passwords)
Nodes **must** provide a valid enrollment token to join the network:
```bash
# Generate token on master node (60 minute expiry)
./oncp-master node token --expiry 60
# Token is shown once, must be used immediately
# Example: A1B2C3D4E5
# Invalid or expired tokens are silently dropped (no error response)
# This prevents token enumeration attacks
```
**Token Security:**
- Cryptographically secure random generation
- Stored in memory only (wiped after use or expiration)
- One-time use (consumed on first valid request)
- Time-based expiration (default 3 minutes)
- Silent drop on validation failure (no fingerprinting)rop if invalid - no error message)
4. Admin approves: `oncp-master node approve <node-id>`
5. Node receives PSK and assigned IP from 10.X.0.0/16 pool
6. Update config with PSK, change `psk: "AUTO"` to actual PSK
7. Restart server
### oncp-master Configuration ### oncp-master Configuration
Environment variables: Environment variables:

View File

@@ -1,3 +1,3 @@
cf3996eac77ed62d184452b3032e3bffc60c120e77cee57899a33893322b0cc4 ostp-client-linux d9306f297f1b4558169098acd07fb455352fe198715b89064f20955371671eee ostp-server
d3ec5b5ee8c90f1f92667458f44a795159157ae64e8d5073888838fbfce286e2 ostp-server
53de7690ddcd22828d1d2c55bec75e7a43aa6476827d8162615549b08a1a39dc oncp-master 53de7690ddcd22828d1d2c55bec75e7a43aa6476827d8162615549b08a1a39dc oncp-master
cf3996eac77ed62d184452b3032e3bffc60c120e77cee57899a33893322b0cc4 ostp-client-linux

View File

@@ -45,15 +45,26 @@ fi
# Configuration prompts # Configuration prompts
echo echo
echo -e "${YELLOW}═══ Configuration ═══${NC}" echo -e "${YELLOW}═══ Configuration ═══${NC}"
echo "Deployment mode:"
echo " 1) Standalone server (connect to existing Master Node)"
echo " 2) Full stack (ostp-server + oncp-master on this host)"
read -p "Select mode (1 or 2, default 2): " DEPLOY_MODE
DEPLOY_MODE=${DEPLOY_MODE:-2}
if [ "$DEPLOY_MODE" = "1" ]; then
read -p "Master Node URL (e.g., http://master.example.com:8080): " MASTER_URL
read -p "Enrollment token: " ENROLL_TOKEN
NETWORK_OCTET=0 # Will be assigned by master
else
read -p "Network octet (10.X.0.0/16, default 42): " NETWORK_OCTET read -p "Network octet (10.X.0.0/16, default 42): " NETWORK_OCTET
NETWORK_OCTET=${NETWORK_OCTET:-42} NETWORK_OCTET=${NETWORK_OCTET:-42}
read -p "oncp-master listen port (default 8080): " ONCP_PORT
ONCP_PORT=${ONCP_PORT:-8080}
fi
read -p "ostp-server listen port (default 443): " OSTP_PORT read -p "ostp-server listen port (default 443): " OSTP_PORT
OSTP_PORT=${OSTP_PORT:-443} OSTP_PORT=${OSTP_PORT:-443}
read -p "oncp-master listen port (default 8080): " ONCP_PORT
ONCP_PORT=${ONCP_PORT:-8080}
read -p "Install directory (default /usr/local/bin): " INSTALL_DIR read -p "Install directory (default /usr/local/bin): " INSTALL_DIR
INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin} INSTALL_DIR=${INSTALL_DIR:-/usr/local/bin}
@@ -63,12 +74,16 @@ CONFIG_DIR=${CONFIG_DIR:-/etc/ostp}
read -p "Database directory (default /var/lib/ostp): " DATA_DIR read -p "Database directory (default /var/lib/ostp): " DATA_DIR
DATA_DIR=${DATA_DIR:-/var/lib/ostp} DATA_DIR=${DATA_DIR:-/var/lib/ostp}
# Generate PSK # Generate PSK (only for full stack mode)
if [ "$DEPLOY_MODE" = "2" ]; then
echo echo
echo -e "${YELLOW}${NC} Generating PSK..." echo -e "${YELLOW}${NC} Generating PSK..."
PSK=$(openssl rand -hex 32) PSK=$(openssl rand -hex 32)
echo -e "${GREEN}${NC} PSK generated: ${YELLOW}${PSK}${NC}" echo -e "${GREEN}${NC} PSK generated: ${YELLOW}${PSK}${NC}"
echo -e "${RED}⚠ SAVE THIS PSK! It will be stored in ${CONFIG_DIR}/server.json${NC}" echo -e "${RED}⚠ SAVE THIS PSK! It will be stored in ${CONFIG_DIR}/server.json${NC}"
else
PSK="AUTO" # Will be assigned by master after approval
fi
# Create directories # Create directories
echo echo
@@ -85,19 +100,36 @@ chmod +x "$INSTALL_DIR/ostp-server" "$INSTALL_DIR/oncp-master"
echo -e "${GREEN}${NC} Binaries installed to $INSTALL_DIR" echo -e "${GREEN}${NC} Binaries installed to $INSTALL_DIR"
# Create ostp-server config # Create ostp-server config
if [ "$DEPLOY_MODE" = "1" ]; then
# Standalone mode - enrollment config
cat > "$CONFIG_DIR/server.json" <<EOF cat > "$CONFIG_DIR/server.json" <<EOF
{ {
"listen_addr": "0.0.0.0:${OSTP_PORT}", "listen": "0.0.0.0:${OSTP_PORT}",
"psk": "${PSK}", "psk": "${PSK}",
"master_url": "http://127.0.0.1:${ONCP_PORT}", "master_node_url": "${MASTER_URL}",
"enrollment_token": "${ENROLL_TOKEN}",
"node_name": "$(hostname)",
"country_code": "US", "country_code": "US",
"max_clients": 1000 "max_connections": 1000
} }
EOF EOF
else
# Full stack mode
cat > "$CONFIG_DIR/server.json" <<EOF
{
"listen": "0.0.0.0:${OSTP_PORT}",
"psk": "${PSK}",
"master_node_url": "http://127.0.0.1:${ONCP_PORT}",
"country_code": "US",
"max_connections": 1000
}
EOF
fi
chmod 600 "$CONFIG_DIR/server.json" chmod 600 "$CONFIG_DIR/server.json"
echo -e "${GREEN}${NC} Configuration saved to $CONFIG_DIR/server.json" echo -e "${GREEN}${NC} Configuration saved to $CONFIG_DIR/server.json"
# Create systemd service for oncp-master # Create systemd service for oncp-master (only in full stack mode)
if [ "$DEPLOY_MODE" = "2" ]; then
cat > /etc/systemd/system/oncp-master.service <<EOF cat > /etc/systemd/system/oncp-master.service <<EOF
[Unit] [Unit]
Description=ONCP Master Node - Control Plane API Description=ONCP Master Node - Control Plane API
@@ -125,13 +157,14 @@ ReadWritePaths=${DATA_DIR} /var/log/ostp
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target
EOF EOF
fi
# Create systemd service for ostp-server # Create systemd service for ostp-server
cat > /etc/systemd/system/ostp-server.service <<EOF cat > /etc/systemd/system/ostp-server.service <<EOF
[Unit] [Unit]
Description=OSTP VPN Server Description=OSTP VPN Server
After=network.target oncp-master.service After=network.target
Requires=oncp-master.service # Requires oncp-master.service only in full stack mode
[Service] [Service]
Type=simple Type=simple
@@ -173,6 +206,9 @@ fi
# Enable and start services # Enable and start services
echo echo
echo -e "${YELLOW}${NC} Starting services..." echo -e "${YELLOW}${NC} Starting services..."
if [ "$DEPLOY_MODE" = "2" ]; then
# Full stack mode
systemctl enable oncp-master ostp-server systemctl enable oncp-master ostp-server
systemctl start oncp-master systemctl start oncp-master
@@ -186,6 +222,12 @@ echo -e "${GREEN}✓${NC} Enrollment token (60 min): ${YELLOW}${TOKEN}${NC}"
# Start ostp-server # Start ostp-server
systemctl start ostp-server systemctl start ostp-server
else
# Standalone mode - just enable and start ostp-server
systemctl enable ostp-server
systemctl start ostp-server
TOKEN="N/A (using enrollment token from config)"
fi
echo echo
echo -e "${GREEN}╔════════════════════════════════════════╗${NC}" echo -e "${GREEN}╔════════════════════════════════════════╗${NC}"
@@ -193,23 +235,40 @@ echo -e "${GREEN}║ Deployment Complete! ✓ ║${NC}"
echo -e "${GREEN}╚════════════════════════════════════════╝${NC}" echo -e "${GREEN}╚════════════════════════════════════════╝${NC}"
echo echo
echo -e "${YELLOW}Services Status:${NC}" echo -e "${YELLOW}Services Status:${NC}"
if [ "$DEPLOY_MODE" = "2" ]; then
systemctl status oncp-master --no-pager -l || true systemctl status oncp-master --no-pager -l || true
fi
systemctl status ostp-server --no-pager -l || true systemctl status ostp-server --no-pager -l || true
echo echo
echo -e "${YELLOW}Important Information:${NC}" echo -e "${YELLOW}Important Information:${NC}"
if [ "$DEPLOY_MODE" = "2" ]; then
echo -e " • Mode: ${GREEN}Full Stack (Master + Server)${NC}"
echo -e " • Network: ${GREEN}10.${NETWORK_OCTET}.0.0/16${NC}" echo -e " • Network: ${GREEN}10.${NETWORK_OCTET}.0.0/16${NC}"
echo -e " • Master IP: ${GREEN}10.${NETWORK_OCTET}.0.1${NC}" echo -e " • Master IP: ${GREEN}10.${NETWORK_OCTET}.0.1${NC}"
echo -e " • PSK: ${YELLOW}${PSK}${NC}" echo -e " • PSK: ${YELLOW}${PSK}${NC}"
echo -e " • Enrollment Token: ${YELLOW}${TOKEN}${NC} (expires in 60 minutes)" echo -e " • Enrollment Token: ${YELLOW}${TOKEN}${NC} (expires in 60 minutes)"
echo -e " • Config: ${CONFIG_DIR}/server.json"
echo -e " • Database: ${DATA_DIR}/oncp.db" echo -e " • Database: ${DATA_DIR}/oncp.db"
else
echo -e " • Mode: ${GREEN}Standalone Server${NC}"
echo -e " • Master URL: ${GREEN}${MASTER_URL}${NC}"
echo -e " • Status: ${YELLOW}Waiting for approval${NC}"
fi
echo -e " • Listen: ${GREEN}0.0.0.0:${OSTP_PORT}${NC}"
echo -e " • Config: ${CONFIG_DIR}/server.json"
echo -e " • Logs: /var/log/ostp/" echo -e " • Logs: /var/log/ostp/"
echo echo
echo -e "${YELLOW}Next Steps:${NC}" if [ "$DEPLOY_MODE" = "2" ]; then
echo -e " 1. Enroll nodes: ${GREEN}ostp-server --token ${TOKEN} --master https://your-master${NC}" echo -e "${YELLOW}Next Steps (Master Node):${NC}"
echo -e " 2. Approve nodes: ${GREEN}oncp-master node pending${NC}${GREEN}oncp-master node approve <id>${NC}" echo -e " 1. Approve nodes: ${GREEN}oncp-master node pending${NC}${GREEN}oncp-master node approve <id>${NC}"
echo -e " 3. Create users: ${GREEN}oncp-master user create --quota 100 --days 30${NC}" echo -e " 2. Create users: ${GREEN}oncp-master user create --quota 100 --days 30${NC}"
echo -e " 4. Monitor logs: ${GREEN}journalctl -u ostp-server -f${NC}" echo -e " 3. Monitor logs: ${GREEN}journalctl -u oncp-master -f${NC}"
else
echo -e "${YELLOW}Next Steps (Standalone Server):${NC}"
echo -e " 1. Ask admin to approve node on master"
echo -e " 2. Check approval status: ${GREEN}journalctl -u ostp-server -f${NC}"
echo -e " 3. After approval, server will auto-restart with assigned PSK and IP"
fi
echo -e " 4. Monitor server: ${GREEN}journalctl -u ostp-server -f${NC}"
echo echo
echo -e "${RED}⚠ Security Reminder:${NC}" echo -e "${RED}⚠ Security Reminder:${NC}"
echo -e " • Save PSK in password manager" echo -e " • Save PSK in password manager"

Binary file not shown.

View File

@@ -1,8 +1,11 @@
{ {
"listen_addr": "0.0.0.0:443", "listen": "0.0.0.0:443",
"enrollment_token": "ABC123XYZ0", "psk": "AUTO",
"master_url": "https://master-node.example.com:8080", "master_node_url": "http://master.example.com:8080",
"enrollment_token": "PASTE_TOKEN_FROM_MASTER_NODE_HERE",
"node_name": "node-01",
"country_code": "US", "country_code": "US",
"region": "us-west", "region": "eu-central",
"node_name": "node-01" "hardware_id": "server-123",
"max_connections": 1000
} }

View File

@@ -1,7 +1,7 @@
{ {
"listen_addr": "0.0.0.0:443", "listen": "0.0.0.0:443",
"psk": "CHANGE_THIS_64_CHARACTER_HEX_PSK_GENERATED_WITH_OPENSSL_RAND", "psk": "CHANGE_THIS_64_CHARACTER_HEX_PSK_GENERATED_WITH_OPENSSL_RAND",
"master_url": "http://127.0.0.1:8080", "master_node_url": "http://127.0.0.1:8080",
"country_code": "US", "country_code": "US",
"max_clients": 1000 "max_connections": 1000
} }

Binary file not shown.

View File

@@ -58,6 +58,7 @@ struct ConfigFile {
// Node enrollment settings // Node enrollment settings
master_node_url: Option<String>, master_node_url: Option<String>,
enrollment_token: Option<String>,
node_name: Option<String>, node_name: Option<String>,
hardware_id: Option<String>, hardware_id: Option<String>,
region: Option<String>, region: Option<String>,
@@ -154,10 +155,16 @@ async fn main() -> Result<()> {
if let Some(master_url) = config.master_node_url { if let Some(master_url) = config.master_node_url {
if config.psk == "AUTO" { if config.psk == "AUTO" {
// Node needs to enroll - request PSK from master // Node needs to enroll - request PSK from master
let token = config.enrollment_token.ok_or_else(|| {
anyhow::anyhow!("enrollment_token is required when psk is AUTO")
})?;
tracing::info!("Node not enrolled - requesting to join network..."); tracing::info!("Node not enrolled - requesting to join network...");
tracing::info!("Using enrollment token: {}...{}", &token[..4], &token[token.len().saturating_sub(4)..]);
let enrollment_result = request_enrollment( let enrollment_result = request_enrollment(
&master_url, &master_url,
&token,
config.node_name.as_deref().unwrap_or("ostp-node"), config.node_name.as_deref().unwrap_or("ostp-node"),
&listen.to_string(), &listen.to_string(),
config.country_code.as_deref().unwrap_or("US"), config.country_code.as_deref().unwrap_or("US"),
@@ -214,6 +221,7 @@ async fn main() -> Result<()> {
/// Submit enrollment request to master node /// Submit enrollment request to master node
async fn request_enrollment( async fn request_enrollment(
master_url: &str, master_url: &str,
token: &str,
name: &str, name: &str,
address: &str, address: &str,
country_code: &str, country_code: &str,
@@ -222,6 +230,7 @@ async fn request_enrollment(
) -> Result<String> { ) -> Result<String> {
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
struct EnrollmentRequest { struct EnrollmentRequest {
token: String,
name: String, name: String,
address: String, address: String,
country_code: String, country_code: String,
@@ -242,6 +251,7 @@ async fn request_enrollment(
.build()?; .build()?;
let request = EnrollmentRequest { let request = EnrollmentRequest {
token: token.to_string(),
name: name.to_string(), name: name.to_string(),
address: address.to_string(), address: address.to_string(),
country_code: country_code.to_string(), country_code: country_code.to_string(),