Help us shape the future of Go! Take the 2026 Go Developer Survey

credentials

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Index

Constants

View Source
const (
	KeysPageURL  = "https://login.tailscale.com/admin/settings/keys"
	OAuthPageURL = "https://login.tailscale.com/admin/settings/oauth"
)

Admin-console pages an operator must visit by hand. TSLink cannot mint a user-owned access token or an OAuth client for the operator, so every credential-related error and expiry warning points here instead of only saying "run tslink login".

View Source
const (
	MetadataSchemaVersion = 1

	SlotAPIKey       = "api-key"
	SlotClientSecret = "client-secret"

	KindAPIAccessToken    = "api_access_token"
	KindOAuthClientSecret = "oauth_client_secret"

	ExpirySourceUser       = "user"
	ExpirySourceAssumedMax = "assumed_max"

	VerifyResultOK           = "ok"
	VerifyResultUnauthorized = "unauthorized"
	VerifyResultForbidden    = "forbidden"
	VerifyResultUnreachable  = "unreachable"

	ExpiryStateNone     = "none"
	ExpiryStateOK       = "ok"
	ExpiryStateExpiring = "expiring"
	ExpiryStateExpired  = "expired"
	ExpiryStateUnknown  = "unknown"

	// APIKeyAssumedMaxLifetime is Tailscale's documented upper bound for a
	// user-owned access token. When the operator does not state the real expiry
	// at login time, TSLink assumes this maximum and records the assumption as
	// expires_at_source=assumed_max so consumers can tell it apart from fact.
	APIKeyAssumedMaxLifetime = 90 * 24 * time.Hour

	// ExpiringSoonThreshold is the window before expires_at in which status and
	// doctor report the api-key slot as expiring rather than ok.
	ExpiringSoonThreshold = 14 * 24 * time.Hour
)

Value-free credential metadata. The file records what TSLink knows about each stored credential without ever holding the credential itself: a short sha256 fingerprint, when it was stored, when it is expected to expire, and the last remote verification result. A missing or corrupt file never blocks a command; readers report the affected slot as unknown and doctor surfaces a warning.

Variables

View Source
var ErrCredentialWritePartial = errors.New("credential write may be partially committed")

ErrCredentialWritePartial means compensation after a failed write could not prove the prior keyring value was restored. Callers with a wider transaction should attempt their own snapshot rollback and report the partial state.

View Source
var ErrMetadataCorrupt = errors.New("credential metadata file is unreadable or malformed")

ErrMetadataCorrupt reports that credential-meta.json exists but cannot be trusted. Callers treat the affected slots as unknown and continue.

View Source
var ErrMutationLockBusy = errors.New("credential transaction lock busy")

ErrMutationLockBusy reports that another credential transaction held the lock for the whole wait. It is wrapped in a registry.StableCodeError with the CLI's retryable "conflict" code.

View Source
var ErrUnknownCredentialSlot = errors.New("unknown credential slot")

ErrUnknownCredentialSlot rejects slot names outside the two supported slots.

View Source
var ErrUserOwnedAPIKeyRequired = errors.New("user-owned Tailscale API access token required")

ErrUserOwnedAPIKeyRequired means an operation cannot use a tailnet-owned OAuth client and needs an API access token tied to an inviting user.

Functions

func BackfillMetadata

func BackfillMetadata(now time.Time) ([]string, error)

BackfillMetadata records metadata for any stored credential that lacks it. It is the daemon-startup migration counterpart of MigrateFromLegacy and returns the slots it backfilled.

func ClassifyAPIError

func ClassifyAPIError(operation string, err error) error

ClassifyAPIError turns a Tailscale REST failure into a stable, actionable error. HTTP 401 means the credential itself is expired, revoked, or invalid and maps to api_token_unauthorized with the key-bootstrap steps; HTTP 403 means the credential is valid but lacks the role or scope and maps to api_forbidden. Every other failure is returned wrapped with the operation label and no stable code, exactly as before. The wrapped chain is preserved so callers that classify with errors.Is keep working.

func ClientSecretAuthKey

func ClientSecretAuthKey(clientSecret string, opts AuthKeyOptions) (string, error)

ClientSecretAuthKey builds a tsnet auth key from a candidate OAuth client secret without reading or mutating any persisted credential. It is used to semantically validate a client secret via a disposable, ephemeral Up before the login transaction commits and retires the previous credential.

func DeleteAPIKey

func DeleteAPIKey()

DeleteAPIKey removes the API key from all storage locations.

