mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 02:11:53 +00:00
CA instead of leaf for StartTunnel (#3046)
* updated docs for CA instead of cert * generate ca instead of self-signed in start-tunnel * Fix formatting in START-TUNNEL.md installation instructions * Fix formatting in START-TUNNEL.md * fix infinite loop * add success message to install * hide loopback and bridge gateways --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
@@ -6,6 +6,14 @@ You can think of StartTunnel as "virtual router in the cloud"
|
||||
|
||||
Use it for private, remote access, to self-hosted services running on a personal server, or to expose self-hosted services to the public Internet without revealing the host server's IP address.
|
||||
|
||||
## Features
|
||||
|
||||
- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router.
|
||||
|
||||
- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique Wireguard config that must be copied, downloaded, or scanned into the device.
|
||||
|
||||
- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet.
|
||||
|
||||
## Installation
|
||||
|
||||
1. Rent a low cost VPS. For most use cases, the cheapest option should be enough.
|
||||
@@ -19,17 +27,12 @@ Use it for private, remote access, to self-hosted services running on a personal
|
||||
1. Access the VPS via SSH.
|
||||
|
||||
1. Install StartTunnel:
|
||||
|
||||
```sh
|
||||
TMP_DIR=$(mktemp -d) && (cd $TMP_DIR && wget https://github.com/Start9Labs/start-os/releases/download/v0.4.0-alpha.12/start-tunnel-0.4.0-alpha.12-68f401b_$(uname -m).deb && apt-get install -y ./start-tunnel-0.4.0-alpha.12-68f401b_$(uname -m).deb) && rm -rf $TMP_DIR && systemctl start start-tunneld && sleep 1 && start-tunnel web init
|
||||
TMP_DIR=$(mktemp -d) && (cd $TMP_DIR && wget https://github.com/Start9Labs/start-os/releases/download/v0.4.0-alpha.12/start-tunnel-0.4.0-alpha.12-68f401b_$(uname -m).deb && apt-get install -y ./start-tunnel-0.4.0-alpha.12-68f401b_$(uname -m).deb) && rm -rf $TMP_DIR && systemctl start start-tunneld && echo "Installation Succeeded"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Create Subnets**: Each subnet creates a private, virtual local area network (VLAN), similar to the LAN created by a home router.
|
||||
|
||||
- **Add Devices**: When you add a device (server, phone, laptop) to a subnet, it receives a LAN IP address on that subnet as well as a unique Wireguard config that must be copied, downloaded, or scanned into the device.
|
||||
|
||||
- **Forward Ports**: Forwarding a port creates a "reverse tunnel", exposing a specific port on a specific device to the public Internet.
|
||||
5. [Initialize the web interface](#web-interface) (recommended)
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -53,8 +56,8 @@ If you choose to enable the web interface (recommended in most cases), StartTunn
|
||||
|
||||
1. Select whether to autogenerate a self-signed certificate or provide your own certificate and key. If you choose to autogenerate, you will be asked to list all IP addresses and domains for which to sign the certificate. For example, if you intend to access your StartTunnel web UI at a domain, include the domain in the list.
|
||||
|
||||
1. You will receive a success message that the webserver is running at the chosen IP:port, as well as your SSL certificate and an autogenerated UI password.
|
||||
1. You will receive a success message with 3 pieces of information:
|
||||
|
||||
1. If not already, trust the certificate in your system keychain and/or browser.
|
||||
|
||||
1. If you lose/forget your password, you can reset it using the CLI.
|
||||
- <https://IP:port>: the URL where you can reach your personal web interface.
|
||||
- Password: an autogenerated password for your interface. If you lose/forget it, you can reset using the CLI.
|
||||
- Root Certificate Authority: the Root CA of your StartTunnel instance. If not already, trust it in your browser or system keychain.
|
||||
|
||||
@@ -251,12 +251,16 @@ impl CertPair {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn root_ca_start_time() -> Result<SystemTime, Error> {
|
||||
Ok(if check_time_is_synchronized().await? {
|
||||
pub async fn root_ca_start_time() -> SystemTime {
|
||||
if check_time_is_synchronized()
|
||||
.await
|
||||
.log_err()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
SystemTime::now()
|
||||
} else {
|
||||
*SOURCE_DATE
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const EC_CURVE_NAME: nid::Nid = nid::Nid::X9_62_PRIME256V1;
|
||||
|
||||
@@ -499,7 +499,7 @@ async fn fresh_setup(
|
||||
..
|
||||
}: SetupExecuteProgress,
|
||||
) -> Result<(SetupResult, RpcContext), Error> {
|
||||
let account = AccountInfo::new(start_os_password, root_ca_start_time().await?)?;
|
||||
let account = AccountInfo::new(start_os_password, root_ca_start_time().await)?;
|
||||
let db = ctx.db().await?;
|
||||
let kiosk = Some(kiosk.unwrap_or(true)).filter(|_| &*PLATFORM != "raspberrypi");
|
||||
sync_kiosk(kiosk).await?;
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
use std::collections::VecDeque;
|
||||
use std::net::{IpAddr, Ipv6Addr, SocketAddr};
|
||||
use std::net::{IpAddr, SocketAddr};
|
||||
use std::sync::Arc;
|
||||
|
||||
use clap::Parser;
|
||||
use hickory_client::proto::rr::rdata::cert;
|
||||
use imbl_value::{InternedString, json};
|
||||
use itertools::Itertools;
|
||||
use openssl::pkey::{PKey, Private};
|
||||
@@ -12,7 +11,6 @@ use rpc_toolkit::{
|
||||
Context, Empty, HandlerArgs, HandlerExt, ParentHandler, from_fn_async, from_fn_async_local,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
use tokio_rustls::rustls::ServerConfig;
|
||||
use tokio_rustls::rustls::crypto::CryptoProvider;
|
||||
use tokio_rustls::rustls::pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
|
||||
@@ -20,7 +18,8 @@ use tokio_rustls::rustls::server::ClientHello;
|
||||
use ts_rs::TS;
|
||||
|
||||
use crate::context::CliContext;
|
||||
use crate::net::ssl::SANInfo;
|
||||
use crate::hostname::Hostname;
|
||||
use crate::net::ssl::{SANInfo, root_ca_start_time};
|
||||
use crate::net::tls::TlsHandler;
|
||||
use crate::net::web_server::Accept;
|
||||
use crate::prelude::*;
|
||||
@@ -134,7 +133,7 @@ pub fn web_api<C: Context>() -> ParentHandler<C> {
|
||||
.subcommand(
|
||||
"generate-certificate",
|
||||
from_fn_async(generate_certificate)
|
||||
.with_about("Generate a self signed certificaet to use for the webserver")
|
||||
.with_about("Generate a certificate to use for the webserver")
|
||||
.with_call_remote::<CliContext>(),
|
||||
)
|
||||
.subcommand(
|
||||
@@ -286,11 +285,21 @@ pub struct GenerateCertParams {
|
||||
pub async fn generate_certificate(
|
||||
ctx: TunnelContext,
|
||||
GenerateCertParams { subject }: GenerateCertParams,
|
||||
) -> Result<Pem<X509>, Error> {
|
||||
) -> Result<Pem<Vec<X509>>, Error> {
|
||||
let saninfo = SANInfo::new(&subject.into_iter().collect());
|
||||
|
||||
let root_key = crate::net::ssl::generate_key()?;
|
||||
let root_cert = crate::net::ssl::make_root_cert(
|
||||
&root_key,
|
||||
&Hostname("start-tunnel".into()),
|
||||
root_ca_start_time().await,
|
||||
)?;
|
||||
let int_key = crate::net::ssl::generate_key()?;
|
||||
let int_cert = crate::net::ssl::make_int_cert((&root_key, &root_cert), &int_key)?;
|
||||
|
||||
let key = crate::net::ssl::generate_key()?;
|
||||
let cert = crate::net::ssl::make_self_signed((&key, &saninfo))?;
|
||||
let cert = crate::net::ssl::make_leaf_cert((&int_key, &int_cert), (&key, &saninfo))?;
|
||||
let chain = Pem(vec![cert, int_cert, root_cert]);
|
||||
|
||||
ctx.db
|
||||
.mutate(|db| {
|
||||
@@ -298,13 +307,13 @@ pub async fn generate_certificate(
|
||||
.as_certificate_mut()
|
||||
.ser(&Some(TunnelCertData {
|
||||
key: Pem(key),
|
||||
cert: Pem(vec![cert.clone()]),
|
||||
cert: chain.clone(),
|
||||
}))
|
||||
})
|
||||
.await
|
||||
.result?;
|
||||
|
||||
Ok(Pem(cert))
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
pub async fn get_certificate(ctx: TunnelContext) -> Result<Option<Pem<Vec<X509>>>, Error> {
|
||||
@@ -501,8 +510,12 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
||||
let cert = from_value::<Pem<Vec<X509>>>(
|
||||
ctx.call_remote::<TunnelContext>("web.get-certificate", json!({}))
|
||||
.await?,
|
||||
)?;
|
||||
println!("📝 SSL Certificate:");
|
||||
)?
|
||||
.0
|
||||
.pop()
|
||||
.map(Pem)
|
||||
.or_not_found("certificate in chain")?;
|
||||
println!("📝 Root SSL Certificate:");
|
||||
print!("{cert}");
|
||||
|
||||
println!(concat!(
|
||||
@@ -594,7 +607,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
||||
impl std::fmt::Display for Choice {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Generate => write!(f, "Generate a Self Signed Certificate"),
|
||||
Self::Generate => write!(f, "Generate an SSL certificate"),
|
||||
Self::Provide => write!(f, "Provide your own certificate and key"),
|
||||
}
|
||||
}
|
||||
@@ -602,7 +615,7 @@ pub async fn init_web(ctx: CliContext) -> Result<(), Error> {
|
||||
let options = vec![Choice::Generate, Choice::Provide];
|
||||
let choice = choose(
|
||||
concat!(
|
||||
"Select whether to autogenerate a self-signed SSL certificate ",
|
||||
"Select whether to generate an SSL certificate ",
|
||||
"or provide your own certificate and key:"
|
||||
),
|
||||
&options,
|
||||
|
||||
@@ -56,7 +56,7 @@ export function getIp({ clients, range }: MappedSubnet) {
|
||||
const net = IpNet.parse(range)
|
||||
const last = net.broadcast()
|
||||
|
||||
for (let ip = net.add(1); ip.cmp(last) === -1; ip.add(1)) {
|
||||
for (let ip = net.add(1); ip.cmp(last) === -1; ip = ip.add(1)) {
|
||||
if (!clients[ip.address]) {
|
||||
return ip.address
|
||||
}
|
||||
|
||||
@@ -228,24 +228,27 @@ export class PublicDomainService {
|
||||
private gatewayAndAuthoritySpec() {
|
||||
const data = this.data()!
|
||||
|
||||
const gateways = data.gateways.filter(
|
||||
({ ipInfo: { deviceType } }) =>
|
||||
deviceType !== 'loopback' && deviceType !== 'bridge',
|
||||
)
|
||||
|
||||
return {
|
||||
gateway: ISB.Value.dynamicSelect(() => ({
|
||||
name: this.i18n.transform('Gateway'),
|
||||
description: this.i18n.transform(
|
||||
'Select a gateway to use for this domain.',
|
||||
),
|
||||
values: data.gateways.reduce<Record<string, string>>(
|
||||
values: gateways.reduce<Record<string, string>>(
|
||||
(obj, gateway) => ({
|
||||
...obj,
|
||||
[gateway.id]: gateway.name || gateway.ipInfo!.name,
|
||||
[gateway.id]: gateway.name || gateway.ipInfo.name,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
default: '',
|
||||
disabled: data.gateways
|
||||
.filter(
|
||||
g => !g.ipInfo!.wanIp || utils.CGNAT.contains(g.ipInfo!.wanIp),
|
||||
)
|
||||
disabled: gateways
|
||||
.filter(g => !g.ipInfo.wanIp || utils.CGNAT.contains(g.ipInfo.wanIp))
|
||||
.map(g => g.id),
|
||||
})),
|
||||
authority: ISB.Value.select({
|
||||
|
||||
Reference in New Issue
Block a user