Skip to content

Commit dec7df7

Browse files
korewaChinomadonuko
andcommitted
refactor, fix: Properly generate the auth keys for the exit node
Exit nodes that are cloud-managed will no longer work without a secret Co-authored-by: madomado <[email protected]>
1 parent a89be23 commit dec7df7

File tree

7 files changed

+70
-40
lines changed

7 files changed

+70
-40
lines changed

src/cloud/aws.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl Provisioner for AWSProvisioner {
112112
&self,
113113
auth: Secret,
114114
exit_node: ExitNode,
115+
node_password: String,
115116
) -> color_eyre::Result<ExitNodeStatus> {
116117
let provisioner = exit_node
117118
.metadata
@@ -125,9 +126,7 @@ impl Provisioner for AWSProvisioner {
125126
)
126127
})?;
127128

128-
let password = generate_password(32);
129-
130-
let cloud_init_config = generate_cloud_init_config(&password, CHISEL_PORT);
129+
let cloud_init_config = generate_cloud_init_config(&node_password, CHISEL_PORT);
131130
let user_data = base64::engine::general_purpose::STANDARD.encode(cloud_init_config);
132131

133132
let aws_api: aws_config::SdkConfig = AWSIdentity::from_secret(&auth, self.region.clone())?
@@ -229,6 +228,7 @@ impl Provisioner for AWSProvisioner {
229228
&self,
230229
auth: Secret,
231230
exit_node: ExitNode,
231+
node_password: String,
232232
) -> color_eyre::Result<ExitNodeStatus> {
233233
let aws_api: aws_config::SdkConfig = AWSIdentity::from_secret(&auth, self.region.clone())?
234234
.generate_aws_config()
@@ -268,7 +268,7 @@ impl Provisioner for AWSProvisioner {
268268
} else {
269269
warn!("No status found for exit node, creating new instance");
270270
// TODO: this should be handled by the controller logic
271-
return self.create_exit_node(auth, exit_node).await;
271+
return self.create_exit_node(auth, exit_node, node_password).await;
272272
}
273273
}
274274