func DeleteAPIKeyChecked

func DeleteAPIKeyChecked() error

DeleteAPIKeyChecked removes the API key from all storage locations and surfaces cleanup failures for callers that need transactional semantics.

func DeleteClientSecret

func DeleteClientSecret()

DeleteClientSecret removes the client secret from all storage locations.

func DeleteClientSecretChecked

func DeleteClientSecretChecked() error

DeleteClientSecretChecked removes the OAuth client secret from all storage locations and surfaces cleanup failures for transactional credential swaps.

func DeleteSlotMetadata

func DeleteSlotMetadata(slot string) error

DeleteSlotMetadata removes one slot record under the credential mutation lock. When the file is corrupt or no slots remain, the whole file is removed.

func DeleteSlotMetadataLocked

func DeleteSlotMetadataLocked(slot string) error

DeleteSlotMetadataLocked is DeleteSlotMetadata for a caller that already holds the credential mutation lock inside WithMutationTransaction.

func DeleteStoredCredentialKindStrict

func DeleteStoredCredentialKindStrict(slot string) error

DeleteStoredCredentialKindStrict removes exactly one credential slot from every supported store and reads back each store, mirroring DeleteStoredCredentialsStrict for selective logout.

func DeleteStoredCredentialKindStrictLocked

func DeleteStoredCredentialKindStrictLocked(slot string) error

DeleteStoredCredentialKindStrictLocked is DeleteStoredCredentialKindStrict for a caller that already holds the credential mutation lock, such as logout removing a slot and its metadata in one transaction.

func DeleteStoredCredentialsStrict

func DeleteStoredCredentialsStrict() error

DeleteStoredCredentialsStrict removes API-key/client-secret credentials from every supported store and reads back each store. It keeps going after errors and returns their join so callers never report success while residual credential risk remains.

func DeleteStoredCredentialsStrictLocked

func DeleteStoredCredentialsStrictLocked() error

DeleteStoredCredentialsStrictLocked is DeleteStoredCredentialsStrict for a caller that already holds the credential mutation lock, such as logout removing values and metadata in one transaction.

func DeriveAuthKey

func DeriveAuthKey(ctx context.Context, opts AuthKeyOptions) (string, error)

DeriveAuthKey creates a short-lived, single-use, pre-authorized auth key from the API key.

func ExpiryState

func ExpiryState(meta *SlotMetadata, present bool, now time.Time) (string, *int)

ExpiryState classifies one slot. A present slot without trusted metadata is unknown; an absent slot is none; a slot with no expires_at is ok.

func Fingerprint

func Fingerprint(value string) string

Fingerprint returns the first eight hex characters of sha256(value). It is safe to print: it identifies a credential without revealing it.

func GetAPIKey

func GetAPIKey() (string, error)

GetAPIKey retrieves the API key from keychain or file. Returns ("", nil) if no key is stored.

func GetAuthKey

func GetAuthKey(ctx context.Context, opts AuthKeyOptions) (string, error)

GetAuthKey returns a usable auth key, trying (in order):

  1. Client secret (OAuth long-lived credential, used directly)
  2. Derive from API key (if available)
  3. Read legacy authkey file

func GetClientSecret

func GetClientSecret() (string, error)

GetClientSecret retrieves the client secret from keychain or file. Returns ("", nil) if no client secret is stored.

func HasClientSecret

func HasClientSecret() bool

HasClientSecret returns true if a client secret is stored.

func HasStoredCredential

func HasStoredCredential() (bool, error)

HasStoredCredential reports whether any supported auth credential is configured without deriving or consuming a fresh auth key.

func IsolateForTesting

func IsolateForTesting()

IsolateForTesting switches this process to go-keyring's in-memory mock, so a test can never read, write, or delete the operator's real OS keyring items. go-keyring offers no way back to the real provider once the mock is installed. It also moves the account credential lock into the config directory, which tests point at temporary directories, and refuses any lock path inside the real account home.

Every test binary that links go-keyring calls this from an init function in a _test.go file; that runs before TestMain and before any test, and self-exec children re-run the same binary and install it again. TestEveryKeyringLinkingTestBinaryInstallsIsolation enforces the rule. Production code must never call it.

func MigrateFromLegacy

func MigrateFromLegacy() (migrated bool)

MigrateFromLegacy moves a file-based API key into the system keychain. Safe to call even if there's nothing to migrate.

func NewTailscaleClient

func NewTailscaleClient() (*tailscale.Client, error)

