La creazione di un’API sicura coinvolge più che la semplice aggiunta dell’autenticazione. È necessario pensare alla sicurezza a ogni livello dell’applicazione, da come gestisci l’input dell’utente a come proteggi il tuo server dagli attacchi. Questa guida ti accompagnerà attraverso le pratiche di sicurezza essenziali per la tua API Goa, con esempi pratici che puoi implementare oggi stesso.
La sicurezza non riguarda l’avere una singola serratura forte - si tratta di avere più livelli di protezione. Se un livello fallisce, gli altri sono ancora presenti per proteggere la tua applicazione. Ecco come implementare più livelli di sicurezza nel tuo servizio Goa:
// Primo livello usa HTTPS
var _ = Service("secure_service", func() {
Security(JWTAuth, func() { // Secondo livello: Richiede autenticazione valida
Scope("api:write") // Terzo livello: Verifica permessi specifici
})
// Quarto livello: Valida tutti gli input
Method("secureEndpoint", func() {
Payload(func() {
Field(1, "data", String)
MaxLength("data", 1000) // Previene payload di grandi dimensioni
})
})
})
Questo codice dimostra come stratificare più controlli di sicurezza. Pensalo come un castello medievale - hai il fossato (HTTPS), il muro esterno (autenticazione), il muro interno (autorizzazione) e, infine, l’attenta ispezione di tutti i visitatori (validazione dell’input).
Per il rate limiting, vorrai implementarlo utilizzando middleware sul tuo server HTTP Goa. Ecco come aggiungere il rate limiting al tuo servizio:
package main
import (
"context"
"net/http"
"time"
"golang.org/x/time/rate"
goahttp "goa.design/goa/v3/http"
"goa.design/goa/v3/middleware"
)
// RateLimiter crea middleware che limita la frequenza delle richieste
func RateLimiter(limit rate.Limit, burst int) middleware.Middleware {
limiter := rate.NewLimiter(limit, burst)
return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow() {
http.Error(w, "Troppe richieste", http.StatusTooManyRequests)
return
}
h.ServeHTTP(w, r)
})
}
}
func main() {
// ... configurazione logger, strumentazione ...
// Crea servizio ed endpoint
svc := NewService()
endpoints := gen.NewEndpoints(svc)
mux := goahttp.NewMuxer()
// Crea server
server := gen.NewServer(endpoints, mux, goahttp.RequestDecoder, goahttp.ResponseEncoder, nil, nil)
// Monta gli handler generati
gen.Mount(mux, server)
// Aggiungi middleware alla catena degli handler del server
var handler http.Handler = mux
handler = RateLimiter(rate.Every(time.Second/100), 10)(handler) // 100 req/sec
handler = log.HTTP(ctx)(handler) // Aggiungi logging
// Crea e avvia il server HTTP
srv := &http.Server{
Addr: ":8080",
Handler: handler,
}
// ... codice per lo spegnimento controllato ...
}
Per il rate limiting per endpoint specifico, puoi applicare il rate limiter direttamente agli endpoint specifici:
// RateLimitEndpoint avvolge un endpoint con il rate limiting
func RateLimitEndpoint(limit rate.Limit, burst int) func(goa.Endpoint) goa.Endpoint {
limiter := rate.NewLimiter(limit, burst)
return func(endpoint goa.Endpoint) goa.Endpoint {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if !limiter.Allow() {
return nil, fmt.Errorf("limite di frequenza superato")
}
return endpoint(ctx, req)
}
}
}
func main() {
// ... codice di configurazione del servizio ...
// Crea endpoint
endpoints := &gen.Endpoints{
Forecast: RateLimitEndpoint(rate.Every(time.Second), 10)(
gen.NewForecastEndpoint(svc),
),
TestAll: gen.NewTestAllEndpoint(svc), // Nessun rate limit
TestSmoke: RateLimitEndpoint(rate.Every(time.Minute), 5)(
gen.NewTestSmokeEndpoint(svc),
),
}
// ... resto della configurazione del server ...
}
Questo approccio:
Uno dei principi di sicurezza più importanti è iniziare con impostazioni predefinite sicure. È molto più sicuro iniziare con tutto bloccato e poi aprire selettivamente l’accesso, piuttosto che iniziare aperti e cercare di bloccare le cose successivamente. Ecco come impostare le impostazioni predefinite sicure nella tua API Goa:
var _ = API("secure_api", func() {
// Richiedi autenticazione di default
Security(JWTAuth)
})
Queste impostazioni assicurano che ogni endpoint nella tua API richieda l’autenticazione per impostazione predefinita. Per la sicurezza del trasporto (HTTPS), configurerai questo a livello di server nella tua implementazione:
func main() {
// ... configurazione servizio ed endpoint ...
// Crea configurazione TLS
tlsConfig := &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
// Crea server HTTPS con configurazione sicura
srv := &http.Server{
Addr: ":443",
Handler: handler,
TLSConfig: tlsConfig,
// Imposta timeout per prevenire attacchi slow-loris
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
}
// Avvia server con TLS
log.Printf("Server HTTPS in ascolto su %s", srv.Addr)
if err := srv.ListenAndServeTLS("cert.pem", "key.pem"); err != nil {
log.Fatalf("impossibile avviare il server HTTPS: %v", err)
}
}
Questa implementazione:
Puoi anche combinare questo con altri middleware di sicurezza come il rate limiting:
Quando si tratta di permessi, meno è meglio. Ogni utente e servizio dovrebbe avere esattamente i permessi necessari per svolgere il proprio lavoro - né più, né meno. Questo limita il potenziale danno se un singolo account viene compromesso. Ecco come implementare permessi granulari nella tua API:
var _ = Service("user_service", func() {
// Gli utenti normali possono leggere il proprio profilo
Method("getProfile", func() {
Security(OAuth2Auth, func() {
Scope("profile:read")
})
// L'implementazione assicura che gli utenti possano leggere solo il proprio profilo
Payload(func() {
UserID("id", String, "Profilo da leggere")
})
})
// Solo gli utenti con permesso di scrittura possono aggiornare i profili
Method("updateProfile", func() {
Security(OAuth2Auth, func() {
Scope("profile:write")
})
})
// Le operazioni amministrative richiedono privilegi speciali
Method("deleteUser", func() {
Security(OAuth2Auth, func() {
Scope("admin")
})
})
})
Questo esempio mostra come creare una gerarchia di permessi. Gli utenti normali possono leggere i propri dati, gli utenti con privilegi elevati possono apportare modifiche e solo gli amministratori possono eseguire operazioni pericolose come le cancellazioni.
L’autenticazione appropriata è la prima linea di difesa della tua API. Vediamo come implementare pratiche di autenticazione sicure.
I token sono come chiavi digitali per la tua API. Proprio come le chiavi fisiche, devono essere create in modo sicuro, controllate attentamente e gestite durante tutto il loro ciclo di vita. Ecco come implementare una gestione sicura dei token:
// Genera un nuovo token con misure di sicurezza appropriate
func GenerateToken(user *User) (string, error) {
now := time.Now()
claims := &Claims{
StandardClaims: jwt.StandardClaims{
// Il token è valido a partire da ora
IssuedAt: now.Unix(),
// Il token scade in 24 ore
ExpiresAt: now.Add(time.Hour * 24).Unix(),
// Identifica chi ha emesso il token
Issuer: "your-api",
// Identifica a chi appartiene il token
Subject: user.ID,
},
// Includi i permessi dell'utente
Scopes: user.Permissions,
}
// Usa un metodo di firma sicuro (ECDSA è più sicuro di HMAC)
token := jwt.NewWithClaims(jwt.SigningMethodES256, claims)
return token.SignedString(privateKey)
}
// Valida accuratamente i token in arrivo
func ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{},
func(token *jwt.Token) (interface{}, error) {
// Verifica sempre il metodo di firma
if _, ok := token.Method.(*jwt.SigningMethodECDSA); !ok {
return nil, fmt.Errorf("metodo di firma inaspettato")
}
return publicKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
// Esegui validazione aggiuntiva
if err := validateClaims(claims); err != nil {
return nil, err
}
return claims, nil
}
return nil, fmt.Errorf("token non valido")
}
Implementa una gestione sicura delle password:
// Hash delle password usando algoritmi forti
func HashPassword(password string) (string, error) {
// Usa bcrypt con costo appropriato
hash, err := bcrypt.GenerateFromPassword(
[]byte(password),
bcrypt.DefaultCost,
)
if err != nil {
return "", err
}
return string(hash), nil
}
// Verifica le password
func VerifyPassword(hashedPassword, password string) error {
return bcrypt.CompareHashAndPassword(
[]byte(hashedPassword),
[]byte(password),
)
}
Implementa una gestione sicura delle chiavi API:
// Genera chiavi API sicure
func GenerateAPIKey() string {
// Usa crypto/rand per generazione casuale sicura
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
panic(err)
}
return base64.URLEncoding.EncodeToString(bytes)
}
// Memorizza le chiavi API in modo sicuro
func StoreAPIKey(key string) error {
// Hash della chiave prima della memorizzazione
hashedKey := sha256.Sum256([]byte(key))
// Memorizza nel database
return db.StoreKey(hex.EncodeToString(hashedKey[:]))
}
Implementa RBAC usando gli scope:
var _ = Service("admin", func() {
// Definisci ruoli e permessi
Security(OAuth2Auth, func() {
Scope("admin:read", "Leggi risorse admin")
Scope("admin:write", "Modifica risorse admin")
Scope("admin:delete", "Elimina risorse admin")
})
Method("getUsers", func() {
Security(OAuth2Auth, func() {
Scope("admin:read")
})
})
Method("createUser", func() {
Security(OAuth2Auth, func() {
Scope("admin:write")
})
})
Method("deleteUser", func() {
Security(OAuth2Auth, func() {
Scope("admin:delete")
})
})
})
Implementa l’autorizzazione a livello di risorsa:
func (s *service) authorizeResource(ctx context.Context,
resourceID string) error {
// Ottieni utente dal contesto
user := auth.UserFromContext(ctx)
// Ottieni risorsa
resource, err := s.db.GetResource(resourceID)
if err != nil {
return err
}
// Controlla proprietà o permessi
if !canAccess(user, resource) {
return fmt.Errorf("accesso non autorizzato alla risorsa")
}
return nil
}
Definisci regole di validazione complete:
var _ = Type("UserInput", func() {
Field(1, "username", String, func() {
Pattern("^[a-zA-Z0-9_]{3,30}$")
Example("john_doe")
})
Field(2, "email", String, func() {
Format(FormatEmail)
Example("[email protected]")
})
Field(3, "age", Int, func() {
Minimum(18)
Maximum(150)
Example(25)
})
Field(4, "website", String, func() {
Format(FormatURI)
Example("https://example.com")
})
Required("username", "email", "age")
})
Implementa misure di sicurezza dei contenuti:
var _ = Service("content", func() {
HTTP(func() {
Response(func() {
// Imposta Content Security Policy
Header("Content-Security-Policy", String,
"default-src 'self'")
// Previeni lo sniffing del tipo MIME
Header("X-Content-Type-Options", String, "nosniff")
// Controlla l'incorporamento dei frame
Header("X-Frame-Options", String, "DENY")
})
})
})
Implementa il rate limiting a più livelli:
var _ = Service("api", func() {
// Rate limit globale
Meta("ratelimit:limit", "1000")
Meta("ratelimit:window", "1h")
// Rate limit specifici per metodo
Method("expensive", func() {
Meta("ratelimit:limit", "10")
Meta("ratelimit:window", "1m")
})
})
Implementa misure di protezione DOS:
var _ = Service("api", func() {
// Limita dimensione payload
MaxLength("request_body", 1024*1024) // limite 1MB
// Timeout per operazioni lunghe
Meta("timeout", "30s")
// Limiti di paginazione
Method("list", func() {
Payload(func() {
Field(1, "page", Int, func() {
Minimum(1)
})
Field(2, "per_page", Int, func() {
Minimum(1)
Maximum(100)
})
})
})
})
Implementa risposte di errore sicure:
var _ = Service("api", func() {
Error("unauthorized", func() {
Description("Autenticazione fallita")
// Non esporre dettagli interni
Field(1, "message", String, "Autenticazione richiesta")
})
Error("validation_error", func() {
Description("Input non valido")
Field(1, "fields", ArrayOf(String), "Campi non validi")
})
Method("secure", func() {
Error("unauthorized")
Error("validation_error")
HTTP(func() {
Response("unauthorized", StatusUnauthorized)
Response("validation_error", StatusBadRequest)
})
})
})
Implementa pratiche di logging sicure:
func (s *service) logSecurityEvent(ctx context.Context,
eventType string, details map[string]interface{}) {
// Aggiungi contesto di sicurezza
details["ip_address"] = getClientIP(ctx)
details["user_id"] = getUserID(ctx)
details["timestamp"] = time.Now().UTC()
// Non loggare mai dati sensibili
delete(details, "password")
delete(details, "token")
// Log con livello appropriato
s.logger.WithFields(details).Info(eventType)
}
Forza l’uso di HTTPS:
var _ = API("secure_api", func() {
// Richiedi HTTPS
Meta("transport", "https")
HTTP(func() {
// Reindirizza HTTP a HTTPS
Meta("redirect_http", "true")
// Imposta header HSTS
Response(func() {
Header("Strict-Transport-Security",
String,
"max-age=31536000; includeSubDomains")
})
})
})
Implementa una corretta gestione dei certificati:
func setupTLS() *tls.Config {
return &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
PreferServerCipherSuites: true,
CipherSuites: []uint16{
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
},
}
}
Scrivi test focalizzati sulla sicurezza:
func TestSecurityHandling(t *testing.T) {
tests := []struct {
name string
token string
expectedCode int
expectedBody string
}{
{
name: "token_valido",
token: generateValidToken(),
expectedCode: http.StatusOK,
},
{
name: "token_scaduto",
token: generateExpiredToken(),
expectedCode: http.StatusUnauthorized,
},
{
name: "firma_non_valida",
token: generateTokenWithInvalidSignature(),
expectedCode: http.StatusUnauthorized,
},
{
name: "token_mancante",
token: "",
expectedCode: http.StatusUnauthorized,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Implementazione del test
})
}
}
Implementa la scansione di sicurezza nella tua pipeline:
# Esempio di workflow GitHub Actions
name: Scansione di Sicurezza
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Esegui Scanner di Sicurezza Gosec
uses: securego/gosec@master
with:
args: ./...
- name: Esegui Nancy per Scansione Dipendenze
uses: sonatype-nexus-community/nancy-github-action@main
- name: Esegui Scansione OWASP ZAP
uses: zaproxy/[email protected]
Implementa il monitoraggio di sicurezza:
func monitorSecurityEvents(ctx context.Context) {
// Monitora fallimenti di autenticazione
go monitorAuthFailures(ctx)
// Monitora violazioni del rate limit
go monitorRateLimits(ctx)
// Monitora pattern sospetti
go monitorSuspiciousActivity(ctx)
}
func monitorAuthFailures(ctx context.Context) {
threshold := 5
window := time.Minute * 5
for {
select {
case <-ctx.Done():
return
default:
failures := getRecentAuthFailures(window)
if failures > threshold {
alertSecurityTeam("Rilevato alto tasso di fallimenti di autenticazione")
}
time.Sleep(time.Minute)
}
}
}
Prepara gestori di risposta agli incidenti:
func handleSecurityIncident(incident *SecurityIncident) {
// Logga dettagli dell'incidente
logSecurityIncident(incident)
// Allerta team di sicurezza
alertSecurityTeam(incident)
// Prendi azione immediata
switch incident.Type {
case "tentativo_forza_bruta":
blockIP(incident.SourceIP)
case "compromissione_chiave_api":
revokeAPIKey(incident.APIKey)
case "accesso_non_autorizzato":
terminateUserSessions(incident.UserID)
}
// Crea report dell'incidente
createIncidentReport(incident)
}