{"id":18952397,"url":"https://github.com/tailscale/setec","last_synced_at":"2025-04-13T04:09:11.050Z","repository":{"id":184577908,"uuid":"668054346","full_name":"tailscale/setec","owner":"tailscale","description":"A secrets management service that uses Tailscale for access control","archived":false,"fork":false,"pushed_at":"2025-03-24T03:48:44.000Z","size":344,"stargazers_count":275,"open_issues_count":2,"forks_count":9,"subscribers_count":23,"default_branch":"main","last_synced_at":"2025-04-11T16:18:52.517Z","etag":null,"topics":["api","go","golang","library","production","secrets","tailscale"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-3-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tailscale.png","metadata":{"files":{"readme":"docs/README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":"audit/audit.go","citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":"AUTHORS","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-18T23:22:11.000Z","updated_at":"2025-04-11T03:23:11.000Z","dependencies_parsed_at":"2023-07-29T05:29:42.559Z","dependency_job_id":"d3c097b3-c33e-4ba9-be14-e0307e4fe39f","html_url":"https://github.com/tailscale/setec","commit_stats":null,"previous_names":["tailscale/setec"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailscale%2Fsetec","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailscale%2Fsetec/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailscale%2Fsetec/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tailscale%2Fsetec/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tailscale","download_url":"https://codeload.github.com/tailscale/setec/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248661705,"owners_count":21141450,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","go","golang","library","production","secrets","tailscale"],"created_at":"2024-11-08T13:33:10.022Z","updated_at":"2025-04-13T04:09:11.010Z","avatar_url":"https://github.com/tailscale.png","language":"Go","funding_links":[],"categories":["Go"],"sub_categories":[],"readme":"# setec\n\nSetec is a lightweight secrets management service that uses Tailscale for access control.\n\n\u003e [!IMPORTANT]\n\u003e File issues in our [primary open source repository](https://github.com/tailscale/tailscale/issues).\n\n## Table of Contents\n\n- [Background](#background)\n- [Get started](#get-started)\n- [API Overview](#api-overview)\n   - [Basic Operations](#basic-operations)\n   - [Current Active Versions](#current-active-versions)\n   - [Deleting Values](#deleting-values)\n- [Basic Usage](#basic-usage)\n- [Migrating to Setec](#migrating-to-setec)\n- [Operations and Maintenance](#operations-and-maintenance)\n   - [Secret Rotation](#secret-rotation)\n   - [Automatic Updates](#automatic-updates)\n   - [Bootstrapping and Availability](#bootstrapping-and-availability)\n- [Testing](#testing)\n\n### Additional documentation\n\n- [API documentation](api.md)\n- [Running a setec server](server.md)\n\n## Background\n\nPrograms in production often need access to passwords, API keys, parameterized\nconnection URLs, and other sensitive information (hereafter, **secrets**) at\nruntime.  Securely deploying secrets in production complicates operations, and\ntypically involves a combination of tedious manual intervention (e.g., copying\nsecrets out of secure storage at startup), or integrating with complex secrets\nmanagement infrastructure (usually specific to the deployment environment).\n\nSetec comprises a lightweight HTTP-based [API](api.md) and a corresponding\nserver, that allows programs running on a tailnet to securely fetch secrets at\nruntime. Access to secrets is governed by the tailnet's policy document, and\nthe server maintains secrets in encrypted storage, keeps an audit log of\naccesses, and manages periodic backups.\n\nThe setec server integrates with existing key-management infrastructure to\nbootstrap its own deployment (as of 24-Sep-2023, AWS KMS is supported).\nOnce the server is running on a tailnet, other programs can use it to access\ntheir production secrets with a basic WireGuard-encrypted HTTP request, rather\nthan having to distribute secrets via files, environment variables, or manual\noperator intervention.\n\nIn addition to reducing deployment toil, this also helps reduce the attack\nsurface for managing secrets in third-party deployment environments: All the\nsecrets are stored in one place, with access controls and audit logs to allow\nforensics in the event of a compromise.\n\n## Get started\n\nTo set up a setec server, follow the instructions in [Running a setec server](server.md).\n\n## API Overview\n\nSee also the [full API documentation](api.md).\n\nA **secret** in `setec` is a named collection of values. Each value is an\narbitrary byte string identified by an integer **version number**.\n\n### Basic Operations\n\nThe [setec API](api.md) defines the following basic operations:\n\n- The `get` method retrieves the value of a single version of a secret.\n  Clients use this method to obtain the secrets they need in production.\n\n- The `info` and `list` methods report the names and available versions (but\n  not the values) of secrets accessible to the caller.\n\n- The `put` method creates or adds a new value to a secret. The server assigns\n  and reports a version number for the value.\n\n### Current Active Versions\n\n- At any time, one version of the secret is designated as its **current active\n  version**. The active version is reported by default from the `get` method if\n  the caller does not specify a specific version.\n\n- When a secret is first created, its initial value (version 1) becomes its\n  current active version.\n\n- Thereafter, the `activate` method must be used to update the current active\n  version. This ensures the operator of the service has precise control over\n  which version of a secret should be used at a time.\n\n### Deleting Values\n\n- The `delete-version` method deletes a single version of a secret.  This\n  method will not delete the current active version.\n\n- The `delete` method deletes all the versions of a secret, removing it\n  entirely from the service.\n\n\n## Basic usage\n\nAll of the methods of the setec API are HTTPS POST requests. Calls to the API\nmust include a `Sec-X-Tailscale-No-Browsers: setec` header.  This prevents\nbrowser scripts from initiating calls to the service. The examples below assume\nyou have a setec server running at `secrets.example.ts.net` on your tailnet.\n\nGo programs can use the [setec client library][setecclient] provided in this\nrepository. The API supports any language, however, so the examples below are\nwritten using the curl command-line tool.\n\nA program that wishes to fetch a secret at runtime does so by issuing an HTTPS\nPOST request to the `/api/get` method of the secrets service.\n\nFor example:\n\n```shell\ncurl -H sec-x-tailscale-no-browsers:setec -H content-type:application/json -X POST \\\n  https://secrets.example.ts.net/api/get -d '{\"Name\": \"prod/myprogram/secret-name\"}'\n```\n\nThe `\"Name\"` field specifies which secret to fetch. The name must be non-empty,\nbut is otherwise not interpreted by the service, and you should choose names\nthat make sense for your environment. Here we're using a basic path layout,\ngrouping secrets by deployment environment (`dev` vs. `prod`) and program.\n\nIf the caller has access to the secret named `prod/myprogram/secret-name`, the\nserver will return the **current active version** of this secret, e.g.,\n\n```js\n{\"Value\":\"aGVsbG8sIHdvcmxk\",\"Version\":1}\n```\n\nThe secret `Value` is base64-encoded (in this case, the string \"hello, world\")\nand the `Version` is an integer indicating the sequential version number of\nthis secret value. New versions of a secret are added by calling `/api/put` and\nany existing version can be set as the \"active\" version of the secret.\n\nThis repository also defines a [`setec` command-line tool][seteccli], written\nin Go, that can be used to call the API from scripts. To install it, run:\n\n```bash\ngo install github.com/tailscale/setec/cmd/setec@latest\n```\n\nIf for some reason you do not want to use the CLI directly, the following shell\nfunction is roughly equivalent for the purposes of these examples:\n\n```bash\n# Usage: setec_get \u003csecret-name\u003e\n# This assumes setec resides at \"secrets.example.ts.net\".\n# Roughly equivalent to \"setec get \u003csecret-name\u003e\" using the CLI.\nsetec_get() {\n  local name=\"$1\"\n  curl -s https://secrets.example.ts.net/api/get -X POST --fail \\\n    -H sec-x-tailscale-no-browsers:setec -H content-type:application/json \\\n    -d '{\"name\":\"'\"$name\"'\"}' \\\n  | jq -r '.Value|@base64d'\n}\n```\n\n\n## Migrating to Setec\n\nWhen migrating existing programs to use setec, there are two main patterns of\nuse you are likely to encounter: Environment variables, and key files.\n\nThe examples below are written as bash scripts, and make use of the [`setec`\ncommand-line tool][seteccli].\n\nIn practice most interesting programs will be written in a more structured\nlanguage, the goal of using bash here is to illustrate the plumbing in a\ngeneric way.\n\n### Migrating Environment Variables\n\nEnvironment variables are often used to plumb secrets to programs running in\ncontainers. Typically the host allows you to add secrets to a store they\nmanage, and to associate them with environment variables that they set up when\nstarting up a container on your behalf.  From the perspective of your program,\nthe environment variable is ambient.\n\nFor example, the following script expects `EXAMPLE_API_KEY` to be defined.\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\ncall_api() {\n   curl -s https://api.example.com/v1/method --url-query \"q=$@\" \\\n      -H \"Authorization: Bearer ${EXAMPLE_API_KEY}\"\n}\n\n# ...\n```\n\nTo replace this usage, load `EXAMPLE_API_KEY` by calling setec:\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\n# Here we assume the secret name is \"prod/script/example-api-key\".\nEXAMPLE_API_KEY=\"$(setec -s https://secrets.example.ts.net get prod/script/example-api-key)\"\n\ncall_api() {\n   curl -s https://api.example.com/v1/method --url-query \"q=$@\" \\\n      -H \"Authorization: Bearer ${EXAMPLE_API_KEY}\"\n}\n\n# ...\n```\n\n### Migrating Key Files\n\nKey files are sometimes used to plumb secrets to programs running in VMs or on\ncolocated physical machines. Typically key files will be stored in a central\nsecret manager and deployed to the VM or hardware using tools like Ansible or\nChef. From the perspective of your program, the secret is a plain file at a\nknown path on the local filesystem.\n\nFor example, the following script expects `/secrets/example-api-key` to contain\nthe API key for a service:\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\nreadonly EXAMPLE_API_KEY=\"$(cat /secrets/example-api-key)\"\n\ncall_api() {\n   curl -s https://api.example.com/v1/method --url-query \"q=$@\" \\\n      -H \"Authorization: Bearer ${EXAMPLE_API_KEY}\"\n}\n\n# ...\n```\n\nTo replace this usage, load `EXAMPLE_API_KEY` by calling setec:\n\n```bash\n#!/usr/bin/env bash\nset -euo pipefail\n\n# Here we assume the secret name is \"prod/script/example-api-key\".\nreadonly EXAMPLE_API_KEY=\"$(setec -s https://secrets.example.ts.net prod/script/example-api-key)\"\n\ncall_api() {\n   curl -s https://api.example.com/v1/method --url-query \"q=$@\" \\\n      -H \"Authorization: Bearer ${EXAMPLE_API_KEY}\"\n}\n\n# ...\n```\n\n\n## Operations and Maintenance\n\nThis section discusses some common service operation and maintenance tasks that\ninteract with secrets management, and how they interact with setec.\n\n### Secret Rotation\n\nSometimes secrets need to be rotated. For example, if an API credential expires\nor is compromised, the programs that depend on that credential will need to be\nupdated to use a new value.\n\nThe simplest way to rotate secrets managed by setec is for the operator to\ninstall a new version of the secret, mark that version as active, and restart\nor redeploy the programs that use that secret so they will pick up the latest\nversion. For example, using the `setec` command-line tool, suppose we have:\n\n```bash\n# HTTP: POST /api/info\nsetec info dev/hello-world\n```\n\nThis will print the name of the secret, along with the version numbers that\nexist and which one is active:\n\n```\nName:           dev/hello-world\nActive version: 7\nVersions:       1, 2, 3, 4, 5, 6, 7\n```\n\nIn this example we see that the `dev/hello-world` secret has 7 versions and\nversion 7 is currently active. To add a new version, use `setec put`:\n\n```bash\n# HTTP: POST /api/put\nsetec put dev/hello-world\n```\n\nThis will prompt you for the new secret version:\n\n```\nEnter secret: ****\nConfirm secret: ****\nSecret saved as \"dev/hello-world\", version 8\n```\n\nAt this point, version 8 exists, but version 7 is still active. To activate\nversion 8, write:\n\n```bash\n# HTTP: POST /api/activate\nsetec activate dev/hello-world 8\n```\n\nNow, any client that fetches the active version of `dev/hello-world` will get\nthis new value instead.\n\n### Automatic Updates\n\nThe example above shows how to simply rotate a secret, but that still requires\nall the programs which depend on that secret to be restarted or redeployed to\npick up the new value. That is fine for batch processing or low-volume services\nthat do not receive a lot of traffic, but may be disruptive for servers with a\nlarger active query load.\n\nA program that does not wish to restart to pick up new secret values can poll\nthe secrets API periodically, to see whether a new version is available. To do\nthis, the request to the `/api/get` method may include the current version the\nclient is using, and set the `\"UpdateIfChanged\"` flag to `true`:\n\n```json\n{\"Name\":\"dev/hello-world\", \"Version\":7, \"UpdateIfChanged\":true}\n```\n\nIf the current active version of the secret still matches what the client\nrequested, the server will report 304 Not Modified, indicating to the client\nthat it still (already) has the active version. This allows the client to check\nfor an update without sending secret values over the wire except when needed,\nand does not trigger an audit-log record unless the server reveals the value of\na secret to the client, or the permissions have changed (causing the client to\nbe denied access).\n\nThe Go client library provides a [`setec.Store`][setecstore] type that handles\nautomatic updates using this polling mechanism, but the same logic can be\nimplemented in any language. In Go, this looks like:\n\n```go\nimport \"github.com/tailscale/setec/client/setec\"\n\nfunc main() {\n   // Construct a setec.Store that tracks a set of secrets this program needs.\n   // This will block until all the requested secrets are available.\n   // Thereafter, in the background, it will poll for new versions at\n   // approximately the given interval.\n   st, err := setec.NewStore(context.Background(), setec.StoreConfig{\n      Client: setec.Client{Server: \"https://secrets.example.ts.net\"},\n      Secrets: []string{\n         \"prod/myprogram/secret-1\",\n         \"prod/myprogram/secret-2\",\n      },\n      PollInterval: 24*time.Hour,\n   })\n   if err != nil {\n      log.Fatalf(\"NewStore: %v\", err)\n   }\n\n   // Fetch a secret from the store. A setec.Secret is a handle that always\n   // delivers the current value, automatically updated as new versions become\n   // available from the server.\n   apiKey := st.Secret(\"prod/myprogram/secret-1\")\n\n   cli := someservice.NewClient(\"username\", apiKey.Get())\n   // ...\n}\n```\n\nAlternatively, you can obtain a [`setec.Updater`][setecupdater], which uses a\nuser-provided callback to update a local value whenever a new version of a\nsecret becomes available. An updater is safe for concurrent use by multiple\ngoroutines.\n\nFor example:\n\n```go\n// Construct an updater, given a callback that takes a secret value and returns\n// a new someservice client using that secret.\nclient, err := setec.NewUpdater(ctx, store, \"secret/name\", func(secret []byte) (*svc.Client, error) {\n   return svc.NewClient(\"username\", secret)\n})\nif err != nil {\n   return fmt.Errorf(\"initialize client: %w\", err)\n}\n\n// Whenever you need a client, call the Get method:\nrsp, err := client.Get().Method(ctx, args)\n// ...\n```\n\nThe updater constructs the initial client by invoking the callback with the\ncurrent secret value when `NewUpdater` is called. Thereafter, calls to\n`u.Get()` will return the same client until the secret changes. When that\nhappens, `Get` invokes the callback again with the new secret value, to make a\nfresh client.  If an error occurs while updating the client, the updater keeps\nreturning the previous value.\n\n#### Explicit Refresh\n\nOrdinarily a `Store` will automatically update secret values in the background.\nIf a program needs to explicitly refresh the values of secrets at a specific\ntime (for example, in response to an operator signal or other event) it may\nexplicitly call the `Store` value's [`Refresh`][strefresh] method, which\neffects a poll of all known secrets synchronously. It is safe for the client to\ndo this concurrently with a background poll; the store will coalesce the\noperations.\n\n### Bootstrapping and Availability\n\nA reasonable concern when fetching secrets from a network service is what\nhappens if the secrets service is not reachable when a program needs to fetch a\nsecret. A good answer depends on the nature of the program: Batch processing\ntools can usually afford to wait and retry until the service becomes\navailable. Interactive services, by contrast, may not be able to tolerate\nwaiting.\n\nTo minimize the impact of a secrets server being temporarily unreachable, a\nprogram should fetch all desired secrets at startup and cache them (typically\nin memory) while running. If the secrets service is unreachable when the\nprogram first starts, it should wait and retry as necessary, or fail the\nstartup process. Once the program has initial values for all its desired\nsecrets, it can poll for new values in the background.\n\nThis ensures that even if the secrets server is occasionally unreachable, the\nprogram always has a good value for each secret, even if one that is\n(temporarily) slightly stale.  The Go client library's [`setec.Store`][setecstore]\ntype implements this logic automatically (see the example above).\n\nA program that needs be able to start immediately, even when the secrets server\nis unavailable, can trade a bit of security for availability by caching the\nactive versions of the secrets it needs in persistent storage (e.g., a local\nfile). When the program start or restarts, it can fall back to the cached\nvalues if the secrets service is not immediately available. The Go client\nlibrary's [`setec.Store`][setecstore] type supports this kind of caching as an\noptional feature, and the same logic can be implemented in any language.\n\nIn Go, you can enable a file cache using `setec.NewFileCache`:\n\n```go\n// Create or open a cache associated with the specified file path.\nfc, err := setec.NewFileCache(\"/data/secrets.cache\")\nif err != nil {\n    return fmt.Errorf(\"creating cache: %w\", err)\n}\nst, err := setec.NewStore(ctx, setec.StoreConfig{\n    Client:       setec.Client{Server: \"https://secrets.example.ts.net\"},\n    Secrets:      []string{\"secret1\", \"secret2\", \"secret3\"},\n    PollInterval: 12 * time.Hour,\n    Cache:        fc,\n})\n// ...\n```\n\nWith the cache enabled, the store will automatically persist new secrets\nfetched from the server into the cache as they become available. Morever, when\nthe store is created, the store will not block waiting for the server if all\nthe requested secrets already have a version stored in the cache.\n\n**Enabling a file cache represents a security tradeoff:** The cache records all\nthe program's secret values to local storage, which means they can be read by\n(other) programs and users with access to that storage. In return, however, the\nprogram can start up immediately using cached data, even if the secrets server\nis not reachable when it launches.\n\n\u003e [!WARNING]\n\u003e When you enable a secrets cache for a program, new secret values may not\n\u003e immediately become available even if the program is restarted. By design, if\n\u003e a cached value is available at startup, the store does not wait for the\n\u003e secrets service to respond before delivering the initial (cached) value.\n\u003e\n\u003e The store will see the new value (and update the cache) the next time it\n\u003e successfully polls.  If the program only looks at the initial value of the\n\u003e secret, however, it will not see the new value until it is restarted _after_\n\u003e the next update.\n\u003e\n\u003e As a general rule, we recommend you _not_ enable a cache unless the program\n\u003e cannot tolerate even a temporary outage of the secrets service or your\n\u003e tailnet at program start (for example, if it is part of your infrastructure\n\u003e bootstrap).  If you _must_ use a cache, we advise you structure your program\n\u003e to automatically handle new secret values, and not to \"lock in\" the initial\n\u003e value of a secret when the program starts up. You may also wish to decrease\n\u003e the polling interval from the default.\n\n## Self-Contained Operation\n\nIn some cases, you may need to run a program entirely without access to a\nsecrets server. For example, in standalone testing and bootstrapping it may be\nimpractical to set up a secrets service, or you may want to deploy the same\nprogram across different environments where a secrets service may or may not be\npresent.\n\nTo support these cases, the Go [`setec`][setecpkg] package provides a\n[`FileClient`][fileclient] type that can be plugged into a\n[`setec.Store`][setecstore].  Unlike the normal [`setec.Client`][setecclient],\na `FileClient` does not use the network at all, but vends secrets read from a\nplaintext JSON file on the local filesystem.\n\nTo use this, construct a `Store` using a `FileClient` instead:\n\n```go\nfc, err := setec.NewFileClient(\"/data/secrets.json\")\nif err != nil {\n   return fmt.Errorf(\"creating file client: %w\", err)\n}\nst, err := setec.NewStore(ctx, setec.StoreConfig{\n   Client:  fc,\n   Secrets: []string{\"svc/secret1\", \"svc/secret2\"},\n})\n```\n\nAs input, the `FileClient` expects a file containing a JSON message like:\n\n```json\n{\n   \"svc/secret1\": {\n      \"secret\": {\"Version\": 1, \"Value\": \"dGhlIGtub3dsZWRnZSBpcyBmb3JiaWRkZW4=\"}\n   },\n   \"svc/secret2\": {\n      \"secret\": {\"Version\": 5, \"TextValue\": \"eat your vegetables\"}\n   }\n}\n```\n\nThe object keys are the secret names, and the values have the structure shown.\nBinary secret values are base64 encoded as `\"Value\"`, or if you are constructing\na secrets file by hand you may include plain text secrets as `\"TextValue\"`\ninstead.\n\nA program that may be used in multiple environments can choose which client to\nuse at startup, and otherwise the store will work the same:\n\n```go\nvar client setec.StoreClient\nif *secretsAddr != \"\" {\n   client = setec.Client{Server: *secretsAddr}\n} else if fc, err := setec.NewFileClient(*localSecrets); err != nil {\n   log.Fatalf(\"Open file client: %v\", err)\n} else {\n   client = fc\n}\n\nst, err := setec.NewStore(ctx, setec.StoreConfig{\n   Client: client,\n   // ... other options as usual\n})\n```\n\n### FileClient and Caching\n\nAlthough the two are related, a `FileClient` differs from the cache mechanism\ndescribed in the previous section.  With a cache enabled, a store loads secrets\nfrom the cache file at startup, but otherwise communicates with a secrets\nservice in the usual way.\n\nWith a `FileClient`, however, the store does not access the network at all: It\nreads the specified file once at startup, and only serves those exact secret\nvalues.\n\nThe two mechanisms are intended to be complementary. For example, you could\nbootstrap a new deployment using the following steps:\n\n- Create a secrets file seeded with the initial secrets your program needs.\n\n- Start up a store with a `FileClient` that uses your seed file. This lets you\n  get your program working even if your secrets server is not yet set up.\n\n- When you are ready to switch to a secrets server, you can enable a cache, and\n  the store will prime the cache with the secrets from your seed file.\n\n- Then, when you switch from a `FileClient` to a regular `Client`, you will\n  have an already-primed cache available (which can be helpful as you are\n  working out the inevitable quirks of a new service configuration).\n\nThe opposite is also true: By design, the format of the cache files can also be\nused directly as the input to a `FileClient` if you need to spin up a new\ninstance of an existing server somewhere else.\n\n\n## Unit Testing\n\nFor programs written in Go, the [`setectest`][setectest] package provides\nin-memory implementations of the setec server and its database for use in\nwriting tests. The servers created by this package use the same implementation\nas the production server (`setec server` in the CLI), but use a stub\nimplementation of encryption (so they do not require an external KMS).\n\nA [`setectest.Server`][stserver] can be used with the [`httptest`][httptest]\npackage to hook up a real, working setec client and/or store in tests:\n\n```go\n// Create a new database and add some secrets to it.\ndb := setectest.NewDB(t, nil)\ndb.MustPut(db.Superuser, \"alpha\", \"ok\")\ndb.MustPut(db.Superuser, \"bravo\", \"yes\")\ndb.MustPut(db.Superuser, \"bravo\", \"no\")\n\n// Create a setec server using that database.\nts := setectest.NewServer(t, db, nil)\n\n// Stand up an in-memory HTTP server exporting ts.\nhs := httptest.NewServer(ts.Mux)\ndefer hs.Close()\n\n// Start a setec.Store talking to this server.\nst, err := setec.NewStore(context.Background(), setec.StoreConfig{\n    // Note the client here uses the httptest URL and client.\n    Client:  setec.Client{Server: hs.URL, DoHTTP: hs.Client().Do},\n    Secrets: []string{\"alpha\", \"bravo\"},\n})\nif err != nil {\n    t.Fatalf(\"NewStore: %v\", err)\n}\n// ... the rest of the test\n```\n\n\n\u003c!-- references --\u003e\n[httptest]: https://godoc.org/net/http/httptest\n[seteccli]: https://github.com/tailscale/setec/tree/main/cmd/setec\n[setecpkg]: https://godoc.org/github.com/tailscale/setec/client/setec\n[setecclient]: https://godoc.org/github.com/tailscale/setec/client/setec#Client\n[fileclient]: https://godoc.org/github.com/tailscale/setec/client/setec#FileClient\n[setecstore]: https://godoc.org/github.com/tailscale/setec/client/setec#Store\n[setectest]: https://godoc.org/github.com/tailscale/setec/setectest\n[setecupdater]: https://godoc.org/github.com/tailscale/setec/client/setec#Updater\n[stserver]: https://godoc.org/github.com/tailscale/setec/setectest#Server\n[strefresh]: https://godoc.org/github.com/tailscale/setec/client/setec#Store.Refresh\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftailscale%2Fsetec","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftailscale%2Fsetec","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftailscale%2Fsetec/lists"}