NewTailscaleClient creates a Tailscale API client from stored credentials. OAuth client secrets take precedence over API keys, matching GetAuthKey. Returns (nil, nil) if no API credential is configured.

func NewTailscaleClientWithAPIKey

func NewTailscaleClientWithAPIKey(key string) (*tailscale.Client, error)

NewTailscaleClientWithAPIKey creates a Tailscale API client from an explicit candidate key without reading or mutating persisted credentials.

func NewTailscaleClientWithUserOwnedAPIKey

func NewTailscaleClientWithUserOwnedAPIKey() (*tailscale.Client, error)

NewTailscaleClientWithUserOwnedAPIKey creates a Tailscale API client from the stored user-owned API access token only. It deliberately does not fall back to an OAuth client secret: invite mutations require an inviting user, while OAuth clients are tailnet-owned and have no user identity.

func NextAPIForbidden

func NextAPIForbidden() []string

NextAPIForbidden is the recovery sequence for an HTTP 403: the credential is valid but its user role or OAuth scopes do not permit the operation.

func NextAPIKeyBootstrap

func NextAPIKeyBootstrap() []string

NextAPIKeyBootstrap is the three-step recovery sequence for a missing, expired, or rejected user-owned tskey-api- token. Every error next list that used to say only "tslink login --api-key-stdin" references this one sequence.

func NextOAuthBootstrap

func NextOAuthBootstrap() []string

NextOAuthBootstrap is the recovery sequence for adding a tailnet-owned OAuth client secret, the durable credential for daemon node authentication.

func RecordCredentialStored

func RecordCredentialStored(slot, value string, opts StoredOptions) (SlotMetadata, *SlotMetadata, error)

RecordCredentialStored writes the slot record after a credential commit and returns the previous record when one existed. A corrupt metadata file is replaced: the new commit is the only fact TSLink can still vouch for. The read-modify-write runs under the credential mutation lock.

func RecordVerification

func RecordVerification(slot, fingerprint, result string, now time.Time) error

RecordVerification stores the outcome of a remote probe of the credential with the given fingerprint. Under the credential mutation lock it records the verdict only if the slot's metadata still describes that credential: a slot without metadata, or one rotated while the probe ran, is left alone so a probe can never invent a stored_at or stamp a verdict on another value.

func RemoveMetadataFile

func RemoveMetadataFile() error

RemoveMetadataFile deletes credential-meta.json under the credential mutation lock. Missing files are not errors.

func RemoveMetadataFileLocked

func RemoveMetadataFileLocked() error

RemoveMetadataFileLocked is RemoveMetadataFile for a caller that already holds the credential mutation lock inside WithMutationTransaction.

func RequireStoredCredential

func RequireStoredCredential() error

RequireStoredCredential returns a user-facing auth error if no credential exists.

func SaveClientSecret

func SaveClientSecret(secret string) error

SaveClientSecret stores an OAuth client secret. It is the source-compatible wrapper for callers that do not need to report the selected backend.

func SaveMetadata

func SaveMetadata(doc Metadata) error

SaveMetadata atomically writes credential-meta.json with owner-only permissions. An empty slot map removes the file instead.

func SetAPIKey

func SetAPIKey(key string) error

SetAPIKey stores the API key. It is the source-compatible wrapper for callers that do not need to report the selected backend.

func SetMutationLockPathForTesting

func SetMutationLockPathForTesting(path string) (restore func())

Used only by fake-keyring tests; production always resolves the OS account.

func SetMutationLockTimeoutForTesting

func SetMutationLockTimeoutForTesting(timeout time.Duration) (restore func())

Used only by lock-contention tests; production always waits 5 s.

func SlotKind

func SlotKind(slot string) string

SlotKind maps a slot to its credential kind label.

func ValidSlot

func ValidSlot(slot string) bool

ValidSlot reports whether slot names a supported credential slot.

func VerificationResultForError

func VerificationResultForError(err error) string

VerificationResultForError maps a remote probe outcome to the stored last_verified_result vocabulary.

func WithMutationTransaction

func WithMutationTransaction(fn func(*MutationTransaction) error) error

func WorstExpiryState

func WorstExpiryState(states ...string) string

WorstExpiryState folds per-slot states into one headline state.

func WriteSlotMetadata

func WriteSlotMetadata(slot string, meta SlotMetadata) error

WriteSlotMetadata replaces one slot record verbatim under the credential mutation lock.

func WriteSlotMetadataLocked

func WriteSlotMetadataLocked(slot string, meta SlotMetadata) error

