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

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