src/cloud/cloud_init.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
pub fn generate_cloud_init_config(password: &str, port: u16) -> String {
1+
pub fn generate_cloud_init_config(auth_string: &str, port: u16) -> String {
22
let cloud_config = serde_json::json!({
33
"runcmd": ["curl https://i.jpillora.com/chisel! | bash", "systemctl enable --now chisel"],
44
"write_files": [{
@@ -19,14 +19,14 @@ RestartSec=1
1919
User=root
2020
# You can add any additional flags here
2121
# This example uses port 9090 for the tunnel socket. `--reverse` is required for our use case.
22-
ExecStart=/usr/local/bin/chisel server --port={port} --reverse --auth chisel:{password}
22+
ExecStart=/usr/local/bin/chisel server --port={port} --reverse --auth {auth_string}
2323
# Additional .env file for auth and secrets
2424
EnvironmentFile=-/etc/sysconfig/chisel
2525
PassEnvironment=AUTH
2626
"#)
2727
}, {
2828
"path": "/etc/sysconfig/chisel",
29-
"content": format!("AUTH=chisel:{}\n", password)
29+
"content": format!("AUTH={auth_string}\n")
3030
}]
3131
});
3232

src/cloud/digitalocean.rs

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,10 @@ impl Provisioner for DigitalOceanProvisioner {
6161
&self,
6262
auth: Secret,
6363
exit_node: ExitNode,
64+
node_password: String,
6465
) -> color_eyre::Result<ExitNodeStatus> {
65-
let password = generate_password(32);
6666

67-
// create secret for password too
68-
69-
let _secret = exit_node.generate_secret(password.clone()).await?;
70-
71-
let config = generate_cloud_init_config(&password, exit_node.spec.port);
67+
let config = generate_cloud_init_config(&node_password, exit_node.spec.port);
7268

7369
// TODO: Secret reference, not plaintext
7470
let api: DigitalOceanApi = DigitalOceanApi::new(self.get_token(auth).await?);
@@ -137,7 +133,7 @@ impl Provisioner for DigitalOceanProvisioner {
137133
droplet_ip.clone(),
138134
Some(droplet_id),
139135
);
140-
136+
141137
debug!(?exit_node, "Created exit node!!");
142138

143139
Ok(exit_node)
@@ -147,6 +143,7 @@ impl Provisioner for DigitalOceanProvisioner {
147143
&self,
148144
auth: Secret,
149145
exit_node: ExitNode,
146+
node_password: String,
150147
) -> color_eyre::Result<ExitNodeStatus> {
151148
// check if droplet exists, then update it
152149
let api: DigitalOceanApi = DigitalOceanApi::new(self.get_token(auth.clone()).await?);
@@ -172,7 +169,7 @@ impl Provisioner for DigitalOceanProvisioner {
172169
} else {
173170
warn!("No status found for exit node, creating new droplet");
174171
// TODO: this should be handled by the controller logic
175-
return self.create_exit_node(auth, exit_node).await;
172+
return self.create_exit_node(auth, exit_node, node_password).await;
176173
}
177174
}
178175

src/cloud/linode.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,9 @@ impl Provisioner for LinodeProvisioner {
5454
&self,
5555
auth: Secret,
5656
exit_node: ExitNode,
57+
node_password: String,
5758
) -> color_eyre::Result<ExitNodeStatus> {
58-
let password = generate_password(32);
59-
60-
let _secret = exit_node.generate_secret(password.clone()).await?;
61-
62-
let config = generate_cloud_init_config(&password, exit_node.spec.port);
59+
let config = generate_cloud_init_config(&node_password, exit_node.spec.port);
6360

6461
// Okay, so apparently Linode uses base64 for user_data, so let's
6562
// base64 encode the config
@@ -87,7 +84,7 @@ impl Provisioner for LinodeProvisioner {
8784

8885
let mut instance = api
8986
.create_instance(&self.region, &self.size)
90-
.root_pass(&password)
87+
.root_pass(&node_password)
9188
.label(&name)
9289
.user_data(&user_data)
9390
.tags(vec![format!("chisel-operator-provisioner:{}", provisioner)])
@@ -152,6 +149,7 @@ impl Provisioner for LinodeProvisioner {
152149
&self,
153150
auth: Secret,
154151
exit_node: ExitNode,
152+
node_password: String,
155153
) -> color_eyre::Result<ExitNodeStatus> {
156154
let api = LinodeApi::new(self.get_token(&auth).await?);
157155

@@ -178,7 +176,7 @@ impl Provisioner for LinodeProvisioner {
178176
Ok(status)
179177
} else {
180178
warn!("No instance status found, creating new instance");
181-
return self.create_exit_node(auth.clone(), exit_node).await;
179+
return self.create_exit_node(auth.clone(), exit_node, node_password).await;
182180
}
183181
}
184182
}

src/cloud/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ pub trait Provisioner {
2727
&self,
2828
auth: Secret,
2929
exit_node: ExitNode,
30+
node_password: String,
3031
) -> color_eyre::Result<ExitNodeStatus>;
3132
async fn update_exit_node(
3233
&self,
3334
auth: Secret,
3435
exit_node: ExitNode,
36+
node_password: String,
3537
) -> color_eyre::Result<ExitNodeStatus>;
3638
async fn delete_exit_node(&self, auth: Secret, exit_node: ExitNode) -> color_eyre::Result<()>;
3739
}

src/daemon.rs

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use color_eyre::Result;
2626
use futures::{FutureExt, StreamExt};
2727
use k8s_openapi::api::{
2828
apps::v1::Deployment,
29-
core::v1::{LoadBalancerIngress, LoadBalancerStatus, Service, ServiceStatus},
29+
core::v1::{LoadBalancerIngress, LoadBalancerStatus, Secret, Service, ServiceStatus},
3030
};
3131
use kube::{
3232
api::{Api, ListParams, Patch, PatchParams, ResourceExt},
@@ -47,7 +47,7 @@ use std::time::Duration;
4747
use tracing::{debug, error, info, instrument, trace, warn};
4848

4949
use crate::{
50-
cloud::Provisioner,
50+
cloud::{pwgen::generate_password, Provisioner},
5151
ops::{
5252
parse_provisioner_label_value, ExitNode, ExitNodeProvisioner, ExitNodeSpec, ExitNodeStatus,
5353
EXIT_NODE_NAME_LABEL, EXIT_NODE_PROVISIONER_LABEL,
@@ -218,7 +218,7 @@ async fn select_exit_node_local(
218218
}
219219

220220
#[instrument(skip(ctx))]
221-
/// Returns the ExitNode resource for a Service resource, either finding an existing one or creating a new one
221+
/// Generates or returns an ExitNode resource for a Service resource, either finding an existing one or creating a new one
222222
async fn exit_node_for_service(
223223
ctx: Arc<Context>,
224224
service: &Service,
@@ -258,7 +258,7 @@ async fn exit_node_for_service(
258258
return Ok(exit_node);
259259
}
260260

261-
let exit_node_tmpl = ExitNode {
261+
let mut exit_node_tmpl = ExitNode {
262262
metadata: ObjectMeta {
263263
name: Some(exit_node_name.clone()),
264264
namespace: service.namespace(),
@@ -285,6 +285,11 @@ async fn exit_node_for_service(
285285
status: None,
286286
};
287287

288+
let password = generate_password(32);
289+
let secret = exit_node_tmpl.generate_secret(password.clone()).await?;
290+
291+
exit_node_tmpl.spec.auth = Some(secret.metadata.name.unwrap());
292+
288293
let serverside = PatchParams::apply(OPERATOR_MANAGER).validation_strict();
289294

290295
let exit_node = nodes
@@ -332,6 +337,7 @@ async fn reconcile_svcs(obj: Arc<Service>, ctx: Arc<Context>) -> Result<Action,
332337
let obj = svc.clone();
333338

334339
let node_list = nodes.list(&ListParams::default().timeout(30)).await?;
340+
335341
// Find service binding of svc name/namespace?
336342
let existing_node = node_list.iter().find(|node| {
337343
node.metadata
@@ -341,6 +347,7 @@ async fn reconcile_svcs(obj: Arc<Service>, ctx: Arc<Context>) -> Result<Action,
341347
.unwrap_or(false)
342348
});
343349

350+
// XXX: Exit node manifest generation starts here
344351
let node = {
345352
if let Some(node) = existing_node {
346353
node.clone()
@@ -523,13 +530,28 @@ async fn reconcile_nodes(obj: Arc<ExitNode>, ctx: Arc<Context>) -> Result<Action
523530

524531
return Ok(Action::await_change());
525532
} else if is_managed {
526-
// XXX: What the fuck.
527533
let provisioner = obj
528534
.metadata
529535
.annotations
530536
.as_ref()
531537
.and_then(|annotations| annotations.get(EXIT_NODE_PROVISIONER_LABEL))
532538
.unwrap();
539+
540+
// We should assume that every managed exit node comes with an `auth` key, which is a reference to a Secret
541+
// that contains the password for the exit node.
542+
// If it doesn't exist, then it's probably bugged, and we should return and error
543+
let node_password = {
544+
let Some(ref node_password_secret_name) = obj.clone().spec.auth else {
545+
return Err(ReconcileError::ManagedExitNodeNoPasswordSet);
546+
};
547+
let secrets_api = Api::namespaced(ctx.client.clone(), &obj.namespace().unwrap());
548+
let secret: Secret = secrets_api.get(node_password_secret_name).await?;
549+
let Some(node_password) = secret.data.as_ref().unwrap().get("auth") else {
550+
return Err(ReconcileError::AuthFieldNotSet);
551+
};
552+
String::from_utf8_lossy(&node_password.0).to_string()
553+
};
554+
533555
trace!(?provisioner, "Provisioner");
534556
if let Some(status) = &obj.status {
535557
// Check for mismatch between annotation's provisioner and status' provisioner
@@ -592,7 +614,8 @@ async fn reconcile_nodes(obj: Arc<ExitNode>, ctx: Arc<Context>) -> Result<Action
592614

593615
let provisioner_api = provisioner.clone().spec.get_inner();
594616

595-
let secret = provisioner
617+
// API key secret, do not use for node password
618+
let api_key_secret = provisioner
596619
.find_secret()
597620
.await
598621
.map_err(|_| crate::error::ReconcileError::CloudProvisionerSecretNotFound)?
@@ -603,24 +626,26 @@ async fn reconcile_nodes(obj: Arc<ExitNode>, ctx: Arc<Context>) -> Result<Action
603626
EXIT_NODE_FINALIZER,
604627
obj.clone(),
605628
|event| async move {
606-
let m: std::prelude::v1::Result<Action, crate::error::ReconcileError> = match event
607-
{
629+
let m: Result<_, crate::error::ReconcileError> = match event {
608630
Event::Apply(node) => {
609-
let _node = {
631+
let _ = {
632+
// XXX: We should get the value of the Secret and pass it in as node_password
610633
let cloud_resource = if let Some(_status) = node.status.as_ref() {
611634
info!("Updating cloud resource for {}", node.name_any());
612635
provisioner_api
613-
.update_exit_node(secret.clone(), (*node).clone())
614-
.await
636+
.update_exit_node(api_key_secret.clone(), (*node).clone(), node_password)
637+
.await?
615638
} else {
616639
info!("Creating cloud resource for {}", node.name_any());
617640
provisioner_api
618-
.create_exit_node(secret.clone(), (*node).clone())
619-
.await
641+
.create_exit_node(api_key_secret.clone(), (*node).clone(), node_password)
642+
.await?
620643
};
644+
645+
// unwrap should be safe here since in k8s it is infallible for a Secret to not have a name
621646
// TODO: Don't replace the entire status and object, sadly JSON is better here
622647
let exitnode_patch = serde_json::json!({
623-
"status": cloud_resource?
648+
"status": cloud_resource,
624649
});
625650

626651
exit_nodes
@@ -641,7 +666,7 @@ async fn reconcile_nodes(obj: Arc<ExitNode>, ctx: Arc<Context>) -> Result<Action
641666
if is_managed {
642667
info!("Deleting cloud resource for {}", node.name_any());
643668
provisioner_api
644-
.delete_exit_node(secret, (*node).clone())
669+
.delete_exit_node(api_key_secret, (*node).clone())
645670
.await
646671
.unwrap_or_else(|e| {
647672
error!(?e, "Error deleting exit node {}", node.name_any())
@@ -650,7 +675,6 @@ async fn reconcile_nodes(obj: Arc<ExitNode>, ctx: Arc<Context>) -> Result<Action
650675
Ok(Action::requeue(Duration::from_secs(3600)))
651676
}
652677
};
653-
654678
m
655679
},
656680
)
@@ -707,7 +731,7 @@ pub async fn run() -> color_eyre::Result<()> {
707731
client: client.clone(),
708732
}),
709733
)
710-
.for_each(|_| futures::future::ready(()))
734+
.for_each(|result_value| futures::future::ready(()))
711735
.boxed(),
712736
);
713737

@@ -732,7 +756,7 @@ pub async fn run() -> color_eyre::Result<()> {
732756
error_policy_exit_node,
733757
Arc::new(Context { client }),
734758
)
735-
.for_each(|_| futures::future::ready(()))
759+
.for_each(|result_value| futures::future::ready(()))
736760
.boxed(),
737761
);
738762

src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ pub enum ReconcileError {
1616
#[error("The secret keys for the cloud provisioner were not found in the cluster")]
1717
CloudProvisionerSecretNotFound,
1818

19+
#[error("The managed exit node spec does not have a password set")]
20+
ManagedExitNodeNoPasswordSet,
21+
22+
#[error("The Secret could not be found in the resource's namespace")]
23+
SecretNotFound,
24+
25+
#[error("The `auth` field is not set in the Secret intended for the password")]
26+
AuthFieldNotSet,
27+
1928
#[error("The operator has encountered an unknown error, this is most likely a bug: {0}")]
2029
UnknownError(#[from] color_eyre::Report),
2130
}

0 commit comments

Comments
 (0)