WriteSlotMetadataLocked is WriteSlotMetadata for a caller that already holds the credential mutation lock inside WithMutationTransaction. Login uses it to record a commit and to restore the previous record on rollback.

Types

type AuthKeyOptions

type AuthKeyOptions struct {
	Tags          []string
	Ephemeral     bool
	Description   string
	ClientFactory TailscaleClientFactory
}

AuthKeyOptions configures the derived auth key.

type CredentialBackend

type CredentialBackend string

CredentialBackend identifies the storage location that accepted a credential. It contains no credential material and is safe to report.

const (
	CredentialBackendKeyring CredentialBackend = "keyring"
	CredentialBackendFile    CredentialBackend = "file"
)

func SaveClientSecretWithBackend

func SaveClientSecretWithBackend(secret string) (CredentialBackend, error)

SaveClientSecretWithBackend stores an OAuth client secret and reports the backend that actually accepted it. The key must start with "tskey-client-". File fallback follows the same write-new, strictly-delete-old transaction as API keys.

func SetAPIKeyWithBackend

func SetAPIKeyWithBackend(key string) (CredentialBackend, error)

SetAPIKeyWithBackend stores the API key and reports the backend that actually accepted it. A failed keyring write may fall back to a private file only after every file precondition is satisfied. It reports file success only after any stale keyring value has been removed and readback proves it absent.

type CredentialFileSnapshot

type CredentialFileSnapshot struct {
	// contains filtered or unexported fields
}

CredentialFileSnapshot holds the credential files that existed when a transaction began. It carries credential material: never log or persist it.

type CredentialKindStatus

type CredentialKindStatus struct {
	Keyring CredentialLocationStatus
	File    CredentialLocationStatus
}

CredentialKindStatus is a value-free presence report for all storage locations of one credential kind.

func (CredentialKindStatus) Present

func (s CredentialKindStatus) Present() bool

type CredentialLocationStatus

type CredentialLocationStatus struct {
	Enabled bool
	Present bool
}

CredentialLocationStatus is a value-free presence report for one credential storage location. It deliberately never carries credential material.

type Inventory

type Inventory struct {
	APIKey       SlotView
	ClientSecret SlotView
	// MetadataError is non-nil when credential-meta.json exists but is
	// unreadable or malformed; present slots then report unknown.
	MetadataError error
	// Backfilled lists slots whose metadata was created or re-anchored during
	// this call because a credential existed without matching bookkeeping.
	Backfilled []string
	// BackfillError is non-nil when a backfill could not be persisted.
	BackfillError error
	// ExpiryState is the worst per-slot state.
	ExpiryState string
}

Inventory is the value-free report for both slots plus metadata health.

func DescribeSlots

func DescribeSlots(values SlotValues, now time.Time, persist bool) Inventory

DescribeSlots computes the value-free inventory for the given stored values. When a slot holds a credential but has no matching metadata (or the stored fingerprint no longer matches), the slot is backfilled with stored_at=now and the assumed maximum lifetime; persist controls whether that backfill is written to disk, which happens only while the slot still stores that value. No disk access happens when neither slot is present.

func DescribeSlotsWithMetadata

func DescribeSlotsWithMetadata(values SlotValues, doc Metadata, loadErr error, now time.Time) Inventory

DescribeSlotsWithMetadata is the pure classification step behind DescribeSlots: it never touches disk, so callers (and tests) can evaluate an inventory against an in-memory document. loadErr is the LoadMetadata error for doc; when non-nil every present slot is reported as unknown.

type Metadata

type Metadata struct {
	SchemaVersion int                     `json:"schema_version"`
	Slots         map[string]SlotMetadata `json:"slots"`
}

Metadata is the on-disk document.

func LoadMetadata

func LoadMetadata() (Metadata, error)

LoadMetadata reads credential-meta.json. A missing file yields an empty document and no error. A malformed or unreadable file yields ErrMetadataCorrupt.

type MutationTransaction

type MutationTransaction struct{}

MutationTransaction keeps the shared keyring lock across a multi-step credential commit, including its readback and rollback. Its methods skip re-locking; callers must use it only inside WithMutationTransaction.

func (*MutationTransaction) DeleteAPIKeyChecked

func (*MutationTransaction) DeleteAPIKeyChecked() error

func (*MutationTransaction) DeleteClientSecretChecked

func (*MutationTransaction) DeleteClientSecretChecked() error

func (*MutationTransaction) RestoreRemovedCredentialFiles

func (*MutationTransaction) RestoreRemovedCredentialFiles(snapshot CredentialFileSnapshot) error

