From 6fbed5891fdd87b88dd45ad1bbcd01792e840441 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Tue, 6 Feb 2024 11:26:32 +0100 Subject: [PATCH 1/2] Add login with certificates support --- src/api/auth.rs | 1 + src/api/auth/cert.rs | 1 + src/api/auth/cert/requests.rs | 18 ++++++++++++++++++ src/auth.rs | 1 + src/auth/cert.rs | 22 ++++++++++++++++++++++ 5 files changed, 43 insertions(+) create mode 100644 src/api/auth/cert.rs create mode 100644 src/api/auth/cert/requests.rs create mode 100644 src/auth/cert.rs diff --git a/src/api/auth.rs b/src/api/auth.rs index 931ba35..b7891ba 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -1,5 +1,6 @@ pub mod approle; pub mod aws; +pub mod cert; pub mod kubernetes; pub mod oidc; pub mod userpass; diff --git a/src/api/auth/cert.rs b/src/api/auth/cert.rs new file mode 100644 index 0000000..0ad3265 --- /dev/null +++ b/src/api/auth/cert.rs @@ -0,0 +1 @@ +pub mod requests; diff --git a/src/api/auth/cert/requests.rs b/src/api/auth/cert/requests.rs new file mode 100644 index 0000000..f3575d2 --- /dev/null +++ b/src/api/auth/cert/requests.rs @@ -0,0 +1,18 @@ +use rustify_derive::Endpoint; + +/// ## Login +/// Login with the TLS certificate method and authenticate against only the named +/// certificate role. +/// +/// * Path: /auth/{self.mount}/login +/// * Method: POST +/// * Response: N/A +/// * Reference: https://developer.hashicorp.com/vault/api-docs/auth/cert#login-with-tls-certificate-method +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint(path = "/auth/{self.mount}/login", method = "POST", builder = "true")] +#[builder(setter(into, strip_option), default)] +pub struct LoginRequest { + #[endpoint(skip)] + pub mount: String, + pub cert_name: String, +} diff --git a/src/auth.rs b/src/auth.rs index 931ba35..b7891ba 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,6 @@ pub mod approle; pub mod aws; +pub mod cert; pub mod kubernetes; pub mod oidc; pub mod userpass; diff --git a/src/auth/cert.rs b/src/auth/cert.rs new file mode 100644 index 0000000..146b2b7 --- /dev/null +++ b/src/auth/cert.rs @@ -0,0 +1,22 @@ +use crate::{ + api::{self, auth::cert::requests::LoginRequest, AuthInfo}, + client::Client, + error::ClientError, +}; + +// Fetch a token with policies corresponding to the certificate. +// +// See [LoginRequest] +#[instrument(skip(client), err)] +pub async fn login( + client: &impl Client, + mount: &str, + cert_name: &str, +) -> Result { + let endpoint = LoginRequest::builder() + .mount(mount) + .cert_name(cert_name) + .build() + .unwrap(); + api::auth(client, endpoint).await +} From 6b5e4f41c829f384dd8933ab972977cd7867525a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Antoine=20Vandecr=C3=A8me?= Date: Tue, 27 Feb 2024 12:52:08 +0000 Subject: [PATCH 2/2] Add cert login test --- .github/workflows/ci.yml | 12 +- Cargo.toml | 3 + src/api/auth/cert.rs | 1 + src/api/auth/cert/requests.rs | 108 ++++++++++++ src/api/auth/cert/responses.rs | 32 ++++ src/auth/cert.rs | 85 ++++++++++ tests/cert.rs | 237 +++++++++++++++++++++++++++ tests/vault_bind_mounts_container.rs | 109 ++++++++++++ 8 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 src/api/auth/cert/responses.rs create mode 100644 tests/cert.rs create mode 100644 tests/vault_bind_mounts_container.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7218d39..16375d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: toolchain: ${{ env.RUST_TOOLCHAIN }} override: true components: rustfmt, clippy - - name: Use cache + - name: Use cache uses: Swatinem/rust-cache@v1 with: working-directory: vaultrs-login/ @@ -87,13 +87,19 @@ jobs: with: command: test args: --no-run - - name: Run cargo test + - name: Run cargo test --all-features uses: actions-rs/cargo@v1 env: RUST_TEST_THREADS: 1 with: command: test args: --all-features + - name: Run cargo test + uses: actions-rs/cargo@v1 + env: + RUST_TEST_THREADS: 1 + with: + command: test test-vaultrs-login: name: Run cargo test for vaultrs-login runs-on: ubuntu-latest @@ -136,4 +142,4 @@ jobs: override: true - uses: katyo/publish-crates@v1 with: - registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} \ No newline at end of file + registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/Cargo.toml b/Cargo.toml index 27c1be9..123311c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,8 +44,11 @@ tracing-subscriber = { version = "0.3.16", default-features = false, features = tracing-test = "0.2.4" test-log = { version = "0.2.11", features = ["trace"] } env_logger = "0.10.0" +dockertest = "0.3.0" dockertest-server = { version = "0.1.7", features = ["hashi", "database", "webserver", "cloud"] } jwt = "0.16.0" sha2 = "0.10.6" hmac = "0.12.1" serial_test = "1.0.0" +rcgen = "0.12.1" +tempfile = "3.10.1" diff --git a/src/api/auth/cert.rs b/src/api/auth/cert.rs index 0ad3265..116da0f 100644 --- a/src/api/auth/cert.rs +++ b/src/api/auth/cert.rs @@ -1 +1,2 @@ pub mod requests; +pub mod responses; diff --git a/src/api/auth/cert/requests.rs b/src/api/auth/cert/requests.rs index f3575d2..c942829 100644 --- a/src/api/auth/cert/requests.rs +++ b/src/api/auth/cert/requests.rs @@ -1,5 +1,113 @@ use rustify_derive::Endpoint; +use super::responses::{ListCaCertificateRoleResponse, ReadCaCertificateRoleResponse}; + +/// ## Create/Update CA certificate role +/// Create or update a CA certificate role. +/// +/// * Path: /auth/{self.mount}/certs/{self.name} +/// * Method: POST +/// * Response: N/A +/// * Reference: https://developer.hashicorp.com/vault/api-docs/auth/cert#create-ca-certificate-role +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "/auth/{self.mount}/certs/{self.name}", + method = "POST", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct CreateCaCertificateRoleRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub name: String, + pub certificate: String, + pub allowed_common_names: Option>, + pub allowed_dns_sans: Option>, + pub allowed_email_sans: Option>, + pub allowed_uri_sans: Option>, + pub allowed_organizational_units: Option>, + pub required_extensions: Option>, + pub allowed_metadata_extensions: Option>, + pub ocsp_enabled: Option, + pub ocsp_ca_certificates: Option, + pub ocsp_servers_override: Option>, + pub ocsp_fail_open: Option, + pub ocsp_query_all_servers: Option, + pub display_name: Option, + pub token_ttl: Option, + pub token_max_ttl: Option, + pub token_policies: Option>, + pub token_bound_cidrs: Option>, + pub token_explicit_max_ttl: Option, + pub token_no_default_policy: Option, + pub token_num_uses: Option, + pub token_period: Option, + pub token_type: Option, +} + +/// ## Read CA certificate role +/// Reads the properties of an existing CA certificate role. +/// +/// * Path: /auth/{self.mount}/certs/{self.name} +/// * Method: GET +/// * Response: [ReadCaCertificateRoleResponse] +/// * Reference: https://developer.hashicorp.com/vault/api-docs/auth/cert#read-ca-certificate-role +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "/auth/{self.mount}/certs/{self.name}", + response = "ReadCaCertificateRoleResponse", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct ReadCaCertificateRoleRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub name: String, +} + +/// ## Delete CA certificate role +/// This endpoint deletes the CA certificate role. +/// +/// * Path: /auth/{self.mount}/certs/{self.name} +/// * Method: DELETE +/// * Response: N/A +/// * Reference: https://developer.hashicorp.com/vault/api-docs/auth/cert#delete-certificate-role +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "/auth/{self.mount}/certs/{self.name}", + method = "DELETE", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct DeleteCaCertificateRoleRequest { + #[endpoint(skip)] + pub mount: String, + #[endpoint(skip)] + pub name: String, +} + +/// ## List CA certificate role +/// List available CA certificate roles. +/// +/// * Path: /auth/{self.mount}/certs +/// * Method: LIST +/// * Response: [ListCaCertificateRoleResponse] +/// * Reference: https://developer.hashicorp.com/vault/api-docs/auth/cert#list-certificate-roles +#[derive(Builder, Debug, Default, Endpoint)] +#[endpoint( + path = "/auth/{self.mount}/certs", + method = "LIST", + response = "ListCaCertificateRoleResponse", + builder = "true" +)] +#[builder(setter(into, strip_option), default)] +pub struct ListCaCertificateRoleRequest { + #[endpoint(skip)] + pub mount: String, +} + /// ## Login /// Login with the TLS certificate method and authenticate against only the named /// certificate role. diff --git a/src/api/auth/cert/responses.rs b/src/api/auth/cert/responses.rs new file mode 100644 index 0000000..6942737 --- /dev/null +++ b/src/api/auth/cert/responses.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +/// Response from executing +/// [ReadCaCertificateRoleRequest][crate::api::auth::cert::requests::ReadCaCertificateRoleRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ReadCaCertificateRoleResponse { + pub allowed_common_names: Option>, + pub allowed_dns_sans: Option>, + pub allowed_email_sans: Option>, + pub allowed_metadata_extensions: Option>, + pub allowed_organizational_units: Option>, + pub allowed_uri_sans: Option>, + pub certificate: String, + pub display_name: String, + pub required_extensions: Option>, + pub token_bound_cidrs: Vec, + pub token_explicit_max_ttl: u64, + pub token_max_ttl: u64, + pub token_no_default_policy: bool, + pub token_num_uses: u64, + pub token_period: u64, + pub token_policies: Vec, + pub token_ttl: u64, + pub token_type: String, +} + +/// Response from executing +/// [ListCaCertificateRoleRequest][crate::api::auth::cert::requests::ListCaCertificateRoleRequest] +#[derive(Deserialize, Debug, Serialize)] +pub struct ListCaCertificateRoleResponse { + pub keys: Vec, +} diff --git a/src/auth/cert.rs b/src/auth/cert.rs index 146b2b7..1985a1b 100644 --- a/src/auth/cert.rs +++ b/src/auth/cert.rs @@ -20,3 +20,88 @@ pub async fn login( .unwrap(); api::auth(client, endpoint).await } + +pub mod ca_cert_role { + use crate::{ + api::{ + self, + auth::cert::{ + requests::{ + CreateCaCertificateRoleRequest, CreateCaCertificateRoleRequestBuilder, + DeleteCaCertificateRoleRequest, ListCaCertificateRoleRequest, + ReadCaCertificateRoleRequest, + }, + responses::{ListCaCertificateRoleResponse, ReadCaCertificateRoleResponse}, + }, + }, + client::Client, + error::ClientError, + }; + + /// Deletes a CA certificate role. + /// + /// See [DeleteCaCertificateRoleRequest] + #[instrument(skip(client), err)] + pub async fn delete(client: &impl Client, mount: &str, name: &str) -> Result<(), ClientError> { + let endpoint = DeleteCaCertificateRoleRequest::builder() + .mount(mount) + .name(name) + .build() + .unwrap(); + api::exec_with_empty(client, endpoint).await + } + + /// Lists CA certificate roles. + /// + /// See [ListCaCertificateRoleRequest] + #[instrument(skip(client), err)] + pub async fn list( + client: &impl Client, + mount: &str, + ) -> Result { + let endpoint = ListCaCertificateRoleRequest::builder() + .mount(mount) + .build() + .unwrap(); + api::exec_with_result(client, endpoint).await + } + + /// Reads information about a CA certificate role. + /// + /// See [ReadCaCertificateRoleRequest] + #[instrument(skip(client), err)] + pub async fn read( + client: &impl Client, + mount: &str, + username: &str, + ) -> Result { + let endpoint = ReadCaCertificateRoleRequest::builder() + .mount(mount) + .name(username) + .build() + .unwrap(); + api::exec_with_result(client, endpoint).await + } + + /// Creates a new CA certificate role + /// + /// See [CreateCaCertificateRoleRequest] + #[instrument(skip(client, opts), err)] + pub async fn set( + client: &impl Client, + mount: &str, + name: &str, + certificate: &str, + opts: Option<&mut CreateCaCertificateRoleRequestBuilder>, + ) -> Result<(), ClientError> { + let mut t = CreateCaCertificateRoleRequest::builder(); + let endpoint = opts + .unwrap_or(&mut t) + .mount(mount) + .name(name) + .certificate(certificate) + .build() + .unwrap(); + api::exec_with_empty(client, endpoint).await + } +} diff --git a/tests/cert.rs b/tests/cert.rs new file mode 100644 index 0000000..3bf6564 --- /dev/null +++ b/tests/cert.rs @@ -0,0 +1,237 @@ +#[macro_use] +extern crate tracing; + +mod common; +mod vault_bind_mounts_container; + +use std::collections::HashMap; +use std::error::Error as _; +use std::fs; + +use dockertest_server::Test; +use rcgen::{BasicConstraints, Certificate, CertificateParams, IsCa}; +use tempfile::TempDir; +use test_log::test; +use vault_bind_mounts_container::{VaultServer, VaultServerConfig}; +use vaultrs::auth::cert::{self}; +use vaultrs::client::{Client, VaultClient, VaultClientSettingsBuilder}; +use vaultrs::error::ClientError; +use vaultrs::sys::auth; + +use crate::common::{PORT, VERSION}; + +#[test] +fn test() { + let certs = generate_certs(); + let test = new_tls_test(&certs.serialized_cert_dir); + test.run(|instance| async move { + let server: VaultServer = instance.server(); + + let ca_cert_path = certs + .serialized_cert_dir + .path() + .to_path_buf() + .join("ca_cert.pem") + .to_str() + .unwrap() + .to_string(); + let client_cert_str = certs + .client_cert + .serialize_pem_with_signer(&certs.ca_cert) + .unwrap(); + let mut data = client_cert_str.as_bytes().to_vec(); + let mut data2 = certs + .client_cert + .serialize_private_key_pem() + .as_bytes() + .to_vec(); + data.append(&mut data2); + let identity = reqwest::Identity::from_pem(&data).unwrap(); + + let client = match VaultClient::new( + VaultClientSettingsBuilder::default() + .address(format!("https://localhost:{PORT}")) + .token(server.token.clone()) + .ca_certs(vec![ca_cert_path]) + .identity(Some(identity)) + .build() + .unwrap(), + ) { + Ok(c) => c, + Err(err) => { + assert!(err + .source() + .unwrap() + .source() + .unwrap() + .to_string() + .eq("incompatible TLS identity type")); + assert!(cfg!(feature = "native-tls").eq(&true)); + return; + } + }; + let endpoint = setup(&client).await.unwrap(); + + // Test CA cert role + ca_cert_role::test_set(&client, &endpoint, client_cert_str).await; + ca_cert_role::test_read(&client, &endpoint).await; + ca_cert_role::test_list(&client, &endpoint).await; + + // Test login + test_login(&client, &endpoint).await; + + // Test delete + ca_cert_role::test_delete(&client, &endpoint).await; + }); +} + +pub async fn test_login(client: &impl Client, endpoint: &CertEndpoint) { + let res = cert::login(client, endpoint.path.as_str(), endpoint.name.as_str()).await; + assert!(res.is_ok()); +} + +pub mod ca_cert_role { + use vaultrs::{auth::cert::ca_cert_role, client::Client}; + + use crate::CertEndpoint; + + pub async fn test_delete(client: &impl Client, endpoint: &CertEndpoint) { + let res = + ca_cert_role::delete(client, endpoint.path.as_str(), endpoint.name.as_str()).await; + assert!(res.is_ok()); + } + + pub async fn test_list(client: &impl Client, endpoint: &CertEndpoint) { + let res = ca_cert_role::list(client, endpoint.path.as_str()).await; + assert!(res.is_ok()); + } + + pub async fn test_read(client: &impl Client, endpoint: &CertEndpoint) { + let res = ca_cert_role::read(client, endpoint.path.as_str(), endpoint.name.as_str()).await; + assert!(res.is_ok()); + } + + pub async fn test_set(client: &impl Client, endpoint: &CertEndpoint, certificate: String) { + let res = ca_cert_role::set( + client, + endpoint.path.as_str(), + endpoint.name.as_str(), + certificate.as_str(), + None, + ) + .await; + assert!(res.is_ok()); + } +} + +#[derive(Debug)] +pub struct CertEndpoint { + pub path: String, + pub name: String, +} + +async fn setup(client: &impl Client) -> Result { + debug!("setting up cert auth engine"); + + let path = "cert_test"; + let name = "test"; + + // Mount the cert auth engine + auth::enable(client, path, "cert", None).await?; + + Ok(CertEndpoint { + path: path.to_string(), + name: name.to_string(), + }) +} + +struct Certificates { + ca_cert: Certificate, + client_cert: Certificate, + serialized_cert_dir: TempDir, +} + +fn generate_certs() -> Certificates { + let mut ca_cert_params = CertificateParams::new([]); + ca_cert_params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + let ca_cert = Certificate::from_params(ca_cert_params).unwrap(); + + let client_cert_params = CertificateParams::new([]); + let client_cert = Certificate::from_params(client_cert_params).unwrap(); + + let server_cert_params = CertificateParams::new(["localhost".to_string()]); + let server_cert = Certificate::from_params(server_cert_params).unwrap(); + + // We need to serialize the ca and server certs so that we can mount them within the vault container + let serialized_cert_dir = tempfile::tempdir().unwrap(); + + let ca_cert_path = serialized_cert_dir.path().to_path_buf().join("ca_cert.pem"); + fs::write(ca_cert_path, ca_cert.serialize_pem().unwrap()).unwrap(); + + let server_cert_path = serialized_cert_dir + .path() + .to_path_buf() + .join("server_cert.pem"); + fs::write( + server_cert_path, + server_cert.serialize_pem_with_signer(&ca_cert).unwrap(), + ) + .unwrap(); + + let server_key_path = serialized_cert_dir + .path() + .to_path_buf() + .join("server_key.pem"); + fs::write(server_key_path, server_cert.serialize_private_key_pem()).unwrap(); + + Certificates { + ca_cert, + client_cert, + serialized_cert_dir, + } +} + +fn new_tls_test(server_certs_dir: &TempDir) -> Test { + let mut test = Test::default(); + let certs_mount_dir = "/etc/vault/certs".to_string(); + let ca_cert_mount_path = format!("{certs_mount_dir}/ca_cert.pem"); + let server_cert_mount_path = format!("{certs_mount_dir}/server_cert.pem"); + let server_key_mount_path = format!("{certs_mount_dir}/server_key.pem"); + let vault_config = HashMap::from([( + "listener", + vec![HashMap::from([( + "tcp", + HashMap::from([ + ("address", "0.0.0.0:8200".to_string()), // 8200 is hardcoded as internal port in VaultServerConfig::into_composition + ("tls_cert_file", server_cert_mount_path), + ("tls_key_file", server_key_mount_path), + ("tls_client_ca_file", ca_cert_mount_path), + ("tls_min_version", "tls12".to_string()), + ]), + )])], + )]); + + let env = HashMap::from([ + ( + "VAULT_DEV_LISTEN_ADDRESS".to_string(), + "0.0.0.0:9999".to_string(), // Setting 9999 to leave 8200 available for the listener configured in VAULT_LOCAL_CONFIG + ), + ( + "VAULT_LOCAL_CONFIG".to_string(), + serde_json::to_string(&vault_config).unwrap(), + ), + ]); + + let config = VaultServerConfig::builder() + .port(PORT) + .version(VERSION.into()) + .env(env) + .bind_mounts(HashMap::from([( + certs_mount_dir, + server_certs_dir.path().to_str().unwrap().to_string(), + )])) + .build() + .unwrap(); + test.register(config); + test +} diff --git a/tests/vault_bind_mounts_container.rs b/tests/vault_bind_mounts_container.rs new file mode 100644 index 0000000..309912c --- /dev/null +++ b/tests/vault_bind_mounts_container.rs @@ -0,0 +1,109 @@ +// This file is copied from https://github.com/jmgilman/dockertest-server/blob/master/src/servers/hashi/vault.rs +// because of https://github.com/jmgilman/dockertest-server/pull/15 + +use derive_builder::Builder; +use dockertest::{waitfor, Source}; +use dockertest_server::{common::rand_string, Config, ContainerConfig, Server}; +use std::collections::HashMap; + +const IMAGE: &str = "vault"; +const PORT: u32 = 8200; +const LOG_MSG: &str = "Development mode should NOT be used in production installations!"; +const SOURCE: Source = Source::DockerHub; + +/// Configuration for creating a Hashicorp Vault server. +/// +/// A token with root permissions will automatically be generated using the +/// `token` field. If it's omitted the token will automatically be generated. +/// +/// By default the Vault server listens on port 8200 for HTTP requests. This +/// is exposed on the container by default, but the exposed port can be +/// controlled by setting the `port` field. +/// +/// See the [Dockerhub](https://hub.docker.com/_/vault) page for more +/// information on the arguments and environment variables that can be used to +/// configure the server. +#[derive(Clone, Default, Builder)] +#[builder(default)] +pub struct VaultServerConfig { + #[builder(default = "Vec::new()")] + pub args: Vec, + #[builder(default = "HashMap::new()")] + pub env: HashMap, + #[builder(default = "dockertest_server::server::new_handle(IMAGE)")] + pub handle: String, + #[builder(default = "8200")] + pub port: u32, + #[builder(default = "15")] + pub timeout: u16, + #[builder(default = "rand_string(16)")] + pub token: String, + #[builder(default = "String::from(\"latest\")")] + pub version: String, + #[builder(default = "HashMap::new()")] + pub bind_mounts: HashMap, +} + +impl VaultServerConfig { + pub fn builder() -> VaultServerConfigBuilder { + VaultServerConfigBuilder::default() + } +} + +impl Config for VaultServerConfig { + fn into_composition(self) -> dockertest::Composition { + let ports = vec![(PORT, self.port)]; + let mut env = self.env.clone(); + env.insert(String::from("VAULT_DEV_ROOT_TOKEN_ID"), self.token.clone()); + + let timeout = self.timeout; + let wait = Box::new(waitfor::MessageWait { + message: LOG_MSG.into(), + source: waitfor::MessageSource::Stdout, + timeout, + }); + + ContainerConfig { + args: self.args, + env, + handle: self.handle, + name: IMAGE.into(), + source: SOURCE, + version: self.version, + ports: Some(ports), + wait: Some(wait), + bind_mounts: self.bind_mounts, + } + .into() + } + + fn handle(&self) -> &str { + self.handle.as_str() + } +} + +/// A running instance of a Vault server. +/// +/// The `token` field contains the root Vault token for the server. The server +/// URL which is accessible from the local host can be found in `local_address`. +/// Other running containers which need access to this server should use the +/// `address` field instead. +pub struct VaultServer { + pub external_port: u32, + pub internal_port: u32, + pub ip: String, + pub token: String, +} + +impl Server for VaultServer { + type Config = VaultServerConfig; + + fn new(config: &Self::Config, container: &dockertest::RunningContainer) -> Self { + VaultServer { + external_port: config.port, + internal_port: PORT, + ip: container.ip().to_string(), + token: config.token.clone(), + } + } +}