Skip to content

Commit 10c8efd

Browse files
committed
feat: add decryption and refactoring
1 parent 66a9814 commit 10c8efd

24 files changed

+1684
-437
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
- name: Run linter
4141
run: uv run ruff check kseal/ tests/
4242

43+
- name: Run pyright
44+
run: uv run basedpyright kseal/ tests/
4345
coverage:
4446
runs-on: ubuntu-latest
4547

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ coverage:
1111

1212
lint:
1313
@uv run ruff check .
14+
@uv run basedpyright kseal/
1415

1516
format:
1617
@uv run ruff format .

README.md

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
[![License](https://img.shields.io/github/license/eznix86/kseal)](LICENSE)
66
[![Tests](https://github.com/eznix86/kseal/actions/workflows/test.yml/badge.svg)](https://github.com/eznix86/kseal/actions/workflows/test.yml)
77

8-
A kubeseal companion CLI for viewing, exporting, and encrypting Kubernetes Secrets.
8+
A kubeseal companion CLI for viewing, exporting, encrypting, and **offline decrypting** Kubernetes Secrets.
99

1010
## Installation
1111

@@ -33,20 +33,25 @@ pip install kseal
3333
### Requirements
3434

3535
- Python 3.12+
36-
- Kubernetes cluster access
36+
- Kubernetes cluster access (not required for offline decryption)
3737
- Sealed Secrets controller installed in cluster
3838

3939
## Quick Start
4040

4141
```bash
42-
# View a decrypted secret
42+
# View a decrypted secret (requires cluster access)
4343
kseal cat secrets/app.yaml
4444

4545
# Export all secrets to files
4646
kseal export --all
4747

4848
# Encrypt a plaintext secret
4949
kseal encrypt secret.yaml -o sealed.yaml
50+
51+
# Offline decryption (no cluster access needed)
52+
kseal export-keys # Backup keys while you have access
53+
kseal decrypt sealed.yaml # Decrypt using local keys
54+
kseal decrypt-all --in-place # Decrypt all SealedSecrets
5055
```
5156

5257
## Commands
@@ -89,8 +94,59 @@ kseal encrypt secret.yaml
8994
# To file
9095
kseal encrypt secret.yaml -o sealed.yaml
9196

92-
# Replace original
93-
kseal encrypt secret.yaml --replace
97+
# Replace original file
98+
kseal encrypt secret.yaml --in-place
99+
```
100+
101+
### `kseal export-keys`
102+
103+
Export sealed-secrets private keys from cluster for offline decryption.
104+
105+
```bash
106+
# Export to default location
107+
kseal export-keys # → .kseal-keys/
108+
109+
# Custom output directory
110+
kseal export-keys -o ./backup
111+
112+
# From different namespace
113+
kseal export-keys -n kube-system
114+
```
115+
116+
### `kseal decrypt`
117+
118+
Decrypt a SealedSecret using local private keys (no cluster access needed).
119+
120+
```bash
121+
# Using keys from default location
122+
kseal decrypt sealed.yaml
123+
124+
# Using specific key file
125+
kseal decrypt sealed.yaml --private-key ./key.pem
126+
127+
# From stdin
128+
cat sealed.yaml | kseal decrypt
129+
130+
# Filter keys by pattern
131+
kseal decrypt sealed.yaml --private-keys-regex "2025"
132+
```
133+
134+
### `kseal decrypt-all`
135+
136+
Decrypt all SealedSecrets in a directory using local private keys.
137+
138+
```bash
139+
# Search current directory, output to stdout
140+
kseal decrypt-all
141+
142+
# Search specific directory
143+
kseal decrypt-all ./manifests
144+
145+
# Replace files in-place
146+
kseal decrypt-all --in-place
147+
148+
# Custom keys location
149+
kseal decrypt-all --private-keys-path ./backup
94150
```
95151

96152
### `kseal init`
@@ -160,9 +216,10 @@ kseal automatically manages kubeseal binary versions:
160216

161217
## Security
162218

163-
- Add `.unsealed/` to your `.gitignore`
164-
- Never commit plaintext secrets to version control
165-
- Requires cluster access to decrypt secrets
219+
- Add `.unsealed/` and `.kseal-keys/` to your `.gitignore`
220+
- Never commit plaintext secrets or private keys to version control
221+
- Store exported keys securely (e.g., password manager, encrypted backup)
222+
- Offline decryption with `kseal decrypt` requires the private keys - keep them safe
166223

167224
## Contributing
168225

kseal/binary.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""Kubeseal binary management - download and version handling."""
22

3+
from __future__ import annotations
4+
35
import platform
46
import shutil
57
import stat
@@ -20,14 +22,17 @@
2022
from rich.status import Status
2123

2224
from .config import get_version as get_config_version
25+
from .github import get_latest_version
2326
from .settings import add_downloaded_version, get_default_version
2427

25-
GITHUB_API_URL = "https://api.github.com/repos/bitnami-labs/sealed-secrets/releases/latest"
2628
DOWNLOAD_URL_TEMPLATE = (
2729
"https://github.com/bitnami-labs/sealed-secrets/releases/download/"
2830
"v{version}/kubeseal-{version}-{os}-{arch}.tar.gz"
2931
)
3032

33+
# Re-export for backwards compatibility
34+
__all__ = ["get_latest_version"]
35+
3136

3237
def get_default_binary_dir() -> Path:
3338
"""Get the default directory for kubeseal binaries."""
@@ -89,15 +94,6 @@ def detect_arch() -> str:
8994
raise RuntimeError(f"Unsupported architecture: {machine}")
9095

9196

92-
def get_latest_version() -> str:
93-
"""Fetch the latest kubeseal version from GitHub API."""
94-
response = httpx.get(GITHUB_API_URL, follow_redirects=True, timeout=30)
95-
response.raise_for_status()
96-
data = response.json()
97-
tag = data["tag_name"]
98-
return tag.lstrip("v")
99-
100-
10197
def get_version() -> str:
10298
"""Get the kubeseal version to use.
10399
@@ -135,7 +131,7 @@ def download_kubeseal(version: str, target_path: Path) -> None:
135131
tarball_path = Path(tmpdir) / "kubeseal.tar.gz"
136132

137133
with httpx.stream("GET", url, follow_redirects=True, timeout=60) as response:
138-
response.raise_for_status()
134+
_ = response.raise_for_status()
139135
total_size = int(response.headers.get("content-length", 0))
140136

141137
with Progress(
@@ -149,8 +145,8 @@ def download_kubeseal(version: str, target_path: Path) -> None:
149145

150146
with open(tarball_path, "wb") as f:
151147
for chunk in response.iter_bytes():
152-
f.write(chunk)
153-
progress.update(task, advance=len(chunk))
148+
_ = f.write(chunk)
149+
_ = progress.update(task, advance=len(chunk))
154150

155151
with Status("[bold blue]Extracting...[/]", console=console):
156152
with tarfile.open(tarball_path, "r:gz") as tar:
@@ -163,7 +159,7 @@ def download_kubeseal(version: str, target_path: Path) -> None:
163159
raise RuntimeError("kubeseal binary not found in tarball")
164160

165161
extracted_binary = Path(tmpdir) / "kubeseal"
166-
extracted_binary.rename(target_path)
162+
_ = extracted_binary.rename(target_path)
167163

168164
target_path.chmod(target_path.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
169165

0 commit comments

Comments
 (0)