Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Local-first CLI that pushes secrets from your vault-backed environment into depl

Vaults solve storage, rotation, access control, and audit. The gap is **delivery**: getting the same secret into GitHub Actions, Vercel, and SST without paste-into-Slack, clipboard history, or hand-copying the same key into three dashboards — and without half-rotated deploys when one destination is forgotten.

SecretSync is a thin CLI for that gap. You declare routing in a checked-in YAML file (reviewable in PRs; plaintext stays out of git), inject values from your vault into the process environment (`op run`, Doppler, etc.), and push. Destination quirks — GitHub repo/environment/org scopes, Vercel deployment targets, SST stages — stay behind connectors so the config stays simple.
SecretSync is a thin CLI for that gap. You declare routing in a checked-in YAML file (reviewable in PRs; plaintext stays out of git), inject values from your vault into the process environment (`op run`, Doppler, etc.), and push. Destination quirks — GitHub repo/environment/org scopes, Vercel deployment targets, SST stages, AWS SSM path prefixes — stay behind connectors so the config stays simple.

Full secrets platforms (Infisical and similar) can do this and more, but they are overkill when you already trust a vault and only need to say which names land where. Plaintext should only move through process memory, authenticated provider APIs, or one-shot env injection — never config, plans, logs, or temp files.

Expand All @@ -18,6 +18,11 @@ Full secrets platforms (Infisical and similar) can do this and more, but they ar
## Install with uv
uv tool install secretsync-cli

# Optional AWS connectors (Parameter Store via boto3):
# uv tool install 'secretsync-cli[aws]'
# Or every optional extra:
# uv tool install 'secretsync-cli[all]'

# Scaffold config + 1Password-style env template
secretsync init
```
Expand Down Expand Up @@ -98,6 +103,30 @@ Kind is declared once under `secrets` or `variables`. Connectors map kind to the
>
> **Breaking:** Vercel destinations require `teamId`. Project env deployments need `scope.kind: environment` (and destination `project`). Team shared env uses `scope.kind: shared-environment` with optional `scope.projects`.

AWS SSM Parameter Store (requires `secretsync-cli[aws]` or `[all]`):

```yaml
destinations:
ssm:
connector: aws-ssm
region: us-east-1 # optional; else AWS_REGION / session default
# keyId: alias/aws/ssm # optional KMS key for SecureString
# tier: Standard