RestoreRemovedCredentialFiles writes back each snapshotted file that is now missing. It never overwrites or deletes a file: after a rollback, an existing file is the one the value restore wrote. Windows never writes credential files, so it restores nothing there.

func (*MutationTransaction) SaveClientSecretWithBackend

func (*MutationTransaction) SaveClientSecretWithBackend(secret string) (CredentialBackend, error)

func (*MutationTransaction) SetAPIKeyWithBackend

func (*MutationTransaction) SetAPIKeyWithBackend(key string) (CredentialBackend, error)

func (*MutationTransaction) SnapshotCredentialFiles

func (*MutationTransaction) SnapshotCredentialFiles() CredentialFileSnapshot

SnapshotCredentialFiles records the API key and client secret files. A keyring write removes the file copy, and a login snapshot records only the effective keyring-first value, so a file that disagreed with the keyring would otherwise be lost when the login rolls back.

type ProbeOutcome

type ProbeOutcome struct {
	Slot     string
	Present  bool
	Result   string
	Recorded bool
	// Cause is the sanitized probe failure; nil when Result is ok.
	Cause error
}

ProbeOutcome is the value-free result of one remote credential probe.

func ProbeStoredCredential

func ProbeStoredCredential(ctx context.Context, slot string, now time.Time) (ProbeOutcome, error)

ProbeStoredCredential verifies one stored slot against the Tailscale API and records the outcome in credential-meta.json. The returned error covers only local failures (unreadable store, unknown slot); remote rejection is data in ProbeOutcome so doctor can classify it without parsing text.

type SlotMetadata

type SlotMetadata struct {
	Kind               string     `json:"kind"`
	Fingerprint        string     `json:"fingerprint"`
	StoredAt           time.Time  `json:"stored_at"`
	ExpiresAt          *time.Time `json:"expires_at"`
	ExpiresAtSource    string     `json:"expires_at_source,omitempty"`
	LastVerifiedAt     *time.Time `json:"last_verified_at"`
	LastVerifiedResult string     `json:"last_verified_result,omitempty"`
}

SlotMetadata is the per-slot bookkeeping record. It never carries the credential value.

func NewSlotMetadata

func NewSlotMetadata(slot, value string, opts StoredOptions) (SlotMetadata, error)

NewSlotMetadata builds the bookkeeping record for a credential that was just stored. The api-key slot defaults to the assumed 90-day maximum when the operator gives no expiry; the client-secret slot never expires.

func ReadSlotMetadata

func ReadSlotMetadata(slot string) (*SlotMetadata, error)

ReadSlotMetadata returns the record for one slot, or nil when absent.

type SlotValues

type SlotValues struct {
	APIKey       string
	ClientSecret string
}

SlotValues carries the currently stored values only long enough to compute presence and fingerprints. Callers must not persist or print it.

func StoredSlotValues

func StoredSlotValues() (SlotValues, error)

StoredSlotValues reads both credential slots for inventory purposes. Read failures are returned so callers can report them without guessing presence.

type SlotView

type SlotView struct {
	Slot        string
	Present     bool
	Metadata    *SlotMetadata
	Backfilled  bool
	ExpiryState string
	DaysLeft    *int
}

SlotView is the value-free report for one slot.

type StoredCredentialStatus

type StoredCredentialStatus struct {
	APIKey       CredentialKindStatus
	ClientSecret CredentialKindStatus
}

StoredCredentialStatus is the strict credential inventory used by destructive cleanup paths. It distinguishes not-found from unreadable/disabled stores.

func InspectStoredCredentialsStrict

func InspectStoredCredentialsStrict() (StoredCredentialStatus, error)

InspectStoredCredentialsStrict reports whether API-key/client-secret credentials are present in every supported store. It fails closed when the keyring is disabled or unreadable because an old credential may remain there.

func (StoredCredentialStatus) AnyPresent

func (s StoredCredentialStatus) AnyPresent() bool

type StoredOptions

type StoredOptions struct {
	Now             time.Time
	ExpiresAt       *time.Time
	ExpiresAtSource string
	// Verified records that the login path proved the credential against the
	// remote API before committing it.
	Verified bool
}

StoredOptions describes what login learned about a freshly committed credential.

type TailscaleClientFactory

type TailscaleClientFactory func() (*tailscale.Client, error)

TailscaleClientFactory constructs the REST client used to derive an auth key. Command callers can supply the tailapi loopback-override gate without creating an import cycle back from credentials to tailapi.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL