feat: add WireGuard VPS setup automation script (#2810)

* feat: add WireGuard VPS setup automation script

Adds a comprehensive bash script that automates:
- SSH key setup and authentication
- WireGuard installation on remote VPS
- Configuration download and import to NetworkManager
- User-friendly CLI interface with validation
- Detailed status messages and error handling
- Instructions for exposing services via ACME/Let's Encrypt

* use cat heredoc for issue files to fix formatting

Replaces echo with cat heredoc when writing to /etc/issue and /etc/issue.net to properly preserve escape sequences and prevent unwanted newlines in login prompts.

* add convent `wg-vps-setup` symlink to PATH

* sync ssh privkey on init

* Update default ssh key location

* simplify to use existing StartOS SSH keys and fix .ssh permission

* finetune

* Switch to start9labs repo

* rename some files

* set correct ownership

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>
This commit is contained in:
Mariusz Kogen
2025-01-23 00:53:31 +01:00
committed by GitHub
parent baa4c1fd25
commit 2aaae5265a
7 changed files with 438 additions and 36 deletions

View File

@@ -25,6 +25,7 @@ use crate::context::{CliContext, InitContext};
use crate::db::model::public::ServerStatus;
use crate::db::model::Database;
use crate::disk::mount::util::unmount;
use crate::hostname::Hostname;
use crate::middleware::auth::LOCAL_AUTH_COOKIE_PATH;
use crate::net::net_controller::{NetController, NetService};
use crate::net::utils::find_wifi_iface;
@@ -35,7 +36,7 @@ use crate::progress::{
};
use crate::rpc_continuations::{Guid, RpcContinuation};
use crate::s9pk::v2::pack::{CONTAINER_DATADIR, CONTAINER_TOOL};
use crate::ssh::SSH_AUTHORIZED_KEYS_FILE;
use crate::ssh::SSH_DIR;
use crate::system::get_mem_info;
use crate::util::io::{create_file, IOHook};
use crate::util::lshw::lshw;
@@ -340,8 +341,10 @@ pub async fn init(
load_ssh_keys.start();
crate::ssh::sync_keys(
&Hostname(peek.as_public().as_server_info().as_hostname().de()?),
&peek.as_private().as_ssh_privkey().de()?,
&peek.as_private().as_ssh_pubkeys().de()?,
SSH_AUTHORIZED_KEYS_FILE,
SSH_DIR,
)
.await?;
load_ssh_keys.complete();

View File

@@ -3,20 +3,23 @@ use std::path::Path;
use clap::builder::ValueParserFactory;
use clap::Parser;
use color_eyre::eyre::eyre;
use imbl_value::InternedString;
use models::FromStrParser;
use rpc_toolkit::{from_fn_async, Context, Empty, HandlerExt, ParentHandler};
use serde::{Deserialize, Serialize};
use tokio::fs::OpenOptions;
use tokio::process::Command;
use tracing::instrument;
use ts_rs::TS;
use crate::context::{CliContext, RpcContext};
use crate::hostname::Hostname;
use crate::prelude::*;
use crate::util::io::create_file;
use crate::util::serde::{display_serializable, HandlerExtSerde, WithIoFormat};
use crate::util::serde::{display_serializable, HandlerExtSerde, Pem, WithIoFormat};
use crate::util::Invoke;
pub const SSH_AUTHORIZED_KEYS_FILE: &str = "/home/start9/.ssh/authorized_keys";
pub const SSH_DIR: &str = "/home/start9/.ssh";
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct SshKeys(BTreeMap<InternedString, WithTimeData<SshPubKey>>);
@@ -143,7 +146,7 @@ pub async fn add(ctx: RpcContext, AddParams { key }: AddParams) -> Result<SshKey
))
})
.await?;
sync_keys(&keys, SSH_AUTHORIZED_KEYS_FILE).await?;
sync_pubkeys(&keys, SSH_DIR).await?;
Ok(res)
}
@@ -175,7 +178,7 @@ pub async fn delete(
}
})
.await?;
sync_keys(&keys, SSH_AUTHORIZED_KEYS_FILE).await
sync_pubkeys(&keys, SSH_DIR).await
}
fn display_all_ssh_keys(params: WithIoFormat<Empty>, result: Vec<SshKeyResponse>) {
@@ -226,23 +229,90 @@ pub async fn list(ctx: RpcContext) -> Result<Vec<SshKeyResponse>, Error> {
}
#[instrument(skip_all)]
pub async fn sync_keys<P: AsRef<Path>>(keys: &SshKeys, dest: P) -> Result<(), Error> {
pub async fn sync_keys<P: AsRef<Path>>(
hostname: &Hostname,
privkey: &Pem<ssh_key::PrivateKey>,
pubkeys: &SshKeys,
ssh_dir: P,
) -> Result<(), Error> {
use tokio::io::AsyncWriteExt;
let dest = dest.as_ref();
let ssh_dir = dest.parent().ok_or_else(|| {
Error::new(
eyre!("SSH Key File cannot be \"/\""),
crate::ErrorKind::Filesystem,
)
})?;
let ssh_dir = ssh_dir.as_ref();
if tokio::fs::metadata(ssh_dir).await.is_err() {
tokio::fs::create_dir_all(ssh_dir).await?;
}
let mut f = create_file(dest).await?;
for key in keys.0.values() {
let id_alg = if privkey.0.algorithm().is_ed25519() {
"id_ed25519"
} else if privkey.0.algorithm().is_ecdsa() {
"id_ecdsa"
} else if privkey.0.algorithm().is_rsa() {
"id_rsa"
} else {
"id_unknown"
};
let privkey_path = ssh_dir.join(id_alg);
let mut f = OpenOptions::new()
.create(true)
.write(true)
.mode(0o600)
.open(&privkey_path)
.await
.with_ctx(|_| {
(
ErrorKind::Filesystem,
lazy_format!("create {privkey_path:?}"),
)
})?;
f.write_all(privkey.to_string().as_bytes()).await?;
f.write_all(b"\n").await?;
f.sync_all().await?;
let mut f = create_file(ssh_dir.join(id_alg).with_extension("pub")).await?;
f.write_all(
(privkey
.0
.public_key()
.to_openssh()
.with_kind(ErrorKind::OpenSsh)?
+ " start9@"
+ &*hostname.0)
.as_bytes(),
)
.await?;
f.write_all(b"\n").await?;
f.sync_all().await?;
let mut f = create_file(ssh_dir.join("authorized_keys")).await?;
for key in pubkeys.0.values() {
f.write_all(key.0.to_key_format().as_bytes()).await?;
f.write_all(b"\n").await?;
}
Command::new("chown")
.arg("-R")
.arg("start9:startos")
.arg(ssh_dir)
.invoke(ErrorKind::Filesystem)
.await?;
Ok(())
}
#[instrument(skip_all)]
pub async fn sync_pubkeys<P: AsRef<Path>>(pubkeys: &SshKeys, ssh_dir: P) -> Result<(), Error> {
use tokio::io::AsyncWriteExt;
let ssh_dir = ssh_dir.as_ref();
if tokio::fs::metadata(ssh_dir).await.is_err() {
tokio::fs::create_dir_all(ssh_dir).await?;
}
let mut f = create_file(ssh_dir.join("authorized_keys")).await?;
for key in pubkeys.0.values() {
f.write_all(key.0.to_key_format().as_bytes()).await?;
f.write_all(b"\n").await?;
}
Ok(())
}