Skip to content
Merged
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
61 changes: 61 additions & 0 deletions .github/SECURITY_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Deployment Security

## GitHub Actions Security Measures

### Secret Protection

1. **Fork PR Protection**: Secrets are NOT available to workflows triggered by pull requests from forks
2. **First-time Contributor Approval**: New contributors require manual approval before workflows run
3. **Secret Masking**: GitHub automatically masks secrets in logs

### Our Additional Protections

1. **Environment Protection**: The deployment workflow uses the `trunk` environment which can be configured with:
- Required reviewers
- Deployment branches restrictions
- Wait timer before deployment

2. **Workflow Conditions**: The deploy workflow only runs when:
- CI passes on the main branch (not from PRs)
- OR manually triggered by authorized users

3. **Branch Protection**: Ensure `main` branch has:
- Required PR reviews
- Required status checks (CI must pass)
- No direct pushes

## Setting Up Environment Protection (Recommended)

1. Go to Settings → Environments in your GitHub repository
2. Create a "trunk" environment
3. Configure protection rules:
- Add required reviewers
- Restrict deployment branches to `main`
- Add any required wait time

4. Move your `FLY_API_TOKEN` secret to the trunk environment:
- Remove it from repository secrets
- Add it to the trunk environment secrets

## Security Best Practices

1. **Rotate API tokens regularly**
2. **Use least-privilege tokens** - Create Fly.io tokens with only necessary permissions
3. **Monitor deployments** - Set up notifications for production deployments
4. **Audit workflow changes** - Review any PR that modifies `.github/workflows/`

## What Attackers Can't Do

Even if an attacker:
- Opens a PR with modified workflows → No access to secrets
- Tries to echo secrets → GitHub masks them
- Modifies the workflow file → Environment protection blocks unauthorized deployments
- Gets their PR merged → Branch protection requires reviews

## Emergency Response

If you suspect compromise:
1. Immediately revoke the Fly.io API token: `fly auth revoke <token>`
2. Generate a new token: `fly auth token`
3. Update the GitHub secret
4. Review deployment logs for unauthorized activity
38 changes: 38 additions & 0 deletions .github/workflows/deploy-fly.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Deploy to Fly.io

on:
workflow_run:
workflows: ["CI"]
types:
- completed
branches: ["main"]
workflow_dispatch: # Allow manual trigger

jobs:
deploy:
name: Deploy to Fly.io
runs-on: ubuntu-latest
environment:
name: trunk
# Only deploy if:
# 1. CI workflow succeeded AND triggered by push to main (not from a PR)
# 2. OR manually triggered (workflow_dispatch)
# This prevents deployment from PRs even if someone modifies the workflow
if: |
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main') ||
github.event_name == 'workflow_dispatch'

steps:
- uses: actions/checkout@v4

- name: Setup Fly CLI
uses: superfly/flyctl-actions/setup-flyctl@master

- name: Deploy to Fly.io
run: |
cd examples/deployments/fly.io
./deploy.sh
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/go.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ jobs:
runs-on: ubuntu-latest
needs: [quality-checks, test]
# Only build and push Docker images on main branch after tests pass
# Note: Fly.io deployment happens in a separate workflow (deploy-fly.yaml)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
Expand Down
53 changes: 53 additions & 0 deletions examples/deployments/fly.io/DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Fly.io Deployment

This directory contains configuration for deploying Haystack to Fly.io.

## Automatic Deployment

The application is automatically deployed to Fly.io when changes are merged to the `main` branch via GitHub Actions.

### Setup Requirements

1. **Fly.io Account**: You need a Fly.io account with an app already created
2. **GitHub Secret**: Add your Fly.io API token as a GitHub secret named `FLY_API_TOKEN`

### Getting Your Fly.io API Token

```bash
fly auth token
```

### Manual Deployment

To deploy manually from your local machine:

```bash
cd examples/deployments/fly.io
fly deploy
```

### Monitoring

Check deployment status:
```bash
fly status
fly logs
```

### Configuration

The deployment configuration is in:
- `fly.toml` - Fly.io app configuration
- `Dockerfile` - Container definition
- `.github/workflows/deploy-fly.yaml` - GitHub Actions workflow

### Environment Variables

The following environment variables are configured in `fly.toml`:
- `HAYSTACK_ADDR`: Server binding address (fly-global-services:1337)
- `HAYSTACK_STORAGE`: Storage backend (memory)
- `HAYSTACK_LOG_LEVEL`: Logging level (debug/info/error/silent)

### UDP Service

The service runs on UDP port 1337 and is accessible via the Fly.io global anycast network.
2 changes: 1 addition & 1 deletion examples/deployments/fly.io/fly.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ app = 'haystack-example-trunk'
primary_region = 'dfw'

[build]
image = 'nomasters/haystack:tree-TREE_HASH'
dockerfile = "Dockerfile"

[deploy]
strategy = 'immediate'
Expand Down
30 changes: 17 additions & 13 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ func (s *Server) serve() {
return
default:
if err := s.processPacket(); err != nil {
// In a production environment, you might want to log this
// but continue processing other packets
// Log the error and continue processing other packets
s.logger.Errorf("Failed to process packet: %v", err)
continue
}
}
Expand Down Expand Up @@ -158,15 +158,13 @@ func (s *Server) processPacket() error {
switch n {
case needle.HashLength:
// GET operation: 32-byte hash query
s.logger.Infof("GET request from %s", addr)
return s.handleGet(buf[:n], addr)
case needle.NeedleLength:
// SET operation: 192-byte needle storage
s.logger.Infof("SET request from %s", addr)
return s.handleSet(buf[:n], addr)
default:
// Invalid packet size, log and drop
s.logger.Debugf("Invalid packet size %d from %s", n, addr)
s.logger.Debugf("Invalid packet size %d", n)
return nil
}
}
Expand All @@ -177,21 +175,21 @@ func (s *Server) handleGet(hashBytes []byte, addr net.Addr) error {
var hash needle.Hash
copy(hash[:], hashBytes)

// Log the GET request with hash
s.logger.Infof("GET request for hash %x", hash)

// Retrieve needle from storage
n, err := s.storage.Get(hash)
if err != nil {
s.logger.Debugf("GET failed for hash %x: %v", hash, err)
return fmt.Errorf("failed to get needle: %w", err)
return fmt.Errorf("GET failed to hash %x: %w", hash, err)
}

// Send the full needle as response
bytesWritten, err := s.conn.WriteTo(n.Bytes(), addr)
if err != nil {
s.logger.Errorf("Failed to send GET response to %s: %v", addr, err)
if _, err := s.conn.WriteTo(n.Bytes(), addr); err != nil {
s.logger.Errorf("Failed to send GET response for hash %x: %v", hash, err)
return err
}

s.logger.Debugf("GET response sent to %s (%d bytes)", addr, bytesWritten)
s.logger.Debugf("GET successful for hash %x", hash)
return nil
}

Expand All @@ -204,11 +202,17 @@ func (s *Server) handleSet(needleBytes []byte, _ net.Addr) error {
return fmt.Errorf("invalid needle: %w", err)
}

// Get the hash for logging
hash := n.Hash()
s.logger.Infof("SET request for hash %x", hash)

// Store the needle
if err := s.storage.Set(n); err != nil {
return fmt.Errorf("failed to store needle: %w", err)
return fmt.Errorf("SET failed to store needle for hash %x: %w", hash, err)
}

s.logger.Debugf("SET successful for hash %x", hash)

// No response for SET operations (by design)
return nil
}