init
Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
This commit is contained in:
commit
5095bcdcdd
17 changed files with 6242 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
2683
Cargo.lock
generated
Normal file
2683
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
54
Cargo.toml
Normal file
54
Cargo.toml
Normal file
|
@ -0,0 +1,54 @@
|
|||
[package]
|
||||
name = "replikey"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.20", features = ["derive"] }
|
||||
env_logger = "0.11.5"
|
||||
log = "0.4"
|
||||
rand_core = { version = "0.6.4", features = ["getrandom"] }
|
||||
x509-parser = "0.16.0"
|
||||
thiserror = "1.0.66"
|
||||
|
||||
time = { version = "0.3.36", optional = true }
|
||||
aes-gcm = { version = "0.10.3", optional = true }
|
||||
sha2 = { version = "0.10.8", optional = true }
|
||||
argon2 = { version = "0.5.3", optional = true }
|
||||
rpassword = { version = "7.3.1", optional = true }
|
||||
rcgen = { version = "0.13.1", optional = true, features = ["crypto", "pem", "x509-parser"] }
|
||||
pem-rfc7468 = { version = "0.7.0", features = ["alloc"], optional = true }
|
||||
toml = { version = "0.8.19", optional = true }
|
||||
reqwest = { version = "0.12.9", optional = true, default-features = false, features = ["rustls-tls"] }
|
||||
openssl = { version = "0.10.68", optional = true }
|
||||
tokio-rustls = { version = "0.26.0", optional = true }
|
||||
serde = { version = "1.0.214", features = ["derive"], optional = true }
|
||||
sqlx = { version = "0.8.2", optional = true, default-features = false, features = ["tls-none", "postgres"] }
|
||||
tokio = { version = "1.41.0", features = ["rt", "rt-multi-thread", "macros", "net", "io-util", "sync"], optional = true }
|
||||
rustls = { version = "0.23.16", optional = true }
|
||||
async-compression = { version = "0.4.17", optional = true, features = ["tokio", "zstd"] }
|
||||
|
||||
[features]
|
||||
default = ["keygen", "networking", "service", "remote-crl", "setup-postgres"]
|
||||
asyncio = ["dep:tokio"]
|
||||
keygen = ["dep:rcgen", "dep:pem-rfc7468", "dep:rpassword", "dep:argon2", "dep:sha2", "dep:aes-gcm", "dep:time"]
|
||||
networking = ["asyncio", "dep:tokio-rustls", "dep:rustls", "dep:async-compression"]
|
||||
test-crosscheck-openssl = ["dep:openssl"]
|
||||
serde = ["dep:serde"]
|
||||
service = ["serde", "networking", "dep:toml"]
|
||||
remote-crl = ["dep:reqwest"]
|
||||
setup-postgres = ["dep:sqlx"]
|
||||
stat-service = ["networking", "serde"]
|
||||
rustls = ["dep:rustls"]
|
||||
async-compression = ["dep:async-compression"]
|
||||
|
||||
[[bin]]
|
||||
name = "replikey"
|
||||
path = "src/bin/replikey.rs"
|
||||
required-features = ["keygen"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.13.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
201
LICENSE
Normal file
201
LICENSE
Normal file
|
@ -0,0 +1,201 @@
|
|||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [2024] [Yumechi yume@yumechi.jp]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
151
README.md
Normal file
151
README.md
Normal file
|
@ -0,0 +1,151 @@
|
|||
# replikey
|
||||
|
||||
Misskey logical replication tool for replication over insecure connections.
|
||||
|
||||
Current development status:
|
||||
- Automated tested: Step 1,2,3,4,5
|
||||
- Works on my machine: Step 6,7,8
|
||||
- Docs: Later :)
|
||||
|
||||
## Architecture
|
||||
|
||||
This is essentially a DB configuration tool and a Web PKI CA workflow and an mTLS proxy combined into one with feature flags to enable or disable each part.
|
||||
|
||||
Network architecture is as follows:
|
||||
|
||||
![Network Architecture](./doc/architecture.svg)
|
||||
|
||||
For postgres the tool has automation for setting up official logical replication, for redis it was just one command so didn't bother automating it.
|
||||
|
||||
## Setup Workflow
|
||||
|
||||
Overview, we want to do these:
|
||||
|
||||
1. Create a root CA for authenticating the server and client
|
||||
2. Create a server certificate for the Misskey instance
|
||||
3. Create a client certificate for the replication client
|
||||
4. Sign the server and client certificates
|
||||
5. Set up Postgres for replication
|
||||
6. Test the connection
|
||||
7. Integrate the program into docker-compose
|
||||
8. Start the replication
|
||||
|
||||
### 1. Create a root CA (you will be prompted for a password to encrypt the key)
|
||||
|
||||
```sh
|
||||
replikey cert create-ca --valid-days 1825 --dn-common-name "MyInstance Replication Root Certificate Authority" -o ca-certs
|
||||
```
|
||||
|
||||
### 2. Create a server CSR, SAN can be any number of combinations of DNS and IP addresses
|
||||
|
||||
|
||||
If you use DNS name SAN, all SNIs you later use must match one of the DNS name or wildcard in the SAN
|
||||
If you use IP address SAN, all connections (supposedly) to your IP address will be considered from your server
|
||||
|
||||
```sh
|
||||
replikey cert create-server --valid-days 365 --dn-common-name "MyInstance Production Server" -d '*.replication.myinstance.com' --ip-address 123.123.123.123 -o server-certs
|
||||
```
|
||||
|
||||
### 3. Sign the server CSR
|
||||
|
||||
```sh
|
||||
replikey cert sign-server-csr --valid-days 365 --ca-dir ca-certs --input-csr server-certs/server.csr --output server-certs-signed.pem
|
||||
|
||||
Enter password:
|
||||
CSR Params:
|
||||
Serial number: 7b6a82c3d9171f7ba8fbd8973aac0146dac611dd
|
||||
SAN: DNS=*.replication.myinstance.com
|
||||
SAN: IP=123.123.123.123
|
||||
Not before: 2024-11-02 22:43:56.751788095 +00:00:00
|
||||
Not after: 2025-11-02 22:43:56.751783366 +00:00:00
|
||||
Distinguished name: DistinguishedName { entries: {CommonName: Utf8String("MyInstance Production Server")}, order: [CommonName] }
|
||||
Key usages: [DigitalSignature, DataEncipherment]
|
||||
Extended key usages: [ServerAuth]
|
||||
CRL distribution points: []
|
||||
Do you want to sign this CSR? (YES/NO)
|
||||
IMPORTANT: Keep this certificate or its serial number for revocation
|
||||
```
|
||||
|
||||
### 4. Create a client CSR (for each client)
|
||||
|
||||
Ideally the workflow is the client should generate their own CSR and send it to you, you sign the certificate and send it back to them.
|
||||
|
||||
```sh
|
||||
replikey cert create-client --valid-days 365 \
|
||||
--dn-common-name "MyInstance Replication Client" \
|
||||
-o client-certs
|
||||
```
|
||||
|
||||
### 5. Sign the client CSR (for each client)
|
||||
|
||||
```sh
|
||||
replikey cert sign-client-csr --valid-days 365 \
|
||||
--ca-dir ca-certs \
|
||||
--input-csr client-certs/client.csr \
|
||||
--output client-certs-signed.pem
|
||||
```
|
||||
|
||||
### BTW.0 Later if you want to revoke a certificate, generate a CRL with the following command, then pass a URL or path to the CRL(s) to any networking command via the --crl option
|
||||
replikey cert generate-crl --ca-dir ca-certs --serial abcdef --serial 123456 --output revoked.crl
|
||||
|
||||
### 6. Check your certificates can communicate, this is just a zstd wrapper around rustls, so you should be able to use any TLS client or server
|
||||
|
||||
```sh
|
||||
replikey network reverse-proxy --listen 0.0.0.0:8443 \
|
||||
--redis-sni localhost --redis-target 127.0.0.1:22 \
|
||||
--postgres-sni postgres --postgres-target 127.0.0.1:8441 \
|
||||
--cert server-signed.pem --key test-server/server.key \
|
||||
--ca test-ca/ca.pem &
|
||||
|
||||
# this SNI MUST match one of the dns name in the server certificate or the IP address is signed (not recommended)
|
||||
replikey network forward-proxy --listen 0.0.0.0:8444 \
|
||||
--sni localhost --target localhost:8443 \
|
||||
--cert client-signed.pem --key test-client/client.key \
|
||||
--ca test-ca/ca.pem &
|
||||
|
||||
ssh -p8444 localhost # this should work
|
||||
```
|
||||
|
||||
### 7. Prepare the replication server for connection
|
||||
|
||||
Login to your master Misskey instance postgres and create a user for connection. You do not have to and should not grant any permissions to the replication user
|
||||
|
||||
```sql
|
||||
CREATE ROLE replication WITH REPLICATION LOGIN ENCRYPTED PASSWORD 'password';
|
||||
```
|
||||
|
||||
### BTW.1 Table names for checking replication status
|
||||
|
||||
```
|
||||
pg_catalog.pg_publication
|
||||
pg_catalog.pg_subscription
|
||||
pg_catalog.pg_stat_subscription
|
||||
```
|
||||
|
||||
### 8. Create postgres publication on the master side
|
||||
|
||||
```sh
|
||||
# DATABASE_URL should be _the_ connection string Misskey uses to connect to the database
|
||||
replikey setup-postgres-master setup --must-not-exist --publication "my_name"
|
||||
replikey setup-postgres-master drop-table --publication "my_name" -t auth_session -t password_reset_request -t access_token
|
||||
```
|
||||
|
||||
|
||||
### 9. Prepare postgres slave on the slave side
|
||||
|
||||
```sh
|
||||
# DATABASE_URL should be any valid connection string to the master database, probably the user you created in step 7
|
||||
replikey setup-postgres-slave setup --must-not-exist --subscription "my_subscription_name" --publication "my_name"
|
||||
```
|
||||
|
||||
### 10. Set redis slave to replicate from the master
|
||||
|
||||
```sh
|
||||
# replace REDIS_PROXY with the address of the redis TLS proxy listener
|
||||
# replace PORT with the port of the redis TLS proxy listener
|
||||
redis-cli 'REPLICAOF REDIS_PROXY PORT'
|
||||
```
|
||||
|
||||
### Integration into docker-compose:
|
||||
|
||||
WIP, but I have `replikey service` subcommand for running the proxies with environment variables or config files and optionally set up the replication on startup.
|
250
deny.toml
Normal file
250
deny.toml
Normal file
|
@ -0,0 +1,250 @@
|
|||
# This template contains all of the possible sections and their default values
|
||||
|
||||
# Note that all fields that take a lint level have these possible values:
|
||||
# * deny - An error will be produced and the check will fail
|
||||
# * warn - A warning will be produced, but the check will not fail
|
||||
# * allow - No warning or error will be produced, though in some cases a note
|
||||
# will be
|
||||
|
||||
# The values provided in this template are the default values that will be used
|
||||
# when any section or field is not specified in your own configuration
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
# dependency, such as, for example, the `nix` crate only being used via the
|
||||
# `target_family = "unix"` configuration, that only having windows targets in
|
||||
# this list would mean the nix crate, as well as any of its exclusive
|
||||
# dependencies not shared by any other crates, would be ignored, as the target
|
||||
# list here is effectively saying which targets you are building for.
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
# from the view of cargo-deny. This is an extremely heavy hammer, as if a crate
|
||||
# is pruned from the graph, all of its dependencies will also be pruned unless
|
||||
# they are connected to another crate in the graph that hasn't been pruned,
|
||||
# so it should be used with care. The identifiers are [Package ID Specifications]
|
||||
# (https://doc.rust-lang.org/cargo/reference/pkgid-spec.html)
|
||||
#exclude = []
|
||||
# If true, metadata will be collected with `--all-features`. Note that this can't
|
||||
# be toggled off if true, if you want to conditionally enable `--all-features` it
|
||||
# is recommended to pass `--all-features` on the cmd line instead
|
||||
all-features = true
|
||||
# If true, metadata will be collected with `--no-default-features`. The same
|
||||
# caveat with `all-features` applies
|
||||
no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
# of features from the crate(s) to all of the graph roots can be far too verbose.
|
||||
# This option can be overridden via `--feature-depth` on the cmd line
|
||||
feature-depth = 1
|
||||
|
||||
# This section is considered when running `cargo deny check advisories`
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
# See Git Authentication for more information about setting up git authentication.
|
||||
#git-fetch-with-cli = true
|
||||
|
||||
# This section is considered when running `cargo deny check licenses`
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
allow = [
|
||||
"CC0-1.0",
|
||||
"MIT",
|
||||
"MPL-2.0",
|
||||
"Apache-2.0",
|
||||
"Unicode-DFS-2016",
|
||||
"ISC",
|
||||
"BSD-3-Clause",
|
||||
"OpenSSL",
|
||||
"Zlib",
|
||||
]
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
# [possible values: any between 0.0 and 1.0].
|
||||
confidence-threshold = 0.8
|
||||
# Allow 1 or more licenses on a per-crate basis, so that particular licenses
|
||||
# aren't accepted for every possible crate as with the normal allow list
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
#[[licenses.clarify]]
|
||||
# The package spec the clarification applies to
|
||||
#crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
#expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
# the license expression. If the contents match, the clarification will be used
|
||||
# when running the license check, otherwise the clarification will be ignored
|
||||
# and the crate will be checked normally, which may produce warnings or errors
|
||||
# depending on the rest of your configuration
|
||||
#license-files = [
|
||||
# Each entry is a crate relative path, and the (opaque) hash of its contents
|
||||
#{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
#]
|
||||
|
||||
[[licenses.clarify]]
|
||||
crate = "ring"
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
license-files = [
|
||||
{ path = "LICENSE", hash = 0xbd0eed23 }
|
||||
]
|
||||
|
||||
[licenses.private]
|
||||
# If true, ignores workspace crates that aren't published, or are only
|
||||
# published to private registries.
|
||||
# To see how to mark a crate as unpublished (to the official registry),
|
||||
# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field.
|
||||
ignore = false
|
||||
# One or more private registries that you might publish crates to, if a crate
|
||||
# is only published to private registries, and ignore is true, the crate will
|
||||
# not have its license(s) checked
|
||||
registries = [
|
||||
#"https://sekretz.com/registry
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check bans`.
|
||||
# More documentation about the 'bans' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html
|
||||
[bans]
|
||||
# Lint level for when multiple versions of the same crate are detected
|
||||
multiple-versions = "warn"
|
||||
# Lint level for when a crate version requirement is `*`
|
||||
wildcards = "allow"
|
||||
# The graph highlighting used when creating dotgraphs for crates
|
||||
# with multiple versions
|
||||
# * lowest-version - The path to the lowest versioned duplicate is highlighted
|
||||
# * simplest-path - The path to the version with the fewest edges is highlighted
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
]
|
||||
|
||||
# List of features to allow/deny
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
#allow = [
|
||||
# "rustls",
|
||||
# "__rustls",
|
||||
# "__tls",
|
||||
# "hyper-rustls",
|
||||
# "rustls",
|
||||
# "rustls-pemfile",
|
||||
# "rustls-tls-webpki-roots",
|
||||
# "tokio-rustls",
|
||||
# "webpki-roots",
|
||||
#]
|
||||
# If true, the allowed features must exactly match the enabled feature set. If
|
||||
# this is set there is no point setting `deny`
|
||||
#exact = true
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
"hashbrown",
|
||||
"sync_wrapper",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
[sources]
|
||||
# Lint level for what to happen when a crate from a crate registry that is not
|
||||
# in the allow list is encountered
|
||||
unknown-registry = "warn"
|
||||
# Lint level for what to happen when a crate from a git repository that is not
|
||||
# in the allow list is encountered
|
||||
unknown-git = "warn"
|
||||
# List of URLs for allowed crate registries. Defaults to the crates.io index
|
||||
# if not specified. If it is specified but empty, no registries are allowed.
|
||||
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||
# List of URLs for allowed Git repositories
|
||||
allow-git = []
|
||||
|
||||
[sources.allow-org]
|
||||
# 1 or more github.com organizations to allow git sources for
|
||||
github = []
|
||||
# 1 or more gitlab.com organizations to allow git sources for
|
||||
gitlab = []
|
||||
# 1 or more bitbucket.org organizations to allow git sources for
|
||||
bitbucket = []
|
60
doc/architecture.gv
Normal file
60
doc/architecture.gv
Normal file
|
@ -0,0 +1,60 @@
|
|||
digraph {
|
||||
subgraph cluster_pki {
|
||||
label="PKI"
|
||||
ca [label="CA Key", shape=note]
|
||||
|
||||
subgraph cluster_pki_crl {
|
||||
label="CRL Infrastructure(Optional)"
|
||||
crl_listener [label="http://my.crl", shape=triangle,rank=0]
|
||||
crl -> crl_listener [label="Static file"]
|
||||
}
|
||||
}
|
||||
|
||||
subgraph cluster_0 {
|
||||
label="Master docker compose"
|
||||
web_app [label="Web app", shape=box]
|
||||
db [label="Postgres", shape=box]
|
||||
redis [label="Redis", shape=box]
|
||||
replikey [label="Replikey", shape=box]
|
||||
replikey -> db [label="SNI Routing",color=orange]
|
||||
replikey -> redis [label="SNI Routing",color=orange]
|
||||
server_cert [label="Server cert", shape=note]
|
||||
server_key [label="Server key", shape=note]
|
||||
server_key -> server_cert [label="Private key"]
|
||||
web_app -> db
|
||||
web_app -> redis
|
||||
|
||||
ca_cert [label="CA cert", shape=note]
|
||||
|
||||
server_cert -> replikey [label="Authenticate"]
|
||||
ca_cert -> replikey [label="Trust"]
|
||||
|
||||
listen_master_web [label=":80", shape=triangle,rank=0]
|
||||
listen_master_replikey [label=":6443", shape=triangle,rank=0]
|
||||
replikey -> listen_master_replikey [label="Listen",dir=back]
|
||||
web_app -> listen_master_web [label="Listen"]
|
||||
}
|
||||
|
||||
subgraph cluster_1 {
|
||||
label="Slave docker compose"
|
||||
db_slave [label="Postgres", shape=box]
|
||||
redis_slave [label="Redis", shape=box]
|
||||
replikey_slave_db [label="Replikey DB Client", shape=box]
|
||||
replikey_slave_redis [label="Replikey Redis Client", shape=box]
|
||||
db_slave -> replikey_slave_db [label="Plain TCP",color=orange]
|
||||
redis_slave -> replikey_slave_redis [label="Plain TCP",color=orange]
|
||||
client_cert [label="Client cert", shape=note]
|
||||
client_key [label="Client key", shape=note]
|
||||
client_key -> client_cert [label="Private key"]
|
||||
ca_cert_slave [label="CA cert", shape=note]
|
||||
|
||||
client_cert -> replikey_slave_db [label="Authenticate"]
|
||||
ca_cert_slave -> replikey_slave_db [label="Trust"]
|
||||
client_cert -> replikey_slave_redis [label="Authenticate"]
|
||||
ca_cert_slave -> replikey_slave_redis [label="Trust"]
|
||||
|
||||
}
|
||||
|
||||
replikey_slave_db -> listen_master_replikey [label="TLS with SNI",constraint=false,color=green]
|
||||
replikey_slave_redis -> listen_master_replikey [label="TLS with SNI",constraint=false,color=green]
|
||||
}
|
385
doc/architecture.svg
Normal file
385
doc/architecture.svg
Normal file
|
@ -0,0 +1,385 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<!-- Generated by graphviz version 12.1.2 (0)
|
||||
-->
|
||||
<!-- Pages: 1 -->
|
||||
<svg width="1229pt" height="431pt"
|
||||
viewBox="0.00 0.00 1229.00 430.50" xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g id="graph0" class="graph" transform="scale(1 1) rotate(0) translate(4 426.5)">
|
||||
<polygon fill="white" stroke="none" points="-4,4 -4,-426.5 1225,-426.5 1225,4 -4,4" />
|
||||
<g id="clust1" class="cluster">
|
||||
<title>cluster_pki</title>
|
||||
<polygon fill="none" stroke="black" points="8,-196 8,-414.5 336,-414.5 336,-196 8,-196" />
|
||||
<text text-anchor="middle" x="172" y="-397.2" font-family="Times,serif"
|
||||
font-size="14.00">PKI</text>
|
||||
</g>
|
||||
<g id="clust2" class="cluster">
|
||||
<title>cluster_pki_crl</title>
|
||||
<polygon fill="none" stroke="black" points="16,-204 16,-382 260,-382 260,-204 16,-204" />
|
||||
<text text-anchor="middle" x="138" y="-364.7" font-family="Times,serif"
|
||||
font-size="14.00">CRL Infrastructure(Optional)</text>
|
||||
</g>
|
||||
<g id="clust3" class="cluster">
|
||||
<title>cluster_0</title>
|
||||
<polygon fill="none" stroke="black" points="809,-8 809,-382 1213,-382 1213,-8 809,-8" />
|
||||
<text text-anchor="middle" x="1011" y="-364.7" font-family="Times,serif"
|
||||
font-size="14.00">Master docker compose</text>
|
||||
</g>
|
||||
<g id="clust4" class="cluster">
|
||||
<title>cluster_1</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="344,-109.5 344,-382 801,-382 801,-109.5 344,-109.5" />
|
||||
<text text-anchor="middle" x="572.5" y="-364.7" font-family="Times,serif"
|
||||
font-size="14.00">Slave docker compose</text>
|
||||
</g>
|
||||
<!-- ca -->
|
||||
<g id="node1" class="node">
|
||||
<title>ca</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="322.12,-349.5 267.88,-349.5 267.88,-313.5 328.12,-313.5 328.12,-343.5 322.12,-349.5" />
|
||||
<polyline fill="none" stroke="black" points="322.12,-349.5 322.12,-343.5" />
|
||||
<polyline fill="none" stroke="black" points="328.12,-343.5 322.12,-343.5" />
|
||||
<text text-anchor="middle" x="298" y="-326.45" font-family="Times,serif"
|
||||
font-size="14.00">CA Key</text>
|
||||
</g>
|
||||
<!-- crl_listener -->
|
||||
<g id="node2" class="node">
|
||||
<title>crl_listener</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="138,-261 24.04,-224.25 251.96,-224.25 138,-261" />
|
||||
<text text-anchor="middle" x="138" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">http://my.crl</text>
|
||||
</g>
|
||||
<!-- crl -->
|
||||
<g id="node3" class="node">
|
||||
<title>crl</title>
|
||||
<ellipse fill="none" stroke="black" cx="138" cy="-331.5" rx="27" ry="18" />
|
||||
<text text-anchor="middle" x="138" y="-326.45" font-family="Times,serif"
|
||||
font-size="14.00">crl</text>
|
||||
</g>
|
||||
<!-- crl->crl_listener -->
|
||||
<g id="edge1" class="edge">
|
||||
<title>crl->crl_listener</title>
|
||||
<path fill="none" stroke="black" d="M138,-313.01C138,-301.58 138,-286.26 138,-272.41" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="141.5,-272.75 138,-262.75 134.5,-272.75 141.5,-272.75" />
|
||||
<text text-anchor="middle" x="164.25" y="-282.2" font-family="Times,serif"
|
||||
font-size="14.00">Static file</text>
|
||||
</g>
|
||||
<!-- web_app -->
|
||||
<g id="node4" class="node">
|
||||
<title>web_app</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="957,-153.5 893,-153.5 893,-117.5 957,-117.5 957,-153.5" />
|
||||
<text text-anchor="middle" x="925" y="-130.45" font-family="Times,serif"
|
||||
font-size="14.00">Web app</text>
|
||||
</g>
|
||||
<!-- db -->
|
||||
<g id="node5" class="node">
|
||||
<title>db</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="986.88,-58.5 925.12,-58.5 925.12,-22.5 986.88,-22.5 986.88,-58.5" />
|
||||
<text text-anchor="middle" x="956" y="-35.45" font-family="Times,serif"
|
||||
font-size="14.00">Postgres</text>
|
||||
</g>
|
||||
<!-- web_app->db -->
|
||||
<g id="edge5" class="edge">
|
||||
<title>web_app->db</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M924.71,-117.39C925,-107.16 926.24,-94.05 930,-83 931.68,-78.07 934.04,-73.12 936.67,-68.44" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="939.58,-70.39 941.88,-60.05 933.63,-66.7 939.58,-70.39" />
|
||||
</g>
|
||||
<!-- redis -->
|
||||
<g id="node6" class="node">
|
||||
<title>redis</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="1059,-58.5 1005,-58.5 1005,-22.5 1059,-22.5 1059,-58.5" />
|
||||
<text text-anchor="middle" x="1032" y="-35.45" font-family="Times,serif"
|
||||
font-size="14.00">Redis</text>
|
||||
</g>
|
||||
<!-- web_app->redis -->
|
||||
<g id="edge6" class="edge">
|
||||
<title>web_app->redis</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M931.67,-117.34C936.69,-106.32 944.5,-92.37 955,-83 969.85,-69.75 978.5,-74.47 996,-65 996.21,-64.88 996.43,-64.77 996.64,-64.65" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="998.19,-67.8 1005.05,-59.73 994.65,-61.76 998.19,-67.8" />
|
||||
</g>
|
||||
<!-- listen_master_web -->
|
||||
<g id="node11" class="node">
|
||||
<title>listen_master_web</title>
|
||||
<polygon fill="none" stroke="black" points="862,-65 816.62,-28.25 907.38,-28.25 862,-65" />
|
||||
<text text-anchor="middle" x="862" y="-35.45" font-family="Times,serif"
|
||||
font-size="14.00">:80</text>
|
||||
</g>
|
||||
<!-- web_app->listen_master_web -->
|
||||
<g id="edge10" class="edge">
|
||||
<title>web_app->listen_master_web</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M904.09,-117.23C898.48,-111.93 892.74,-105.82 888.25,-99.5 882.02,-90.73 876.73,-80.24 872.57,-70.61" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="875.93,-69.59 868.93,-61.64 869.45,-72.22 875.93,-69.59" />
|
||||
<text text-anchor="middle" x="905.12" y="-86.2" font-family="Times,serif"
|
||||
font-size="14.00">Listen</text>
|
||||
</g>
|
||||
<!-- replikey -->
|
||||
<g id="node7" class="node">
|
||||
<title>replikey</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="1080.38,-153.5 1015.62,-153.5 1015.62,-117.5 1080.38,-117.5 1080.38,-153.5" />
|
||||
<text text-anchor="middle" x="1048" y="-130.45" font-family="Times,serif"
|
||||
font-size="14.00">Replikey</text>
|
||||
</g>
|
||||
<!-- replikey->db -->
|
||||
<g id="edge2" class="edge">
|
||||
<title>replikey->db</title>
|
||||
<path fill="none" stroke="orange"
|
||||
d="M1015.48,-128.13C998.56,-123.14 978.97,-114.45 967,-99.5 960.42,-91.28 957.35,-80.31 956.03,-70.12" />
|
||||
<polygon fill="orange" stroke="orange"
|
||||
points="959.53,-69.96 955.3,-60.25 952.55,-70.48 959.53,-69.96" />
|
||||
<text text-anchor="middle" x="1001.5" y="-86.2" font-family="Times,serif"
|
||||
font-size="14.00">SNI Routing</text>
|
||||
</g>
|
||||
<!-- replikey->redis -->
|
||||
<g id="edge3" class="edge">
|
||||
<title>replikey->redis</title>
|
||||
<path fill="none" stroke="orange"
|
||||
d="M1044.99,-117.01C1042.71,-103.76 1039.53,-85.28 1036.89,-69.92" />
|
||||
<polygon fill="orange" stroke="orange"
|
||||
points="1040.4,-69.67 1035.25,-60.41 1033.5,-70.85 1040.4,-69.67" />
|
||||
<text text-anchor="middle" x="1076.4" y="-86.2" font-family="Times,serif"
|
||||
font-size="14.00">SNI Routing</text>
|
||||
</g>
|
||||
<!-- listen_master_replikey -->
|
||||
<g id="node12" class="node">
|
||||
<title>listen_master_replikey</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="1141,-65 1077.19,-28.25 1204.81,-28.25 1141,-65" />
|
||||
<text text-anchor="middle" x="1141" y="-35.45" font-family="Times,serif"
|
||||
font-size="14.00">:6443</text>
|
||||
</g>
|
||||
<!-- replikey->listen_master_replikey -->
|
||||
<g id="edge9" class="edge">
|
||||
<title>replikey->listen_master_replikey</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M1090.83,-117.59C1099.6,-112.71 1108.21,-106.71 1115,-99.5 1124.83,-89.06 1131.39,-74.26 1135.45,-62.23" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="1089.25,-114.47 1081.92,-122.12 1092.42,-120.71 1089.25,-114.47" />
|
||||
<text text-anchor="middle" x="1142.87" y="-86.2" font-family="Times,serif"
|
||||
font-size="14.00">Listen</text>
|
||||
</g>
|
||||
<!-- server_cert -->
|
||||
<g id="node8" class="node">
|
||||
<title>server_cert</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="1033.62,-254.5 964.38,-254.5 964.38,-218.5 1039.62,-218.5 1039.62,-248.5 1033.62,-254.5" />
|
||||
<polyline fill="none" stroke="black" points="1033.62,-254.5 1033.62,-248.5" />
|
||||
<polyline fill="none" stroke="black" points="1039.62,-248.5 1033.62,-248.5" />
|
||||
<text text-anchor="middle" x="1002" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">Server cert</text>
|
||||
</g>
|
||||
<!-- server_cert->replikey -->
|
||||
<g id="edge7" class="edge">
|
||||
<title>server_cert->replikey</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M998.38,-218.25C996.45,-204.79 995.76,-186.02 1003,-171.5 1004.78,-167.94 1007.11,-164.63 1009.77,-161.59" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="1012.1,-164.21 1016.89,-154.76 1007.25,-159.15 1012.1,-164.21" />
|
||||
<text text-anchor="middle" x="1037.5" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Authenticate</text>
|
||||
</g>
|
||||
<!-- server_key -->
|
||||
<g id="node9" class="node">
|
||||
<title>server_key</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="1033.25,-349.5 964.75,-349.5 964.75,-313.5 1039.25,-313.5 1039.25,-343.5 1033.25,-349.5" />
|
||||
<polyline fill="none" stroke="black" points="1033.25,-349.5 1033.25,-343.5" />
|
||||
<polyline fill="none" stroke="black" points="1039.25,-343.5 1033.25,-343.5" />
|
||||
<text text-anchor="middle" x="1002" y="-326.45" font-family="Times,serif"
|
||||
font-size="14.00">Server key</text>
|
||||
</g>
|
||||
<!-- server_key->server_cert -->
|
||||
<g id="edge4" class="edge">
|
||||
<title>server_key->server_cert</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M1002,-313.01C1002,-299.89 1002,-281.64 1002,-266.37" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="1005.5,-266.43 1002,-256.43 998.5,-266.43 1005.5,-266.43" />
|
||||
<text text-anchor="middle" x="1032.75" y="-282.2" font-family="Times,serif"
|
||||
font-size="14.00">Private key</text>
|
||||
</g>
|
||||
<!-- ca_cert -->
|
||||
<g id="node10" class="node">
|
||||
<title>ca_cert</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="1117,-254.5 1065,-254.5 1065,-218.5 1123,-218.5 1123,-248.5 1117,-254.5" />
|
||||
<polyline fill="none" stroke="black" points="1117,-254.5 1117,-248.5" />
|
||||
<polyline fill="none" stroke="black" points="1123,-248.5 1117,-248.5" />
|
||||
<text text-anchor="middle" x="1094" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">CA cert</text>
|
||||
</g>
|
||||
<!-- ca_cert->replikey -->
|
||||
<g id="edge8" class="edge">
|
||||
<title>ca_cert->replikey</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M1089.42,-218.15C1085.61,-204.97 1079.6,-186.61 1072,-171.5 1070.62,-168.76 1069.05,-165.98 1067.4,-163.25" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="1070.41,-161.46 1062.01,-155 1064.55,-165.29 1070.41,-161.46" />
|
||||
<text text-anchor="middle" x="1093.04" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Trust</text>
|
||||
</g>
|
||||
<!-- db_slave -->
|
||||
<g id="node13" class="node">
|
||||
<title>db_slave</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="413.88,-254.5 352.12,-254.5 352.12,-218.5 413.88,-218.5 413.88,-254.5" />
|
||||
<text text-anchor="middle" x="383" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">Postgres</text>
|
||||
</g>
|
||||
<!-- replikey_slave_db -->
|
||||
<g id="node15" class="node">
|
||||
<title>replikey_slave_db</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="511,-153.5 387,-153.5 387,-117.5 511,-117.5 511,-153.5" />
|
||||
<text text-anchor="middle" x="449" y="-130.45" font-family="Times,serif"
|
||||
font-size="14.00">Replikey DB Client</text>
|
||||
</g>
|
||||
<!-- db_slave->replikey_slave_db -->
|
||||
<g id="edge11" class="edge">
|
||||
<title>db_slave->replikey_slave_db</title>
|
||||
<path fill="none" stroke="orange"
|
||||
d="M379.66,-218.12C377.98,-204.42 377.85,-185.38 386.5,-171.5 389.02,-167.46 392.18,-163.85 395.73,-160.63" />
|
||||
<polygon fill="orange" stroke="orange"
|
||||
points="397.63,-163.58 403.43,-154.72 393.36,-158.03 397.63,-163.58" />
|
||||
<text text-anchor="middle" x="414.25" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Plain TCP</text>
|
||||
</g>
|
||||
<!-- redis_slave -->
|
||||
<g id="node14" class="node">
|
||||
<title>redis_slave</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="764,-254.5 710,-254.5 710,-218.5 764,-218.5 764,-254.5" />
|
||||
<text text-anchor="middle" x="737" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">Redis</text>
|
||||
</g>
|
||||
<!-- replikey_slave_redis -->
|
||||
<g id="node16" class="node">
|
||||
<title>replikey_slave_redis</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="753,-153.5 617,-153.5 617,-117.5 753,-117.5 753,-153.5" />
|
||||
<text text-anchor="middle" x="685" y="-130.45" font-family="Times,serif"
|
||||
font-size="14.00">Replikey Redis Client</text>
|
||||
</g>
|
||||
<!-- redis_slave->replikey_slave_redis -->
|
||||
<g id="edge12" class="edge">
|
||||
<title>redis_slave->replikey_slave_redis</title>
|
||||
<path fill="none" stroke="orange"
|
||||
d="M727.94,-218.26C720.09,-203.3 708.58,-181.39 699.45,-164.01" />
|
||||
<polygon fill="orange" stroke="orange"
|
||||
points="702.58,-162.44 694.83,-155.22 696.38,-165.7 702.58,-162.44" />
|
||||
<text text-anchor="middle" x="738.75" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Plain TCP</text>
|
||||
</g>
|
||||
<!-- replikey_slave_db->listen_master_replikey -->
|
||||
<g id="edge18" class="edge">
|
||||
<title>replikey_slave_db->listen_master_replikey</title>
|
||||
<path fill="none" stroke="green"
|
||||
d="M510.56,-117.01C554.75,-105.24 615.93,-90.54 670.75,-83 845.84,-58.91 894.07,-96.36 1068,-65 1080.51,-62.74 1093.85,-58.9 1105.65,-54.97" />
|
||||
<polygon fill="green" stroke="green"
|
||||
points="1106.69,-58.31 1114.99,-51.72 1104.39,-51.7 1106.69,-58.31" />
|
||||
<text text-anchor="middle" x="709.38" y="-86.2" font-family="Times,serif"
|
||||
font-size="14.00">TLS with SNI</text>
|
||||
</g>
|
||||
<!-- replikey_slave_redis->listen_master_replikey -->
|
||||
<g id="edge19" class="edge">
|
||||
<title>replikey_slave_redis->listen_master_replikey</title>
|
||||
<path fill="none" stroke="green"
|
||||
d="M714.72,-117.15C736.62,-105.29 767.56,-90.42 796.75,-83 913.84,-53.22 949.33,-87.68 1068,-65 1080.39,-62.63 1093.6,-58.8 1105.34,-54.91" />
|
||||
<polygon fill="green" stroke="green"
|
||||
points="1106.32,-58.27 1114.63,-51.7 1104.03,-51.66 1106.32,-58.27" />
|
||||
<text text-anchor="middle" x="835.38" y="-86.2" font-family="Times,serif"
|
||||
font-size="14.00">TLS with SNI</text>
|
||||
</g>
|
||||
<!-- client_cert -->
|
||||
<g id="node17" class="node">
|
||||
<title>client_cert</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="534.5,-254.5 467.5,-254.5 467.5,-218.5 540.5,-218.5 540.5,-248.5 534.5,-254.5" />
|
||||
<polyline fill="none" stroke="black" points="534.5,-254.5 534.5,-248.5" />
|
||||
<polyline fill="none" stroke="black" points="540.5,-248.5 534.5,-248.5" />
|
||||
<text text-anchor="middle" x="504" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">Client cert</text>
|
||||
</g>
|
||||
<!-- client_cert->replikey_slave_db -->
|
||||
<g id="edge14" class="edge">
|
||||
<title>client_cert->replikey_slave_db</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M475.84,-218.05C465.79,-210.21 455.57,-200.01 450,-188 446.79,-181.08 445.61,-173.05 445.46,-165.42" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="448.95,-165.63 445.87,-155.49 441.96,-165.34 448.95,-165.63" />
|
||||
<text text-anchor="middle" x="484.5" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Authenticate</text>
|
||||
</g>
|
||||
<!-- client_cert->replikey_slave_redis -->
|
||||
<g id="edge16" class="edge">
|
||||
<title>client_cert->replikey_slave_redis</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M509.9,-218.18C515.78,-203.56 526.22,-183.19 542,-171.5 560.36,-157.9 583.34,-149.53 605.46,-144.4" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="606.03,-147.85 615.1,-142.37 604.59,-141 606.03,-147.85" />
|
||||
<text text-anchor="middle" x="576.5" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Authenticate</text>
|
||||
</g>
|
||||
<!-- client_key -->
|
||||
<g id="node18" class="node">
|
||||
<title>client_key</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="534.12,-349.5 467.88,-349.5 467.88,-313.5 540.12,-313.5 540.12,-343.5 534.12,-349.5" />
|
||||
<polyline fill="none" stroke="black" points="534.12,-349.5 534.12,-343.5" />
|
||||
<polyline fill="none" stroke="black" points="540.12,-343.5 534.12,-343.5" />
|
||||
<text text-anchor="middle" x="504" y="-326.45" font-family="Times,serif"
|
||||
font-size="14.00">Client key</text>
|
||||
</g>
|
||||
<!-- client_key->client_cert -->
|
||||
<g id="edge13" class="edge">
|
||||
<title>client_key->client_cert</title>
|
||||
<path fill="none" stroke="black" d="M504,-313.01C504,-299.89 504,-281.64 504,-266.37" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="507.5,-266.43 504,-256.43 500.5,-266.43 507.5,-266.43" />
|
||||
<text text-anchor="middle" x="534.75" y="-282.2" font-family="Times,serif"
|
||||
font-size="14.00">Private key</text>
|
||||
</g>
|
||||
<!-- ca_cert_slave -->
|
||||
<g id="node19" class="node">
|
||||
<title>ca_cert_slave</title>
|
||||
<polygon fill="none" stroke="black"
|
||||
points="671,-254.5 619,-254.5 619,-218.5 677,-218.5 677,-248.5 671,-254.5" />
|
||||
<polyline fill="none" stroke="black" points="671,-254.5 671,-248.5" />
|
||||
<polyline fill="none" stroke="black" points="677,-248.5 671,-248.5" />
|
||||
<text text-anchor="middle" x="648" y="-231.45" font-family="Times,serif"
|
||||
font-size="14.00">CA cert</text>
|
||||
</g>
|
||||
<!-- ca_cert_slave->replikey_slave_db -->
|
||||
<g id="edge15" class="edge">
|
||||
<title>ca_cert_slave->replikey_slave_db</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M642.45,-218.08C636.85,-203.4 626.77,-182.99 611,-171.5 585.53,-152.94 552.21,-143.86 522.41,-139.54" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="523.05,-136.09 512.69,-138.3 522.17,-143.03 523.05,-136.09" />
|
||||
<text text-anchor="middle" x="640.5" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Trust</text>
|
||||
</g>
|
||||
<!-- ca_cert_slave->replikey_slave_redis -->
|
||||
<g id="edge17" class="edge">
|
||||
<title>ca_cert_slave->replikey_slave_redis</title>
|
||||
<path fill="none" stroke="black"
|
||||
d="M654.44,-218.26C659.98,-203.44 668.07,-181.8 674.53,-164.5" />
|
||||
<polygon fill="black" stroke="black"
|
||||
points="677.75,-165.89 677.97,-155.3 671.2,-163.44 677.75,-165.89" />
|
||||
<text text-anchor="middle" x="685.15" y="-174.7" font-family="Times,serif"
|
||||
font-size="14.00">Trust</text>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 21 KiB |
200
src/bin/replikey.rs
Normal file
200
src/bin/replikey.rs
Normal file
|
@ -0,0 +1,200 @@
|
|||
use clap::Parser;
|
||||
|
||||
#[cfg(feature = "setup-postgres")]
|
||||
use replikey::ops::postgres::{
|
||||
add_table_to_postgres_pub, drop_postgres_pub, drop_postgres_sub, drop_table_from_postgres_pub,
|
||||
setup_postgres_pub, setup_postgres_sub, SetupPostgresMasterCommand,
|
||||
SetupPostgresMasterSubCommand, SetupPostgresSlaveCommand, SetupPostgresSlaveSubCommand,
|
||||
};
|
||||
|
||||
#[cfg(feature = "keygen")]
|
||||
use replikey::{
|
||||
cert::UsageType,
|
||||
ops::service::{
|
||||
service_replicate_master, service_replicate_slave, ServiceCommand, ServiceSubCommand,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg(feature = "keygen")]
|
||||
use replikey::ops::cert::*;
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
use replikey::ops::network::*;
|
||||
use rustls::crypto::{aws_lc_rs, CryptoProvider};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = "replikey")]
|
||||
#[clap(version = env!("CARGO_PKG_VERSION"))]
|
||||
struct Opts {
|
||||
#[clap(subcommand)]
|
||||
subcmd: SubCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
#[clap(name = "not-available")]
|
||||
struct NotAvailable;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
enum SubCommand {
|
||||
#[cfg(feature = "keygen")]
|
||||
#[clap(name = "cert")]
|
||||
Cert(CertCommand),
|
||||
|
||||
#[cfg(feature = "networking")]
|
||||
#[clap(name = "network")]
|
||||
Network(NetworkCommand),
|
||||
|
||||
#[clap(name = "service")]
|
||||
#[cfg(feature = "service")]
|
||||
Service(ServiceCommand),
|
||||
|
||||
#[cfg(feature = "setup-postgres")]
|
||||
#[clap(name = "setup-postgres-master")]
|
||||
SetupPostgresMaster(SetupPostgresMasterCommand),
|
||||
|
||||
#[cfg(feature = "setup-postgres")]
|
||||
#[clap(name = "setup-postgres-slave")]
|
||||
SetupPostgresSlave(SetupPostgresSlaveCommand),
|
||||
|
||||
Info,
|
||||
}
|
||||
#[cfg(feature = "asyncio")]
|
||||
fn start_runtime() -> tokio::runtime::Runtime {
|
||||
use tokio::runtime::Builder;
|
||||
Builder::new_current_thread().enable_all().build().unwrap()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
if std::env::var("RUST_LOG").is_err() {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
env_logger::init();
|
||||
|
||||
let opts: Opts = Opts::parse();
|
||||
|
||||
CryptoProvider::install_default(aws_lc_rs::default_provider())
|
||||
.expect("Failed to install crypto provider");
|
||||
|
||||
match opts.subcmd {
|
||||
SubCommand::Info => {
|
||||
println!("replikey v{}", env!("CARGO_PKG_VERSION"));
|
||||
println!("Feature flags:");
|
||||
macro_rules! print_feature {
|
||||
($feature:literal) => {
|
||||
println!(
|
||||
" {}: {}",
|
||||
$feature,
|
||||
if cfg!(feature = $feature) {
|
||||
"YES"
|
||||
} else {
|
||||
"NO"
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
print_feature!("serde");
|
||||
print_feature!("keygen");
|
||||
print_feature!("asyncio");
|
||||
print_feature!("networking");
|
||||
print_feature!("service");
|
||||
print_feature!("remote-crl");
|
||||
print_feature!("setup-postgres");
|
||||
}
|
||||
#[cfg(feature = "keygen")]
|
||||
SubCommand::Cert(cert) => match cert.subcmd {
|
||||
CertSubCommand::CreateCa(ca) => {
|
||||
create_ca(ca, true);
|
||||
}
|
||||
CertSubCommand::CreateServer(server) => {
|
||||
create_server(server);
|
||||
}
|
||||
CertSubCommand::CreateClient(client) => {
|
||||
create_client(client);
|
||||
}
|
||||
CertSubCommand::SignServerCSR(opts) => {
|
||||
sign_csr(opts, UsageType::Server, true);
|
||||
}
|
||||
CertSubCommand::SignClientCSR(opts) => {
|
||||
sign_csr(opts, UsageType::Client, true);
|
||||
}
|
||||
CertSubCommand::GenerateCrl(opts) => {
|
||||
revoke_cert(opts);
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "service")]
|
||||
SubCommand::Network(network) => match network.subcmd {
|
||||
NetworkSubCommand::ReverseProxy(opts) => {
|
||||
println!("Reverse proxy: {:?}", opts);
|
||||
let rt = start_runtime();
|
||||
|
||||
rt.block_on(reverse_proxy(opts))
|
||||
.expect("Failed to run reverse proxy");
|
||||
}
|
||||
NetworkSubCommand::ForwardProxy(opts) => {
|
||||
println!("Forward proxy: {:?}", opts);
|
||||
|
||||
let rt = start_runtime();
|
||||
|
||||
rt.block_on(forward_proxy(opts))
|
||||
.expect("Failed to run forward proxy");
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "service")]
|
||||
SubCommand::Service(service) => match service.subcmd {
|
||||
ServiceSubCommand::ReplicateMaster { config } => {
|
||||
service_replicate_master(config);
|
||||
}
|
||||
ServiceSubCommand::ReplicateSlave { config } => {
|
||||
service_replicate_slave(config);
|
||||
}
|
||||
},
|
||||
#[cfg(feature = "setup-postgres")]
|
||||
SubCommand::SetupPostgresMaster(opts) => {
|
||||
let conn = opts.connection_string.as_deref();
|
||||
let rt = start_runtime();
|
||||
rt.block_on(async {
|
||||
match opts.subcmd {
|
||||
SetupPostgresMasterSubCommand::Setup(opts) => {
|
||||
setup_postgres_pub(conn, opts)
|
||||
.await
|
||||
.expect("Failed to setup publication");
|
||||
}
|
||||
SetupPostgresMasterSubCommand::Drop(opts) => {
|
||||
drop_postgres_pub(conn, opts)
|
||||
.await
|
||||
.expect("Failed to drop publication");
|
||||
}
|
||||
SetupPostgresMasterSubCommand::AddTable(opts) => {
|
||||
add_table_to_postgres_pub(conn, opts)
|
||||
.await
|
||||
.expect("Failed to add table to publication");
|
||||
}
|
||||
SetupPostgresMasterSubCommand::DropTable(opts) => {
|
||||
drop_table_from_postgres_pub(conn, opts)
|
||||
.await
|
||||
.expect("Failed to drop table from publication");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
#[cfg(feature = "setup-postgres")]
|
||||
SubCommand::SetupPostgresSlave(opts) => {
|
||||
let conn = opts.connection_string.as_deref();
|
||||
let rt = start_runtime();
|
||||
rt.block_on(async {
|
||||
match opts.subcmd {
|
||||
SetupPostgresSlaveSubCommand::Setup(opts) => {
|
||||
setup_postgres_sub(conn, opts)
|
||||
.await
|
||||
.expect("Failed to setup subscription");
|
||||
}
|
||||
SetupPostgresSlaveSubCommand::Drop(opts) => {
|
||||
drop_postgres_sub(conn, opts)
|
||||
.await
|
||||
.expect("Failed to drop subscription");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
161
src/cert.rs
Normal file
161
src/cert.rs
Normal file
|
@ -0,0 +1,161 @@
|
|||
use rand_core::{OsRng, RngCore};
|
||||
use rcgen::{
|
||||
BasicConstraints, CertificateParams, CrlDistributionPoint, DistinguishedName,
|
||||
ExtendedKeyUsagePurpose, Ia5String, IsCa, KeyUsagePurpose, SanType, SerialNumber,
|
||||
};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
pub fn default_ca_options(not_after: OffsetDateTime, dn: DistinguishedName) -> CertificateParams {
|
||||
let mut start = CertificateParams::default();
|
||||
|
||||
let mut serial = [0u8; 20];
|
||||
OsRng.fill_bytes(&mut serial);
|
||||
|
||||
start.serial_number = Some(SerialNumber::from_slice(&serial));
|
||||
start.not_before = OffsetDateTime::now_utc();
|
||||
start.not_after = not_after;
|
||||
start.distinguished_name = dn;
|
||||
start.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::CrlSign,
|
||||
KeyUsagePurpose::KeyCertSign,
|
||||
];
|
||||
start.extended_key_usages = vec![
|
||||
ExtendedKeyUsagePurpose::ServerAuth,
|
||||
ExtendedKeyUsagePurpose::ClientAuth,
|
||||
];
|
||||
|
||||
start.is_ca = IsCa::Ca(BasicConstraints::Unconstrained);
|
||||
|
||||
start
|
||||
}
|
||||
|
||||
pub fn default_server_cert_options(
|
||||
not_after: OffsetDateTime,
|
||||
dns_names: &[&str],
|
||||
ip_addrs: &[&str],
|
||||
dn: DistinguishedName,
|
||||
csr: bool, // remove fields that are not needed for csr
|
||||
) -> CertificateParams {
|
||||
let mut start = CertificateParams::default();
|
||||
|
||||
let mut serial = [0u8; 20];
|
||||
OsRng.fill_bytes(&mut serial);
|
||||
start.distinguished_name = dn;
|
||||
start.not_before = OffsetDateTime::now_utc();
|
||||
start.not_after = not_after;
|
||||
start.subject_alt_names = dns_names
|
||||
.iter()
|
||||
.map(|n| SanType::DnsName(Ia5String::try_from(*n).expect("Invalid DNS name")))
|
||||
.chain(
|
||||
ip_addrs
|
||||
.iter()
|
||||
.map(|n| SanType::IpAddress(n.parse().unwrap())),
|
||||
)
|
||||
.collect();
|
||||
|
||||
if !csr {
|
||||
start.serial_number = Some(SerialNumber::from_slice(&serial));
|
||||
start.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::DataEncipherment,
|
||||
];
|
||||
start.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||
start.is_ca = IsCa::ExplicitNoCa;
|
||||
}
|
||||
|
||||
start
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum UsageType {
|
||||
Server,
|
||||
Client,
|
||||
}
|
||||
|
||||
pub fn csr_apply_server(
|
||||
csr: &mut CertificateParams,
|
||||
not_after: OffsetDateTime,
|
||||
dns_names: &[&str],
|
||||
ip_addrs: &[&str],
|
||||
) {
|
||||
let mut serial = [0u8; 20];
|
||||
OsRng.fill_bytes(&mut serial);
|
||||
csr.serial_number = Some(SerialNumber::from_slice(&serial));
|
||||
csr.not_before = OffsetDateTime::now_utc();
|
||||
csr.not_after = not_after.min(csr.not_after);
|
||||
csr.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::DataEncipherment,
|
||||
];
|
||||
csr.extended_key_usages = vec![ExtendedKeyUsagePurpose::ServerAuth];
|
||||
csr.is_ca = IsCa::ExplicitNoCa;
|
||||
|
||||
csr.subject_alt_names = dns_names
|
||||
.iter()
|
||||
.map(|n| SanType::DnsName(Ia5String::try_from(*n).expect("Invalid DNS name")))
|
||||
.chain(
|
||||
ip_addrs
|
||||
.iter()
|
||||
.map(|n| SanType::IpAddress(n.parse().unwrap())),
|
||||
)
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn csr_apply_client(
|
||||
csr: &mut CertificateParams,
|
||||
not_after: OffsetDateTime,
|
||||
dns_names: &[&str],
|
||||
ip_addrs: &[&str],
|
||||
) {
|
||||
let mut serial = [0u8; 20];
|
||||
OsRng.fill_bytes(&mut serial);
|
||||
csr.serial_number = Some(SerialNumber::from_slice(&serial));
|
||||
csr.not_before = OffsetDateTime::now_utc();
|
||||
csr.not_after = not_after.min(csr.not_after);
|
||||
csr.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::DataEncipherment,
|
||||
];
|
||||
csr.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
|
||||
csr.is_ca = IsCa::ExplicitNoCa;
|
||||
|
||||
csr.subject_alt_names = dns_names
|
||||
.iter()
|
||||
.map(|n| SanType::DnsName(Ia5String::try_from(*n).expect("Invalid DNS name")))
|
||||
.chain(
|
||||
ip_addrs
|
||||
.iter()
|
||||
.map(|n| SanType::IpAddress(n.parse().unwrap())),
|
||||
)
|
||||
.collect();
|
||||
}
|
||||
|
||||
pub fn default_client_cert_options(
|
||||
not_after: OffsetDateTime,
|
||||
dn: DistinguishedName,
|
||||
crls: Vec<CrlDistributionPoint>,
|
||||
csr: bool,
|
||||
) -> CertificateParams {
|
||||
let mut start = CertificateParams::default();
|
||||
start.distinguished_name = dn;
|
||||
start.not_before = OffsetDateTime::now_utc();
|
||||
start.not_after = not_after;
|
||||
|
||||
let mut serial = [0u8; 20];
|
||||
OsRng.fill_bytes(&mut serial);
|
||||
|
||||
if !csr {
|
||||
start.serial_number = Some(SerialNumber::from_slice(&serial));
|
||||
start.key_usages = vec![
|
||||
KeyUsagePurpose::DigitalSignature,
|
||||
KeyUsagePurpose::DataEncipherment,
|
||||
];
|
||||
start.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth];
|
||||
start.is_ca = IsCa::ExplicitNoCa;
|
||||
|
||||
start.crl_distribution_points = crls;
|
||||
}
|
||||
|
||||
start
|
||||
}
|
221
src/fs_crypt.rs
Normal file
221
src/fs_crypt.rs
Normal file
|
@ -0,0 +1,221 @@
|
|||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::{Read, Seek, Write},
|
||||
os::unix::fs::OpenOptionsExt,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use aes_gcm::{
|
||||
aead::{Aead, AeadMutInPlace},
|
||||
AeadCore, Aes256Gcm, KeyInit,
|
||||
};
|
||||
use argon2::Argon2;
|
||||
use rand_core::{OsRng, RngCore};
|
||||
use rpassword::prompt_password;
|
||||
|
||||
const KEY_SIZE: usize = 32;
|
||||
const SALT_SIZE: usize = 16;
|
||||
const NONCE_SIZE: usize = 12;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Argon2Hardness {
|
||||
Test,
|
||||
Default,
|
||||
Hard,
|
||||
}
|
||||
|
||||
pub fn private_fs_write<P: AsRef<Path>, C: AsRef<[u8]>>(path: P, data: C) -> std::io::Result<()> {
|
||||
let mut file = OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.mode(0o600)
|
||||
.open(path)?;
|
||||
|
||||
file.write_all(data.as_ref())
|
||||
}
|
||||
|
||||
pub fn derive_password(password: &str, salt: &[u8], hardness: Argon2Hardness) -> [u8; KEY_SIZE] {
|
||||
let mut key = [0u8; KEY_SIZE];
|
||||
Argon2::new(
|
||||
argon2::Algorithm::Argon2id,
|
||||
argon2::Version::V0x13,
|
||||
match hardness {
|
||||
Argon2Hardness::Test => argon2::ParamsBuilder::default()
|
||||
.t_cost(1)
|
||||
.m_cost(16)
|
||||
.build()
|
||||
.unwrap(),
|
||||
Argon2Hardness::Default => argon2::ParamsBuilder::default().build().unwrap(),
|
||||
Argon2Hardness::Hard => argon2::ParamsBuilder::default()
|
||||
.t_cost(30)
|
||||
.m_cost(128 << 20)
|
||||
.build()
|
||||
.unwrap(),
|
||||
},
|
||||
)
|
||||
.hash_password_into(password.as_bytes(), salt, &mut key)
|
||||
.unwrap();
|
||||
key
|
||||
}
|
||||
|
||||
fn pkcs7_pad(output: &mut Vec<u8>, block_size: usize) {
|
||||
let pad = block_size - output.len() % block_size;
|
||||
output.extend(std::iter::repeat(pad as u8).take(pad));
|
||||
}
|
||||
|
||||
pub fn encrypt(data: &mut Vec<u8>, password: &str, hardness: Argon2Hardness) {
|
||||
let mut salt = [0u8; SALT_SIZE];
|
||||
OsRng
|
||||
.try_fill_bytes(&mut salt)
|
||||
.expect("Failed to generate salt");
|
||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||
let key = derive_password(password, &salt, hardness);
|
||||
|
||||
let mut header = vec![1]; // in case we need to add a version byte
|
||||
header.extend_from_slice(&salt);
|
||||
header.extend_from_slice(&nonce);
|
||||
|
||||
let mut cipher = Aes256Gcm::new_from_slice(&key).unwrap();
|
||||
|
||||
pkcs7_pad(data, 16);
|
||||
|
||||
cipher
|
||||
.encrypt_in_place(&nonce, &[], data)
|
||||
.expect("Encryption failed");
|
||||
|
||||
data.splice(0..0, header);
|
||||
}
|
||||
|
||||
pub fn decrypt(data: &[u8], password: &str, hardness: Argon2Hardness) -> Vec<u8> {
|
||||
if data[0] != 1 {
|
||||
panic!("Unsupported version");
|
||||
}
|
||||
let salt = &data[1..SALT_SIZE + 1];
|
||||
let nonce = &data[SALT_SIZE + 1..SALT_SIZE + NONCE_SIZE + 1];
|
||||
let key = derive_password(password, salt, hardness);
|
||||
|
||||
let cipher = Aes256Gcm::new_from_slice(&key).unwrap();
|
||||
if (data.len() - 1 - SALT_SIZE - NONCE_SIZE) % 16 != 0 {
|
||||
panic!("Invalid data length");
|
||||
}
|
||||
|
||||
let mut output = cipher
|
||||
.decrypt(nonce.into(), &data[SALT_SIZE + NONCE_SIZE + 1..])
|
||||
.expect("Decryption failed");
|
||||
|
||||
let pad = output[output.len() - 1] as usize;
|
||||
output.truncate(output.len() - pad);
|
||||
output
|
||||
}
|
||||
|
||||
pub fn encrypt_to_pem(data: &[u8], password: &str, hardness: Argon2Hardness) -> String {
|
||||
let mut encrypted = data.to_vec();
|
||||
encrypt(&mut encrypted, password, hardness);
|
||||
|
||||
pem_rfc7468::encode_string("ENCRYPTED DATA", pem_rfc7468::LineEnding::LF, &encrypted)
|
||||
.expect("Failed to encode PEM")
|
||||
}
|
||||
|
||||
pub fn write_encrypted_pem<W: Write>(
|
||||
mut writer: W,
|
||||
data: &[u8],
|
||||
password: &str,
|
||||
hardness: Argon2Hardness,
|
||||
) -> std::io::Result<()> {
|
||||
let mut encrypted = data.to_vec();
|
||||
encrypt(&mut encrypted, password, hardness);
|
||||
|
||||
let pem = pem_rfc7468::encode_string("ENCRYPTED DATA", pem_rfc7468::LineEnding::LF, &encrypted)
|
||||
.expect("Failed to encode PEM");
|
||||
|
||||
writer.write_all(pem.as_bytes())
|
||||
}
|
||||
|
||||
pub fn read_maybe_encrypted_pem<R: Read + Seek>(
|
||||
mut reader: R,
|
||||
password: Option<&str>,
|
||||
hardness: Argon2Hardness,
|
||||
) -> std::io::Result<Option<Vec<u8>>> {
|
||||
const SIGNATURE: &[u8] = b"-----BEGIN ENCRYPTED DATA-----";
|
||||
let mut signature = [0u8; SIGNATURE.len()];
|
||||
match reader.read_exact(&mut signature) {
|
||||
Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
|
||||
reader.seek(std::io::SeekFrom::Start(0))?;
|
||||
return Ok(None);
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(e);
|
||||
}
|
||||
Ok(_) => {
|
||||
if signature != SIGNATURE {
|
||||
reader.seek(std::io::SeekFrom::Start(0))?;
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
}
|
||||
reader.seek(std::io::SeekFrom::Start(0))?;
|
||||
let mut pem = String::new();
|
||||
reader.read_to_string(&mut pem)?;
|
||||
|
||||
let (_, encrypted) = pem_rfc7468::decode_vec(pem.as_bytes()).expect("Failed to decode PEM");
|
||||
|
||||
match password {
|
||||
None => {
|
||||
let password = prompt_password("Enter password: ").expect("Failed to read password");
|
||||
|
||||
Ok(Some(decrypt(&encrypted, &password, hardness)))
|
||||
}
|
||||
Some(password) => Ok(Some(decrypt(&encrypted, password, hardness))),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::io::Cursor;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_kdf() {
|
||||
let password = "password";
|
||||
let salt = [0u8; SALT_SIZE];
|
||||
let key = derive_password(password, &salt, Argon2Hardness::Test);
|
||||
assert_eq!(key.len(), KEY_SIZE);
|
||||
let key2 = derive_password(password, &salt, Argon2Hardness::Test);
|
||||
assert_eq!(key, key2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt() {
|
||||
let data = (0..64).map(|_| "Hello, world!").collect::<String>();
|
||||
|
||||
for len in 0..data.len() {
|
||||
let password = "password";
|
||||
let mut encrypted = data.as_bytes()[..len].to_vec();
|
||||
encrypt(&mut encrypted, password, Argon2Hardness::Test);
|
||||
let decrypted = decrypt(&encrypted, password, Argon2Hardness::Test);
|
||||
assert_eq!(&data.as_bytes()[..len], &decrypted[..]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pem() {
|
||||
let data = (0..64).map(|_| "Hello, world!").collect::<String>();
|
||||
|
||||
for len in 0..data.len() {
|
||||
let password = "password";
|
||||
let pt = data.as_bytes()[..len].to_vec();
|
||||
let mut pem = Vec::new();
|
||||
write_encrypted_pem(&mut pem, &pt, password, Argon2Hardness::Test)
|
||||
.expect("Failed to write PEM");
|
||||
let mut pem = Cursor::new(pem);
|
||||
let decrypted =
|
||||
read_maybe_encrypted_pem(&mut pem, Some(password), Argon2Hardness::Test)
|
||||
.expect("Failed to read PEM")
|
||||
.expect("Failed to decrypt");
|
||||
assert_eq!(&data.as_bytes()[..len], &decrypted[..]);
|
||||
}
|
||||
}
|
||||
}
|
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
|
@ -0,0 +1,5 @@
|
|||
#[cfg(feature = "keygen")]
|
||||
pub mod cert;
|
||||
pub mod ops;
|
||||
|
||||
pub mod fs_crypt;
|
1020
src/ops/cert.rs
Normal file
1020
src/ops/cert.rs
Normal file
File diff suppressed because it is too large
Load diff
9
src/ops/mod.rs
Normal file
9
src/ops/mod.rs
Normal file
|
@ -0,0 +1,9 @@
|
|||
#[cfg(feature = "keygen")]
|
||||
pub mod cert;
|
||||
#[cfg(feature = "networking")]
|
||||
pub mod network;
|
||||
#[cfg(feature = "service")]
|
||||
pub mod service;
|
||||
|
||||
#[cfg(feature = "setup-postgres")]
|
||||
pub mod postgres;
|
362
src/ops/network.rs
Normal file
362
src/ops/network.rs
Normal file
|
@ -0,0 +1,362 @@
|
|||
use std::{io::Cursor, net::ToSocketAddrs, sync::Arc};
|
||||
|
||||
use clap::Parser;
|
||||
use tokio::{
|
||||
io::{AsyncBufRead, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader},
|
||||
net::TcpStream,
|
||||
};
|
||||
use tokio_rustls::{
|
||||
rustls::{
|
||||
client::WebPkiServerVerifier,
|
||||
pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer, ServerName},
|
||||
server::WebPkiClientVerifier,
|
||||
ClientConfig, RootCertStore, ServerConfig,
|
||||
},
|
||||
TlsAcceptor, TlsConnector,
|
||||
};
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct NetworkCommand {
|
||||
#[clap(subcommand)]
|
||||
pub subcmd: NetworkSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum NetworkSubCommand {
|
||||
#[clap(name = "reverse-proxy")]
|
||||
ReverseProxy(ReverseProxyCommand),
|
||||
|
||||
#[clap(name = "forward-proxy")]
|
||||
ForwardProxy(ForwardProxyCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ReverseProxyCommand {
|
||||
#[clap(short, long)]
|
||||
pub listen: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub redis_sni: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub redis_target: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub postgres_sni: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub postgres_target: String,
|
||||
|
||||
#[clap(long, help = "Certificate")]
|
||||
pub cert: String,
|
||||
|
||||
#[clap(long, help = "Private key")]
|
||||
pub key: String,
|
||||
|
||||
#[clap(long, help = "CA to trust")]
|
||||
pub ca: String,
|
||||
|
||||
#[clap(long, help = "CRLs to use")]
|
||||
pub crl: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ForwardProxyCommand {
|
||||
#[clap(short, long)]
|
||||
pub listen: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
pub sni: String,
|
||||
|
||||
#[clap(short, long)]
|
||||
pub target: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub cert: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub key: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub ca: String,
|
||||
|
||||
#[clap(long)]
|
||||
pub crl: Vec<String>,
|
||||
}
|
||||
|
||||
fn compressor_to(w: impl AsyncWrite + Unpin) -> impl AsyncWrite + Unpin {
|
||||
async_compression::tokio::write::ZstdEncoder::new(w)
|
||||
}
|
||||
|
||||
fn decompressor_from(r: impl AsyncBufRead + Unpin) -> impl AsyncRead + Unpin {
|
||||
async_compression::tokio::bufread::ZstdDecoder::new(r)
|
||||
}
|
||||
|
||||
async fn send_static_string(w: &mut (impl AsyncWrite + Unpin), s: &str) -> tokio::io::Result<()> {
|
||||
let mut cursor = Cursor::new(s);
|
||||
|
||||
tokio::io::copy(&mut cursor, &mut compressor_to(w)).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn copy_bidirectional_compressed(
|
||||
local: impl AsyncRead + AsyncWrite + Unpin,
|
||||
remote: impl AsyncRead + AsyncWrite + Unpin,
|
||||
) -> tokio::io::Result<(u64, u64)> {
|
||||
let (mut local_rx, mut local_tx) = tokio::io::split(local);
|
||||
let (remote_rx, remote_tx) = tokio::io::split(remote);
|
||||
|
||||
let remote_rx_buf = BufReader::new(remote_rx);
|
||||
|
||||
let mut remote_tx_comp = compressor_to(remote_tx);
|
||||
let mut remote_rx_decomp = decompressor_from(remote_rx_buf);
|
||||
|
||||
log::info!("Starting transfer");
|
||||
|
||||
let uplink = async move {
|
||||
let res = tokio::io::copy(&mut local_rx, &mut remote_tx_comp).await;
|
||||
let shutdown = remote_tx_comp.shutdown().await;
|
||||
let res = res?;
|
||||
shutdown?;
|
||||
tokio::io::Result::Ok(res)
|
||||
};
|
||||
let downlink = async move {
|
||||
let res = tokio::io::copy(&mut remote_rx_decomp, &mut local_tx).await;
|
||||
let shutdown = local_tx.shutdown().await;
|
||||
let res = res?;
|
||||
shutdown?;
|
||||
tokio::io::Result::Ok(res)
|
||||
};
|
||||
let res = tokio::try_join!(uplink, downlink)?;
|
||||
log::info!(
|
||||
"Finished transferring {} bytes from local to remote and {} bytes from remote to local (compressed)",
|
||||
res.0,
|
||||
res.1
|
||||
);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn reverse_proxy(opts: ReverseProxyCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_, ca_pem) = x509_parser::pem::parse_x509_pem(&std::fs::read(&opts.ca)?)?;
|
||||
let (_, ca_cert) = x509_parser::parse_x509_certificate(&ca_pem.contents)?;
|
||||
let mut cert_store = RootCertStore::empty();
|
||||
cert_store.add(CertificateDer::from_pem_file(&opts.ca)?)?;
|
||||
let cert_store = Arc::new(cert_store);
|
||||
|
||||
let mut crls = Vec::new();
|
||||
|
||||
for crl_def in &opts.crl {
|
||||
#[cfg(feature = "remote-crl")]
|
||||
{
|
||||
// crls are signed so we can trust them
|
||||
if crl_def.starts_with("http://") || crl_def.starts_with("https://") {
|
||||
log::info!("Downloading CRL: {}", crl_def);
|
||||
let crl = reqwest::get(crl_def).await?.bytes().await?;
|
||||
let (_, parsed) = x509_parser::parse_x509_crl(&crl)?;
|
||||
if let Err(e) = parsed.verify_signature(ca_cert.public_key()) {
|
||||
log::error!("Failed to verify CRL signature: {}", e);
|
||||
if !crl_def.starts_with("https://") {
|
||||
return Err(e.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
crls.push(crl.to_vec().into());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
crls.push(std::fs::read(crl_def).expect("Failed to read CRL").into());
|
||||
}
|
||||
|
||||
let cv = WebPkiClientVerifier::builder(cert_store.clone())
|
||||
.with_crls(crls)
|
||||
.build()
|
||||
.expect("Failed to build client verifier");
|
||||
|
||||
let config = ServerConfig::builder()
|
||||
.with_client_cert_verifier(cv)
|
||||
.with_single_cert(
|
||||
vec![CertificateDer::from_pem_file(&opts.cert)?],
|
||||
PrivateKeyDer::from_pem_file(&opts.key)?,
|
||||
)?;
|
||||
|
||||
let acceptor = TlsAcceptor::from(Arc::new(config));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&opts.listen).await?;
|
||||
log::info!("Listening on: {}", opts.listen);
|
||||
|
||||
let (redis_sni, postgres_sni, redis_target, postgres_target) = (
|
||||
Arc::new(opts.redis_sni.clone()),
|
||||
Arc::new(opts.postgres_sni.clone()),
|
||||
opts.redis_target.clone(),
|
||||
opts.postgres_target.clone(),
|
||||
);
|
||||
if let Err(e) = opts.redis_target.to_socket_addrs() {
|
||||
eprintln!("Failed to resolve redis target: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
if let Err(e) = opts.postgres_target.to_socket_addrs() {
|
||||
eprintln!("Failed to resolve postgres target: {}", e);
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
let (pt_stream, _) = match listener.accept().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to accept connection: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let acceptor = acceptor.clone();
|
||||
let (redis_sni, postgres_sni, redis_target, postgres_target) = (
|
||||
redis_sni.clone(),
|
||||
postgres_sni.clone(),
|
||||
redis_target.clone(),
|
||||
postgres_target.clone(),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
match acceptor.accept(pt_stream).await {
|
||||
Ok(mut tls) => match tls.get_ref().1.server_name().map(|s| s.to_string()) {
|
||||
Some(sni) if sni == *redis_sni => {
|
||||
log::info!(
|
||||
"Accepted Redis connection for {:?}",
|
||||
tls.get_ref().1.server_name()
|
||||
);
|
||||
match tokio::net::TcpStream::connect(&redis_target).await {
|
||||
Ok(redis) => {
|
||||
if let Err(e) = copy_bidirectional_compressed(redis, tls).await {
|
||||
eprintln!("Failed to copy data: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to redis: {}", e);
|
||||
tls.shutdown().await.expect("Failed to shutdown TLS stream");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(sni) if sni == *postgres_sni => {
|
||||
log::info!(
|
||||
"Accepted Postgres connection for {:?}",
|
||||
tls.get_ref().1.server_name()
|
||||
);
|
||||
match tokio::net::TcpStream::connect(&postgres_target).await {
|
||||
Ok(postgres) => {
|
||||
if let Err(e) = copy_bidirectional_compressed(postgres, tls).await {
|
||||
eprintln!("Failed to copy data: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to postgres: {}", e);
|
||||
tls.shutdown().await.expect("Failed to shutdown TLS stream");
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(sni) => {
|
||||
log::warn!("Accepted connection for {:?}, but SNI {} does not match any configured SNI", tls.get_ref().1.server_name(), sni);
|
||||
send_static_string(
|
||||
&mut tls,
|
||||
format!("SNI {} does not match any configured SNI", sni).as_str(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to send static string");
|
||||
tls.shutdown().await.expect("Failed to shutdown TLS stream");
|
||||
}
|
||||
_ => {
|
||||
send_static_string(&mut tls, "No SNI provided")
|
||||
.await
|
||||
.expect("Failed to send static string");
|
||||
eprintln!("No SNI provided");
|
||||
tls.shutdown().await.expect("Failed to shutdown TLS stream");
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to accept connection: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn forward_proxy(opts: ForwardProxyCommand) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let (_, ca_pem) = x509_parser::pem::parse_x509_pem(&std::fs::read(&opts.ca)?)?;
|
||||
let (_, ca_cert) = x509_parser::parse_x509_certificate(&ca_pem.contents)?;
|
||||
let mut cert_store = RootCertStore::empty();
|
||||
cert_store.add(CertificateDer::from_pem_file(&opts.ca)?)?;
|
||||
let cert_store = Arc::new(cert_store);
|
||||
|
||||
let mut crls = Vec::new();
|
||||
|
||||
for crl_def in &opts.crl {
|
||||
#[cfg(feature = "remote-crl")]
|
||||
{
|
||||
// crls are signed so we can trust them
|
||||
if crl_def.starts_with("http://") || crl_def.starts_with("https://") {
|
||||
log::info!("Downloading CRL: {}", crl_def);
|
||||
let crl = reqwest::get(crl_def).await?.bytes().await?;
|
||||
let (_, parsed) = x509_parser::parse_x509_crl(&crl)?;
|
||||
if let Err(e) = parsed.verify_signature(ca_cert.public_key()) {
|
||||
log::error!("Failed to verify CRL signature: {}", e);
|
||||
if !crl_def.starts_with("https://") {
|
||||
return Err(e.into());
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
crls.push(crl.to_vec().into());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
crls.push(std::fs::read(crl_def).expect("Failed to read CRL").into());
|
||||
}
|
||||
|
||||
let cv = WebPkiServerVerifier::builder(cert_store.clone())
|
||||
.with_crls(crls)
|
||||
.build()
|
||||
.expect("Failed to build server verifier");
|
||||
|
||||
let config = ClientConfig::builder()
|
||||
.with_webpki_verifier(cv)
|
||||
.with_client_auth_cert(
|
||||
vec![CertificateDer::from_pem_file(&opts.cert)?],
|
||||
PrivateKeyDer::from_pem_file(&opts.key)?,
|
||||
)?;
|
||||
|
||||
let connector = TlsConnector::from(Arc::new(config));
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&opts.listen).await?;
|
||||
log::info!("Listening on: {}", opts.listen);
|
||||
|
||||
let sni = ServerName::try_from(opts.sni.as_str()).expect("Failed to parse SNI");
|
||||
loop {
|
||||
let (pt_stream, _) = match listener.accept().await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to accept connection: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let connector = connector.clone();
|
||||
let tls_stream = match TcpStream::connect(&opts.target).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to target: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let sni = sni.to_owned();
|
||||
tokio::spawn(async move {
|
||||
match connector.connect(sni, tls_stream).await {
|
||||
Ok(tls) => {
|
||||
if let Err(e) = copy_bidirectional_compressed(pt_stream, tls).await {
|
||||
eprintln!("Failed to copy data: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to connect to target: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
344
src/ops/postgres.rs
Normal file
344
src/ops/postgres.rs
Normal file
|
@ -0,0 +1,344 @@
|
|||
use clap::Parser;
|
||||
use sqlx::{Connection, PgConnection};
|
||||
|
||||
const DEFAULT_URL_ENV: &str = "DATABASE_URL";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetupPostgresMasterCommand {
|
||||
#[clap(long, help = "Postgres Connection String, defaults to DATABASE_URL")]
|
||||
pub connection_string: Option<String>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub subcmd: SetupPostgresMasterSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum SetupPostgresMasterSubCommand {
|
||||
#[clap(name = "setup")]
|
||||
Setup(SetupPublicationCommand),
|
||||
|
||||
#[clap(name = "teardown")]
|
||||
Drop(DropPublicationCommand),
|
||||
|
||||
#[clap(name = "add-table")]
|
||||
AddTable(AddTableCommand),
|
||||
|
||||
#[clap(name = "drop-table")]
|
||||
DropTable(DropTableCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetupPublicationCommand {
|
||||
#[clap(long, help = "Publication Name")]
|
||||
pub publication: String,
|
||||
|
||||
#[clap(long, help = "Whitelist mode, each table must be added manually")]
|
||||
pub whitelist: bool,
|
||||
|
||||
#[clap(long, help = "Publish Delete", default_value = "true")]
|
||||
pub publish_delete: bool,
|
||||
|
||||
#[clap(long, help = "Publish Truncate")]
|
||||
pub publish_truncate: bool,
|
||||
|
||||
#[clap(long, help = "Fail if publication already exists")]
|
||||
pub must_not_exist: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DropPublicationCommand {
|
||||
#[clap(long, help = "Publication Name")]
|
||||
pub publication: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DropTableCommand {
|
||||
#[clap(long, help = "Publication Name")]
|
||||
pub publication: String,
|
||||
|
||||
#[clap(short, long, help = "Table Name")]
|
||||
pub table: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct AddTableCommand {
|
||||
#[clap(long, help = "Publication Name")]
|
||||
pub publication: String,
|
||||
|
||||
#[clap(short, long, help = "Table Name")]
|
||||
pub table: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetupPostgresSlaveCommand {
|
||||
#[clap(long, help = "Postgres Connection String, defaults to DATABASE_URL")]
|
||||
pub connection_string: Option<String>,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub subcmd: SetupPostgresSlaveSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum SetupPostgresSlaveSubCommand {
|
||||
#[clap(name = "setup")]
|
||||
Setup(SetupSubscriptionCommand),
|
||||
|
||||
#[clap(name = "teardown")]
|
||||
Drop(DropSubscriptionCommand),
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct SetupSubscriptionCommand {
|
||||
#[clap(long, help = "Publication Name")]
|
||||
pub publication: String,
|
||||
|
||||
#[clap(long, help = "Subscription Name")]
|
||||
pub subscription: String,
|
||||
|
||||
#[clap(long, help = "Two phase transaction")]
|
||||
pub two_phase: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct DropSubscriptionCommand {
|
||||
#[clap(long, help = "Subscription Name")]
|
||||
pub subscription: String,
|
||||
}
|
||||
|
||||
pub fn postgres_connection_string_from_env() -> Option<String> {
|
||||
std::env::var("DATABASE_URL").ok()
|
||||
}
|
||||
|
||||
fn safe_ident(name: &str) -> Option<&str> {
|
||||
if name
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.all(|(i, &c)| i > 0 && c.is_ascii_digit() || c.is_ascii_lowercase() || c == b'_')
|
||||
{
|
||||
Some(name)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum PostgresSetupError {
|
||||
#[error("Missing Connection String, set via DATABASE_URL")]
|
||||
MissingConnection,
|
||||
|
||||
#[error("Entity already exists: {0}")]
|
||||
AlreadyExists(String),
|
||||
|
||||
#[error("Invalid Identifier: {0}")]
|
||||
InvalidIdentifier(String),
|
||||
|
||||
#[error("Postgres Error: {0}")]
|
||||
PostgresError(#[from] sqlx::Error),
|
||||
}
|
||||
|
||||
pub async fn setup_postgres_pub(
|
||||
connection_string: Option<&str>,
|
||||
opts: SetupPublicationCommand,
|
||||
) -> Result<(), PostgresSetupError> {
|
||||
let mut postgres = PgConnection::connect(
|
||||
&&connection_string
|
||||
.map(|s| s.to_string())
|
||||
.or_else(postgres_connection_string_from_env)
|
||||
.ok_or(PostgresSetupError::MissingConnection)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let existing_pub =
|
||||
sqlx::query_scalar::<_, i32>("SELECT 1 FROM pg_publication WHERE pubname = $1")
|
||||
.bind(&opts.publication)
|
||||
.fetch_optional(&mut postgres)
|
||||
.await?;
|
||||
|
||||
if existing_pub.is_some() {
|
||||
return if opts.must_not_exist {
|
||||
Err(PostgresSetupError::AlreadyExists(
|
||||
"Publication already exists".to_string(),
|
||||
))
|
||||
} else {
|
||||
Ok(())
|
||||
};
|
||||
}
|
||||
|
||||
let query = format!(
|
||||
"CREATE PUBLICATION {}{}{}",
|
||||
safe_ident(&opts.publication)
|
||||
.ok_or_else(|| PostgresSetupError::InvalidIdentifier(opts.publication.clone()))?,
|
||||
if opts.whitelist {
|
||||
""
|
||||
} else {
|
||||
" FOR TABLES IN SCHEMA public"
|
||||
},
|
||||
match (opts.publish_delete, opts.publish_truncate) {
|
||||
(true, true) => " WITH (publish = 'insert, update, delete, truncate')",
|
||||
(true, false) => " WITH (publish = 'insert, update, delete')",
|
||||
(false, true) => {
|
||||
log::warn!("Publishing truncate without delete does not make sense!");
|
||||
" WITH (publish = 'insert, update, truncate')"
|
||||
}
|
||||
(false, false) => " WITH (publish = 'insert, update')",
|
||||
}
|
||||
);
|
||||
|
||||
log::info!("Executing: {}", query);
|
||||
|
||||
sqlx::query(&query).execute(&mut postgres).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn drop_postgres_pub(
|
||||
connection_string: Option<&str>,
|
||||
opts: DropPublicationCommand,
|
||||
) -> Result<(), PostgresSetupError> {
|
||||
let mut postgres = PgConnection::connect(
|
||||
&&connection_string
|
||||
.map(|s| s.to_string())
|
||||
.or_else(postgres_connection_string_from_env)
|
||||
.ok_or(PostgresSetupError::MissingConnection)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = format!(
|
||||
"DROP PUBLICATION {}",
|
||||
safe_ident(&opts.publication).unwrap()
|
||||
);
|
||||
|
||||
log::info!("Executing: {}", query);
|
||||
|
||||
sqlx::query(&query).execute(&mut postgres).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_table_to_postgres_pub(
|
||||
connection_string: Option<&str>,
|
||||
opts: AddTableCommand,
|
||||
) -> Result<(), PostgresSetupError> {
|
||||
let mut postgres = PgConnection::connect(
|
||||
&&connection_string
|
||||
.map(|s| s.to_string())
|
||||
.or_else(postgres_connection_string_from_env)
|
||||
.ok_or(PostgresSetupError::MissingConnection)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for table in opts.table {
|
||||
let query = format!(
|
||||
"ALTER PUBLICATION {} ADD TABLE {}",
|
||||
safe_ident(&opts.publication).unwrap(),
|
||||
safe_ident(&table).unwrap()
|
||||
);
|
||||
|
||||
log::info!("Executing: {}", query);
|
||||
|
||||
sqlx::query(&query).execute(&mut postgres).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn drop_table_from_postgres_pub(
|
||||
connection_string: Option<&str>,
|
||||
opts: DropTableCommand,
|
||||
) -> Result<(), PostgresSetupError> {
|
||||
let mut postgres = PgConnection::connect(
|
||||
&&connection_string
|
||||
.map(|s| s.to_string())
|
||||
.or_else(postgres_connection_string_from_env)
|
||||
.ok_or(PostgresSetupError::MissingConnection)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
for table in opts.table {
|
||||
let query = format!(
|
||||
"ALTER PUBLICATION {} DROP TABLE {}",
|
||||
safe_ident(&opts.publication).unwrap(),
|
||||
safe_ident(&table).unwrap()
|
||||
);
|
||||
|
||||
log::info!("Executing: {}", query);
|
||||
|
||||
sqlx::query(&query).execute(&mut postgres).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn setup_postgres_sub(
|
||||
connection_string: Option<&str>,
|
||||
opts: SetupSubscriptionCommand,
|
||||
) -> Result<(), PostgresSetupError> {
|
||||
let mut postgres = PgConnection::connect(
|
||||
&&connection_string
|
||||
.map(|s| s.to_string())
|
||||
.or_else(postgres_connection_string_from_env)
|
||||
.ok_or(PostgresSetupError::MissingConnection)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let existing_sub =
|
||||
sqlx::query_scalar::<_, i32>("SELECT 1 FROM pg_subscription WHERE subname = $1")
|
||||
.bind(&opts.subscription)
|
||||
.fetch_optional(&mut postgres)
|
||||
.await?;
|
||||
|
||||
if existing_sub.is_some() {
|
||||
return Err(PostgresSetupError::AlreadyExists(
|
||||
"Subscription already exists".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let query = format!(
|
||||
"CREATE SUBSCRIPTION {} CONNECTION $1 PUBLICATION {}{}",
|
||||
safe_ident(&opts.subscription)
|
||||
.ok_or_else(|| PostgresSetupError::InvalidIdentifier(opts.subscription.clone()))?,
|
||||
safe_ident(&opts.publication).unwrap(),
|
||||
if opts.two_phase {
|
||||
" WITH (two_phase = on)"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
);
|
||||
|
||||
log::info!(
|
||||
"Executing: {} with $1 = {}",
|
||||
query,
|
||||
connection_string.unwrap_or(DEFAULT_URL_ENV)
|
||||
);
|
||||
|
||||
sqlx::query(&query)
|
||||
.bind(connection_string.unwrap_or(DEFAULT_URL_ENV))
|
||||
.execute(&mut postgres)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn drop_postgres_sub(
|
||||
connection_string: Option<&str>,
|
||||
opts: DropSubscriptionCommand,
|
||||
) -> Result<(), PostgresSetupError> {
|
||||
let mut postgres = PgConnection::connect(
|
||||
&&connection_string
|
||||
.map(|s| s.to_string())
|
||||
.or_else(postgres_connection_string_from_env)
|
||||
.ok_or(PostgresSetupError::MissingConnection)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let query = format!(
|
||||
"DROP SUBSCRIPTION {}",
|
||||
safe_ident(&opts.subscription).unwrap()
|
||||
);
|
||||
|
||||
log::info!("Executing: {}", query);
|
||||
|
||||
sqlx::query(&query).execute(&mut postgres).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
135
src/ops/service.rs
Normal file
135
src/ops/service.rs
Normal file
|
@ -0,0 +1,135 @@
|
|||
use std::path::Path;
|
||||
|
||||
use clap::Parser;
|
||||
use serde::Deserialize;
|
||||
|
||||
use super::network::ReverseProxyCommand;
|
||||
|
||||
const DEF_CONFIG_FILE: &str = "/etc/replikey.toml";
|
||||
const CA_CERT: &str = "ca.pem";
|
||||
const SERVER_CERT: &str = "server.pem";
|
||||
const SERVER_KEY: &str = "server.key";
|
||||
const CLIENT_CERT: &str = "client.pem";
|
||||
const CLIENT_KEY: &str = "client.key";
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub struct ServiceCommand {
|
||||
#[clap(subcommand)]
|
||||
pub subcmd: ServiceSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
pub enum ServiceSubCommand {
|
||||
#[clap(name = "replicate-master")]
|
||||
ReplicateMaster {
|
||||
#[clap(short, long, default_value = DEF_CONFIG_FILE)]
|
||||
config: String,
|
||||
},
|
||||
|
||||
#[clap(name = "replicate-slave")]
|
||||
ReplicateSlave {
|
||||
#[clap(short, long, default_value = DEF_CONFIG_FILE)]
|
||||
config: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
connection: ConnectionConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ConnectionConfig {
|
||||
master: Option<MasterConfig>,
|
||||
slave: Option<SlaveConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MasterConfig {
|
||||
listen: String,
|
||||
redis_sni: String,
|
||||
redis_target: String,
|
||||
postgres_sni: String,
|
||||
postgres_target: String,
|
||||
workdir: Option<String>,
|
||||
crl: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SlaveConfig {
|
||||
listen: String,
|
||||
redis_sni: String,
|
||||
postgres_sni: String,
|
||||
workdir: Option<String>,
|
||||
crl: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn service_replicate_master(config: String) {
|
||||
let config = std::fs::read_to_string(config).unwrap();
|
||||
let config: Config = toml::from_str(&config).expect("Failed to parse config");
|
||||
|
||||
if let Some(wd) = config
|
||||
.connection
|
||||
.master
|
||||
.as_ref()
|
||||
.and_then(|m| m.workdir.as_ref())
|
||||
{
|
||||
std::env::set_current_dir(wd).expect("Failed to change directory");
|
||||
}
|
||||
|
||||
let master_conf = config.connection.master.as_ref().unwrap();
|
||||
let cmd = ReverseProxyCommand {
|
||||
listen: master_conf.listen.clone(),
|
||||
redis_sni: master_conf.redis_sni.clone(),
|
||||
redis_target: master_conf.redis_target.clone(),
|
||||
postgres_sni: master_conf.postgres_sni.clone(),
|
||||
postgres_target: master_conf.postgres_target.clone(),
|
||||
|
||||
cert: Path::new(SERVER_CERT).to_string_lossy().to_string(),
|
||||
key: Path::new(SERVER_KEY).to_string_lossy().to_string(),
|
||||
ca: Path::new(CA_CERT).to_string_lossy().to_string(),
|
||||
crl: config.connection.master.as_ref().unwrap().crl.clone(),
|
||||
};
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(crate::ops::network::reverse_proxy(cmd))
|
||||
.unwrap();
|
||||
|
||||
println!("Replication master started");
|
||||
}
|
||||
|
||||
pub fn service_replicate_slave(config: String) {
|
||||
let config = std::fs::read_to_string(config).unwrap();
|
||||
let config: Config = toml::from_str(&config).expect("Failed to parse config");
|
||||
|
||||
if let Some(wd) = config
|
||||
.connection
|
||||
.slave
|
||||
.as_ref()
|
||||
.and_then(|m| m.workdir.as_ref())
|
||||
{
|
||||
std::env::set_current_dir(wd).expect("Failed to change directory");
|
||||
}
|
||||
|
||||
let slave_conf = config.connection.slave.as_ref().unwrap();
|
||||
let cmd = ReverseProxyCommand {
|
||||
listen: slave_conf.listen.clone(),
|
||||
redis_sni: slave_conf.redis_sni.clone(),
|
||||
redis_target: slave_conf.redis_sni.clone(),
|
||||
postgres_sni: slave_conf.postgres_sni.clone(),
|
||||
postgres_target: slave_conf.postgres_sni.clone(),
|
||||
|
||||
cert: Path::new(CLIENT_CERT).to_string_lossy().to_string(),
|
||||
key: Path::new(CLIENT_KEY).to_string_lossy().to_string(),
|
||||
ca: Path::new(CA_CERT).to_string_lossy().to_string(),
|
||||
crl: config.connection.slave.as_ref().unwrap().crl.clone(),
|
||||
};
|
||||
|
||||
tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(crate::ops::network::reverse_proxy(cmd))
|
||||
.unwrap();
|
||||
|
||||
println!("Replication slave started");
|
||||
}
|
Loading…
Reference in a new issue