vaultctl 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,14 @@
1
+ {
2
+ "features": {
3
+ "ghcr.io/devcontainers-extra/features/ruff:2.0.0": {
4
+ "version": "2.0.0",
5
+ "resolved": "ghcr.io/devcontainers-extra/features/ruff@sha256:80f1d8ff0fbd84fffbc9383ff11cc54524898f8bbc820cae642dff4902575117",
6
+ "integrity": "sha256:80f1d8ff0fbd84fffbc9383ff11cc54524898f8bbc820cae642dff4902575117"
7
+ },
8
+ "ghcr.io/devcontainers-extra/features/uv:1.0.2": {
9
+ "version": "1.0.2",
10
+ "resolved": "ghcr.io/devcontainers-extra/features/uv@sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8",
11
+ "integrity": "sha256:1ac5b9f17a9e9e745933d0ac2ecf758e06ed3da4423cd22c94cce4d482fd2dd8"
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "vaultctl devcontainer",
3
+ "image": "mcr.microsoft.com/devcontainers/python:3-3.14",
4
+ "postCreateCommand": "uv sync",
5
+ "customizations": {
6
+ "vscode": {
7
+ "extensions": [
8
+ "ms-python.python",
9
+ "ms-python.vscode-pylance",
10
+ "charliermarsh.ruff",
11
+ "the0807.uv-toolkit"
12
+ ],
13
+ "settings": {
14
+ "python.defaultInterpreterPath": "/workspaces/vaultctl/.venv/bin/python",
15
+ "[python]": {
16
+ "editor.defaultFormatter": "charliermarsh.ruff",
17
+ "editor.formatOnSave": true
18
+ },
19
+ "ruff.path": ["/usr/local/bin/ruff"]
20
+ }
21
+ }
22
+ },
23
+ "features": {
24
+ "ghcr.io/devcontainers-extra/features/uv:1.0.2": {},
25
+ "ghcr.io/devcontainers-extra/features/ruff:2.0.0": { "version": "0.15.16"}
26
+ }
27
+ }
@@ -0,0 +1,121 @@
1
+ name: Release
2
+
3
+ on:
4
+ workflow_dispatch:
5
+
6
+ permissions:
7
+ contents: write
8
+ packages: write
9
+ id-token: write
10
+ attestations: write
11
+ artifact-metadata: write
12
+
13
+ jobs:
14
+ docker:
15
+ runs-on: ubuntu-24.04
16
+ env:
17
+ REGISTRY: ghcr.io
18
+
19
+ steps:
20
+ - name: Checkout repository
21
+ uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
22
+
23
+ - name: Set up Python
24
+ uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
25
+ with:
26
+ python-version: "3.14"
27
+
28
+ - name: Install uv
29
+ uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
30
+
31
+ - name: Get project name and version from pyproject.toml
32
+ id: meta
33
+ run: |
34
+ python3 <<'PY'
35
+ import tomllib, pathlib
36
+ p = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]
37
+ print(f"version={p['version']}")
38
+ print(f"name={p['name']}")
39
+ PY
40
+ shell: bash
41
+
42
+ - name: Set up QEMU
43
+ uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0
44
+
45
+ - name: Set up Docker Buildx
46
+ uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
47
+
48
+ - name: Set up Cosign
49
+ uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
50
+
51
+ - name: Log in to GitHub Container Registry
52
+ uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
53
+ with:
54
+ registry: ghcr.io
55
+ username: ${{ github.actor }}
56
+ password: ${{ secrets.GITHUB_TOKEN }}
57
+
58
+ - name: Build CLI package
59
+ run: |
60
+ rm -rf dist/ build/ *.egg-info
61
+ uv build
62
+
63
+ - name: Publish CLI to PyPI
64
+ run: |
65
+ uv publish
66
+
67
+ - name: Create GitHub Release
68
+ uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
69
+ with:
70
+ tag_name: v${{ steps.meta.outputs.version }}
71
+ name: v${{ steps.meta.outputs.version }}
72
+ generate_release_notes: true
73
+ files: |
74
+ dist/*
75
+
76
+ - name: Build and Push Docker Image
77
+ id: build-and-push
78
+ uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0
79
+ with:
80
+ context: .
81
+ file: ./Dockerfile
82
+ push: true
83
+ platforms: linux/amd64,linux/arm64
84
+ provenance: false
85
+ build-args: |
86
+ CLI_VERSION=${{ steps.meta.outputs.version }}
87
+ VENDOR=${{ github.repository_owner }}
88
+ labels: |
89
+ org.opencontainers.image.revision=${{ github.sha }}
90
+ org.opencontainers.image.vendor=${{ github.repository_owner }}
91
+ org.opencontainers.image.url=https://ghcr.io/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}
92
+ tags: |
93
+ ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}:latest
94
+ ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}:${{ steps.meta.outputs.version }}
95
+
96
+ - name: Sign Image with Cosign
97
+ run: |
98
+ cosign sign --yes \
99
+ ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}@${{ steps.build-and-push.outputs.digest }}
100
+
101
+ - name: Attest build provenance
102
+ uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
103
+ with:
104
+ subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}
105
+ subject-digest: ${{ steps.build-and-push.outputs.digest }}
106
+ push-to-registry: true
107
+
108
+ - name: Generate SBOM
109
+ uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
110
+ with:
111
+ image: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}@${{ steps.build-and-push.outputs.digest }}
112
+ format: spdx-json
113
+ output-file: sbom.spdx.json
114
+
115
+ - name: Attest SBOM
116
+ uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
117
+ with:
118
+ subject-name: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ steps.meta.outputs.name }}
119
+ subject-digest: ${{ steps.build-and-push.outputs.digest }}
120
+ sbom-path: sbom.spdx.json
121
+ push-to-registry: true
@@ -0,0 +1,6 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ .venv/
5
+ dist
6
+ .*_cache
@@ -0,0 +1,23 @@
1
+ {
2
+ $schema: 'https://docs.renovatebot.com/renovate-schema.json',
3
+ extends: [
4
+ 'config:recommended',
5
+ 'docker:enableMajor',
6
+ 'customManagers:dockerfileVersions',
7
+ 'helpers:pinGitHubActionDigests',
8
+ ':disableRateLimiting',
9
+ ':dependencyDashboard',
10
+ ':semanticCommits',
11
+ ':timezone(Europe/Sofia)',
12
+ ],
13
+ dependencyDashboardTitle: 'Renovate Dashboard 🤖',
14
+ suppressNotifications: [
15
+ 'prEditedNotification',
16
+ 'prIgnoreNotification',
17
+ ],
18
+ pip_requirements: {
19
+ enabled: true,
20
+ managerFilePatterns: ["(^|/)pyproject([-.]\\w+)?\\.toml$"],
21
+ },
22
+ }
23
+
@@ -0,0 +1,17 @@
1
+ FROM python:3.14-slim@sha256:9b81fe9acff79e61affb44aaf3b6ff234392e8ca477cb86c9f7fd11732ce9b6a
2
+
3
+ RUN \
4
+ groupadd -g 65532 nonroot \
5
+ && \
6
+ useradd -r -u 65532 -g 65532 -m nonroot
7
+
8
+
9
+ COPY dist/*.whl /tmp/
10
+ RUN \
11
+ pip install --no-cache-dir /tmp/*.whl \
12
+ && \
13
+ rm -f /tmp/*.whl
14
+
15
+ USER nonroot
16
+
17
+ ENTRYPOINT ["vaultctl"]
vaultctl-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Georgi Panov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: vaultctl
3
+ Version: 0.1.0
4
+ Summary: vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault.
5
+ Author-email: d4rkfella <georgi.panov@darkfellanetwork.com>
6
+ License: MIT
7
+ License-File: LICENSE
8
+ Requires-Python: <4.0,>=3.14
9
+ Requires-Dist: boto3==1.43.27
10
+ Requires-Dist: botocore[crt]
11
+ Requires-Dist: cryptography==48.0.1
12
+ Requires-Dist: hvac==2.4
13
+ Requires-Dist: python-dateutil==2.9.0
14
+ Requires-Dist: typer==0.26.7
15
+ Requires-Dist: validators==0.35.0
16
+ Description-Content-Type: text/markdown
17
+
18
+ vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault.
@@ -0,0 +1 @@
1
+ vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault.
@@ -0,0 +1,59 @@
1
+ [project]
2
+ name = "vaultctl"
3
+ version = "0.1.0"
4
+ description = "vaultctl is a command-line interface (CLI) tool designed to facilitate operations with HashiCorp Vault."
5
+ readme = "README.md"
6
+ license = { text = "MIT" }
7
+ authors = [
8
+ { name = "d4rkfella", email = "georgi.panov@darkfellanetwork.com" }
9
+ ]
10
+ requires-python = ">=3.14,<4.0"
11
+ dependencies = [
12
+ "typer==0.26.7",
13
+ "validators==0.35.0",
14
+ "hvac==2.4",
15
+ "cryptography==48.0.1",
16
+ "python-dateutil==2.9.0",
17
+ "boto3==1.43.27",
18
+ "botocore[crt]",
19
+ ]
20
+
21
+ [project.scripts]
22
+ vaultctl = "vaultctl.main:app"
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/vaultctl"]
30
+
31
+ [dependency-groups]
32
+ dev = [
33
+ "types-hvac==2.4.0.20260610",
34
+ "mypy-boto3-s3==1.43.14",
35
+ ]
36
+
37
+ [tool.ruff]
38
+ target-version = "py314"
39
+
40
+ [tool.ruff.lint]
41
+ select = ["ALL"]
42
+ ignore = [
43
+ "E501",
44
+ "D100", "D101", "D102", "D103", "D104", "D105", "D106", "D107",
45
+ "D211",
46
+ "D213",
47
+ "D203",
48
+ ]
49
+
50
+ [tool.ruff.lint.flake8-type-checking]
51
+ runtime-evaluated-base-classes = [
52
+ "typing.Annotated",
53
+ "typing_extensions.Annotated"
54
+ ]
55
+
56
+ runtime-evaluated-decorators = [
57
+ "typer.Typer.command",
58
+ "typer.Typer.callback"
59
+ ]
File without changes
@@ -0,0 +1,15 @@
1
+ import typer
2
+
3
+ from vaultctl.commands.backup_raft_snapshot import app as backup_raft_snapshot
4
+ from vaultctl.commands.bootstrap import app as bootstrap
5
+ from vaultctl.commands.login import app as login
6
+ from vaultctl.commands.pki.rotate_issuing import app as pki
7
+ from vaultctl.commands.restore_raft_snapshot import app as restore_raft_snapshot
8
+
9
+ app = typer.Typer()
10
+
11
+ app.add_typer(bootstrap)
12
+ app.add_typer(restore_raft_snapshot)
13
+ app.add_typer(backup_raft_snapshot)
14
+ app.add_typer(login)
15
+ app.add_typer(pki)
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import hashlib
5
+ import io
6
+ import tarfile
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Annotated, cast
10
+
11
+ import hvac
12
+ import typer
13
+ from botocore.exceptions import ClientError
14
+ from requests import Response
15
+
16
+ from vaultctl.options import (
17
+ AddressOption,
18
+ AwsAccessKeyIdOption,
19
+ AwsEndpointUrlOption,
20
+ AwsProfileOption,
21
+ AwsRegionOption,
22
+ AwsSecretAccessKeyOption,
23
+ CACertOption,
24
+ CAPathOption,
25
+ K8sMountPointOption,
26
+ K8sRoleOption,
27
+ OutputFileOption,
28
+ S3BucketNameOption,
29
+ S3KeyPrefixOption,
30
+ SkipVerifyOption,
31
+ TimeoutOption,
32
+ TokenOption,
33
+ )
34
+ from vaultctl.utils import handle_s3_authentication, handle_vault_authentication, logger
35
+
36
+ app = typer.Typer()
37
+
38
+
39
+ class RaftUploadError(Exception):
40
+ """Raised when uploading a Raft snapshot to S3 fails."""
41
+
42
+
43
+ class S3ChecksumAlgorithm(Enum):
44
+ CRC32 = "CRC32"
45
+ CRC32C = "CRC32C"
46
+ SHA1 = "SHA1"
47
+ SHA256 = "SHA256"
48
+ CRC64NVME = "CRC64NVME"
49
+
50
+
51
+ def parse_sha256sums(content: bytes) -> dict[str, str]:
52
+ sums = {}
53
+ lines = content.strip().split(b"\n")
54
+ for line in lines:
55
+ trimmed_line = line.strip()
56
+ if not trimmed_line:
57
+ continue
58
+ parts = trimmed_line.split()
59
+ if len(parts) == 2: # noqa: PLR2004
60
+ checksum = parts[0].decode("utf-8")
61
+ filename = parts[1].decode("utf-8")
62
+ sums[filename] = checksum
63
+ return sums
64
+
65
+
66
+ def verify_internal_checksums(snapshot_data: bytes) -> None:
67
+ logger.info("Verifying snapshot integrity...")
68
+ snapshot_stream = io.BytesIO(snapshot_data)
69
+
70
+ try:
71
+ with tarfile.open(fileobj=snapshot_stream, mode="r:gz") as tar:
72
+ sha_sums_content = None
73
+ files_in_tar: dict[str, bytes] = {}
74
+
75
+ for member in tar.getmembers():
76
+ if not member.isfile():
77
+ continue
78
+
79
+ if not (f := tar.extractfile(member)):
80
+ continue
81
+
82
+ content = f.read()
83
+
84
+ if member.name == "SHA256SUMS":
85
+ sha_sums_content = content
86
+
87
+ files_in_tar[member.name] = content
88
+
89
+ if sha_sums_content is None:
90
+ msg = "SHA256SUMS file not found in the Raft snapshot archive."
91
+ raise ValueError(
92
+ msg,
93
+ )
94
+
95
+ expected_sums = parse_sha256sums(sha_sums_content)
96
+
97
+ for name, expected_sum in expected_sums.items():
98
+ content = files_in_tar.get(name)
99
+
100
+ if content is None:
101
+ msg = f"file '{name}' listed in SHA256SUMS not found in archive."
102
+ raise ValueError(
103
+ msg,
104
+ )
105
+
106
+ computed_sum = hashlib.sha256(content).hexdigest()
107
+
108
+ if computed_sum != expected_sum:
109
+ msg = f"checksum mismatch for file '{name}'. Expected: {expected_sum}, Got: {computed_sum}"
110
+ raise ValueError(
111
+ msg,
112
+ )
113
+
114
+ logger.info("Snapshot integrity verified.")
115
+
116
+ except tarfile.TarError as e:
117
+ msg = f"reading archive: {e}"
118
+ raise tarfile.TarError(msg) from e
119
+
120
+
121
+ @app.command(
122
+ help="Executes a complete workflow for obtaining a HashiCorp Vault Raft snapshot from a cluster, verifying its integrity, and uploading it securely to S3 storage. Provides flexible authentication options for both HashiCorp Vault and S3 APIs.",
123
+ )
124
+ def backup_raft_snapshot( # noqa: PLR0913
125
+ address: AddressOption,
126
+ s3_bucket_name: S3BucketNameOption = None,
127
+ output_file: OutputFileOption = None,
128
+ k8s_role: K8sRoleOption = None,
129
+ k8s_mount_point: K8sMountPointOption = "kubernetes",
130
+ token: TokenOption = None,
131
+ ca_cert: CACertOption = None,
132
+ ca_path: CAPathOption = None,
133
+ aws_profile: AwsProfileOption = None,
134
+ aws_access_key_id: AwsAccessKeyIdOption = None,
135
+ aws_secret_access_key: AwsSecretAccessKeyOption = None,
136
+ aws_endpoint_url: AwsEndpointUrlOption = None,
137
+ aws_region: AwsRegionOption = "us-east-1",
138
+ s3_key_prefix: S3KeyPrefixOption = "",
139
+ s3_checksum_algorithm: Annotated[
140
+ S3ChecksumAlgorithm,
141
+ typer.Option(help="The algorithm to use for s3 transport checksum."),
142
+ ] = S3ChecksumAlgorithm.CRC64NVME,
143
+ timeout: TimeoutOption = 30,
144
+ *,
145
+ skip_verify: SkipVerifyOption = False,
146
+ ) -> None:
147
+ if not s3_bucket_name and not output_file:
148
+ msg = "Either --s3-bucket-name or --output-file is required"
149
+ raise typer.BadParameter(msg)
150
+
151
+ vault_client = handle_vault_authentication(
152
+ hvac.Client(
153
+ url=address,
154
+ timeout=timeout,
155
+ verify=(
156
+ str(ca_cert)
157
+ if ca_cert
158
+ else str(ca_path)
159
+ if ca_path
160
+ else (not skip_verify)
161
+ ),
162
+ ),
163
+ token=token,
164
+ k8s_role=k8s_role,
165
+ k8s_mount_point=k8s_mount_point,
166
+ )
167
+
168
+ if vault_client.sys.is_sealed():
169
+ logger.error("Vault is sealed. Cannot proceed with backup.")
170
+ raise typer.Exit(code=1)
171
+
172
+ logger.info("Starting backup...")
173
+
174
+ logger.info("Requesting raft snapshot...")
175
+ response: Response = vault_client.sys.take_raft_snapshot()
176
+
177
+ if response.status_code != 200: # noqa: PLR2004
178
+ logger.error(
179
+ "Raft snapshot request failed with status code %s.",
180
+ response.status_code,
181
+ )
182
+ logger.debug("Response body: %s", response.text)
183
+ raise typer.Exit(1)
184
+
185
+ snapshot_data: bytes = response.content
186
+
187
+ logger.info("Raft snapshot retrieved.")
188
+
189
+ try:
190
+ verify_internal_checksums(snapshot_data)
191
+ except (tarfile.TarError, ValueError):
192
+ logger.exception("Verifying raft snapshot integrity")
193
+ raise typer.Exit(1) from None
194
+
195
+ if output_file:
196
+ output_path = Path(output_file)
197
+ if output_path.is_dir():
198
+ timestamp = datetime.datetime.now(tz=datetime.UTC).strftime("%Y%m%d_%H%M%S")
199
+ output_path = output_path / f"vault-snapshot-{timestamp}.snap"
200
+ output_path.write_bytes(snapshot_data)
201
+ logger.info("Snapshot written to %s", output_path)
202
+ logger.info("Backup completed")
203
+ return
204
+
205
+ s3_bucket_name = cast("str", s3_bucket_name)
206
+
207
+ s3_client = handle_s3_authentication(
208
+ bucket_name=s3_bucket_name,
209
+ aws_access_key_id=aws_access_key_id,
210
+ aws_secret_access_key=aws_secret_access_key,
211
+ aws_region=aws_region,
212
+ aws_profile=aws_profile,
213
+ endpoint_url=aws_endpoint_url,
214
+ )
215
+
216
+ timestamp = datetime.datetime.now(tz=datetime.UTC).strftime("%Y%m%d_%H%M%S")
217
+ snapshot_name = f"vault-snapshot-{timestamp}.snap"
218
+ s3_key = f"{s3_key_prefix}{snapshot_name}"
219
+
220
+ try:
221
+ logger.info("Uploading raft snapshot to %s/%s...", s3_bucket_name, s3_key)
222
+ s3_client.upload_fileobj(
223
+ io.BytesIO(snapshot_data),
224
+ s3_bucket_name,
225
+ s3_key,
226
+ ExtraArgs={
227
+ "ContentType": "application/gzip",
228
+ "ChecksumAlgorithm": s3_checksum_algorithm,
229
+ },
230
+ )
231
+ except ClientError:
232
+ logger.exception("Uploading raft snapshot")
233
+ raise typer.Exit(1) from None
234
+
235
+ logger.info("Raft snapshot uploaded.")
236
+ logger.info("Backup completed")