deployments:
- name: ssm-production
set: production
destination: ssm
scope:
pathPrefix: /myapp/production
secrets:
apiKey: API_KEY # → /myapp/production/API_KEY (SecureString)
variables:
logLevel: LOG_LEVEL # → /myapp/production/LOG_LEVEL (String)
```

Auth uses the standard AWS credential chain (`AWS_PROFILE` or `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`). No `auth.tokenEnv`.

Vercel destination modes (selected by `scope.kind`):

```yaml
Expand Down Expand Up @@ -185,6 +214,7 @@ We currently support these destinations.
- [GitHub Actions](https://github.com/features/actions)
- [Vercel](https://vercel.com/)
- [SST](https://sst.dev/)
- [AWS Systems Manager Parameter Store](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) (`aws-ssm`; install `[aws]` or `[all]`)

## Audit

Expand Down
10 changes: 5 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ This document describes how SecretSync is structured and how secrets move — es
|
validate -> plan -> apply
|
+--------+--------+--------+
| | | |
GitHub Vercel SST fakes
(HTTPS) (HTTPS) (process)
+--------+--------+--------+--------+
| | | | |
GitHub Vercel SST aws-ssm fakes
(HTTPS) (HTTPS) (process) (boto3)
```

Both Click and Textual use the same [`AppServices`](../src/secretsync/application/services.py) composition root. There is no second plan/apply implementation.
Expand All @@ -35,7 +35,7 @@ Both Click and Textual use the same [`AppServices`](../src/secretsync/applicatio
| `config/` | Pydantic YAML schema + loader + set composition |
| `application/` | validate, plan, apply coordinator |
| `sources/` | Environment secret source |
| `destinations/` | Connector protocol, registry, GitHub/Vercel/SST/fakes |
| `destinations/` | Connector protocol, registry, GitHub/Vercel/SST/aws-ssm/fakes |
| `infrastructure/` | HTTP client, process runner, dotenv encoder, redaction |
| `presentation/` | Human + versioned JSON renderers (value-free) |
| `tui/` | Textual screens + CSS |
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ license = "MIT"
license-files = ["LICENSE"]
authors = [{ name = "SecretSync contributors" }]
requires-python = ">=3.12"
keywords = ["secrets", "github-actions", "vercel", "sst", "1password", "cli"]
keywords = ["secrets", "github-actions", "vercel", "sst", "aws", "ssm", "1password", "cli"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Environment :: Console",
Expand All @@ -30,6 +30,10 @@ dependencies = [
"loguru>=0.7.3",
]

[project.optional-dependencies]
aws = ["boto3>=1.35,<2"]
all = ["secretsync-cli[aws]"]

[project.urls]
Homepage = "https://github.com/achadha235/secretsync"
Repository = "https://github.com/achadha235/secretsync"
Expand Down Expand Up @@ -61,6 +65,7 @@ dev = [
"ruff>=0.12,<1",
"mypy>=1.17,<2",
"types-pyyaml>=6.0.12",
"boto3>=1.35,<2",
]

[tool.uv]
Expand Down
61 changes: 58 additions & 3 deletions src/secretsync/application/health.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
from collections.abc import Mapping
from dataclasses import dataclass
from pathlib import Path
from typing import Any

import anyio
import httpx
from loguru import logger

Expand Down Expand Up @@ -46,6 +48,7 @@ async def run_health(
results.append(await _check_github(environ, github_token_env))
results.append(await _check_vercel(environ, vercel_token_env))
results.append(await _check_aws(environ))
results.append(await _check_aws_ssm_boto(environ))
return HealthReport(results=tuple(results))


Expand Down Expand Up @@ -103,11 +106,15 @@ async def _check_vercel(environ: Mapping[str, str], token_env: str) -> HealthChe
)


async def _check_aws(environ: Mapping[str, str]) -> HealthCheckResult:
name = "SST / AWS"
def _aws_credentials_present(environ: Mapping[str, str]) -> bool:
has_profile = bool(environ.get("AWS_PROFILE"))
has_keys = bool(environ.get("AWS_ACCESS_KEY_ID") and environ.get("AWS_SECRET_ACCESS_KEY"))
if not has_profile and not has_keys:
return has_profile or has_keys


async def _check_aws(environ: Mapping[str, str]) -> HealthCheckResult:
name = "SST / AWS"
if not _aws_credentials_present(environ):
msg = (
"AWS_PROFILE (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY) not set, "
"skipping check for SST connector"
Expand Down Expand Up @@ -156,6 +163,54 @@ async def _check_aws(environ: Mapping[str, str]) -> HealthCheckResult:
)


async def _check_aws_ssm_boto(environ: Mapping[str, str]) -> HealthCheckResult:
"""Optional boto3 STS probe for the aws-ssm connector."""
name = "AWS SSM (boto3)"
if not _aws_credentials_present(environ):
msg = (
"AWS_PROFILE (or AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY) not set, "
"skipping check for aws-ssm connector"
)
logger.info(msg)
return HealthCheckResult(name=name, status="skip", message=msg)

try:
import boto3 # type: ignore[import-untyped]
except ImportError:
msg = (
"boto3 not installed, skipping check for aws-ssm connector "
"(install secretsync-cli[aws] or [all])"
)
logger.info(msg)
return HealthCheckResult(name=name, status="skip", message=msg)

region = environ.get("AWS_REGION") or environ.get("AWS_DEFAULT_REGION")
kwargs: dict[str, Any] = {}
if region:
kwargs["region_name"] = region
logger.debug("Running boto3 sts get_caller_identity for aws-ssm health")
try:
client = boto3.client("sts", **kwargs)

def _probe() -> Any:
return client.get_caller_identity()

identity = await anyio.to_thread.run_sync(_probe)
if identity.get("Account"):
return HealthCheckResult(name=name, status="ok", message="AWS SSM (boto3): OK")
return HealthCheckResult(
name=name,
status="fail",
message="AWS SSM (boto3): FAIL (empty identity)",
)
except Exception as exc: # noqa: BLE001
return HealthCheckResult(
name=name,
status="fail",
message=f"AWS SSM (boto3): FAIL ({type(exc).__name__})",
)


def health_token_envs_from_config(config_path: Path, environ: Mapping[str, str]) -> tuple[str, str]:
"""Best-effort read tokenEnv names from yaml; fall back to defaults."""
github, vercel = "GITHUB_TOKEN", "VERCEL_TOKEN"
Expand Down
2 changes: 2 additions & 0 deletions src/secretsync/application/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing import Any

from secretsync.config.loader import ConfigLoader
from secretsync.destinations.aws_ssm import AwsSsmFactory
from secretsync.destinations.fake import builtin_fake_factories
from secretsync.destinations.github_actions import GitHubActionsFactory
from secretsync.destinations.registry import ConnectorRegistry
Expand Down Expand Up @@ -42,6 +43,7 @@ def create_services(environ: Mapping[str, str]) -> AppServices:
GitHubActionsFactory(),
VercelFactory(),
SstFactory(),
AwsSsmFactory(),
]
return AppServices(
config_loader=ConfigLoader(),
Expand Down
2 changes: 2 additions & 0 deletions src/secretsync/destinations/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Destination connectors."""

from secretsync.destinations.aws_ssm import AwsSsmFactory
from secretsync.destinations.fake import builtin_fake_factories
from secretsync.destinations.github_actions import GitHubActionsFactory
from secretsync.destinations.registry import KNOWN_CONNECTOR_IDS, ConnectorRegistry
Expand All @@ -8,6 +9,7 @@

__all__ = [
"KNOWN_CONNECTOR_IDS",
"AwsSsmFactory",
"ConnectorRegistry",
"GitHubActionsFactory",
"SstFactory",
Expand Down
Loading