16 min read

Learning Objectives

By the end of this module, you will be able to:

  • Automate API token lifecycle using the IONOS Token Manager: generate scoped tokens, set TTLs, and rotate without downtime
  • Provision IAM users, groups, and resource shares as code with Terraform `ionoscloud_user`, `ionoscloud_group`, and `ionoscloud_share`
  • Manage Network Security Group rules through Terraform as part of the infrastructure pipeline, accounting for the resources NSGs do not cover
  • Derive Kubernetes secrets from Terraform outputs so credentials never live in a manifest or a Git repository
  • Automate SSH key distribution and rotation across a fleet using the SSH Key Manager and cloud-init

Unit 5.2: Security Automation

Introduction

You shipped TaskBoard to Managed Kubernetes in Module 3, and it now talks to PostgreSQL, Redis, Object Storage, and Kafka. Every one of those connections is guarded by a credential, and right now those credentials are scattered across token files, Terraform state, and CI secret stores. The moment a token leaks or an engineer leaves the team, you need to rotate fast and you need to do it without a manual click-through in a console.

This unit treats security as code. You will automate the full token lifecycle through the Token Manager, manage users and access with the IONOS IAM model in Terraform, push firewall rules through the same pipeline that provisions your servers, and wire database credentials into Kubernetes secrets straight from Terraform outputs. The IONOS access model has specific edges, group-based authorization rather than fine-grained policies, NSGs that do not bind to managed load balancers or Kubernetes nodes, and tokens that are shown exactly once, and getting those edges right is the difference between an audit-clean platform and a 2 a.m. incident.

1. API Token Lifecycle Automation

Bearer tokens are how every API call, SDK client, and CI pipeline authenticates to IONOS Cloud. Treating them as long-lived secrets pasted into config is the most common security mistake on the platform. The Token Manager lets you generate per-service tokens with bounded lifetimes and rotate them programmatically.

A token is requested against your contract credentials and returned as a JWT. The Basic Authentication method (username and password on each request) is being discontinued and should not be used in automation, so standardize on bearer tokens everywhere.

1.1 Generating and scoping tokens

Request a token using the auth API. The returned token value is a JWT you then pass as Authorization: Bearer <token> on subsequent Cloud API calls.

# Request a bearer token using contract credentials (one-time, e.g. in a bootstrap step)
curl -s --request GET \
  --user "$IONOS_USERNAME:$IONOS_PASSWORD" \
  'https://api.ionos.com/auth/v1/tokens/generate' \
  | jq -r '.token' > taskboard-ci.token

Each token carries a Time To Live (TTL) that fixes how long it is valid before it expires and becomes inactive. The available TTL values are fixed: 1 Hour, 4 Hours, 1 Day, 7 Days, 30 Days, 60 Days, 90 Days, 180 Days, and 365 Days. Pick the shortest TTL a given consumer can tolerate. A CI pipeline that runs on every merge can live with a 7 Day token rotated weekly; a long-running controller may need 30 Days.

A single user can hold up to 100 tokens at once. That ceiling is generous enough to give every service and every environment its own token, which is exactly what you want: one token per service, per environment, so revoking a leaked credential never takes down anything else.

1.2 Rotation without downtime

The token value is displayed exactly once at generation and is not recoverable afterward. There is no "show token again" call, so your rotation logic must capture the value at creation time and write it directly to its destination (a CI secret, a Kubernetes secret) in the same step.

Rotation follows a generate-then-revoke pattern so there is never a gap where no valid token exists.

#!/usr/bin/env bash
set -euo pipefail

# 1. Generate the new token and capture it immediately (only chance to read it)
NEW_TOKEN=$(curl -s --request GET --user "$IONOS_USERNAME:$IONOS_PASSWORD" \
  'https://api.ionos.com/auth/v1/tokens/generate' | jq -r '.token')

# 2. Push it to consumers BEFORE revoking the old one (overlap window)
kubectl create secret generic ionos-api-token \
  --from-literal=token="$NEW_TOKEN" \
  --namespace taskboard --dry-run=client -o yaml | kubectl apply -f -

# 3. List existing tokens, identify the old one by its jti, then delete it
curl -s --request GET --header "Authorization: Bearer $NEW_TOKEN" \
  'https://api.ionos.com/auth/v1/tokens' | jq '.tokens[] | {id: .id, expirationDate}'

Deleting a token in the Token Manager deactivates it immediately, even if its TTL has not yet expired, so the old credential is dead the moment you call DELETE. Run this script on a schedule (a CI cron job or a Kubernetes CronJob) and rotation becomes a property of the platform rather than a task someone remembers to do.

2. IAM as Code

User and group access on IONOS Cloud follows a Role-Based Access Control model built on groups, not on per-resource policy documents. You assign privileges to a group, add users to the group, and share specific resources with the group. There is no fine-grained, per-action policy language, so model your access design around groups from the start.

Managing this in Terraform keeps your access map reviewable in pull requests and reproducible across contracts.

2.1 Users, groups, and privileges

A group carries a set of contract-level privileges. The available group privileges are a fixed catalog: Create Data Center, Create Snapshots, Reserve IP Blocks, Create Internet Access, Use Object Storage, Create Backup Units, Create Kubernetes Clusters, and Access Activity Log. You enable the privileges a group needs and nothing more.

resource "ionoscloud_group" "taskboard_deployers" {
  name                  = "taskboard-deployers"
  create_datacenter     = false
  create_snapshot       = false
  reserve_ip            = false
  access_activity_log   = true
  create_k8s_cluster    = true
  s3_privilege          = true # Use Object Storage
}

resource "ionoscloud_user" "ci_bot" {
  first_name     = "TaskBoard"
  last_name      = "CI"
  email          = "ci-bot@taskboard.example"
  password       = var.ci_bot_password # sensitive, from a secret store
  administrator  = false
  force_sec_auth = false
}

resource "ionoscloud_group_user" "ci_bot_membership" {
  group_id = ionoscloud_group.taskboard_deployers.id
  user_id  = ionoscloud_user.ci_bot.id
}

Keep administrator = false for every automation user. An administrator bypasses group privileges entirely, which defeats the least-privilege model you are building.

2.2 Sharing resources with groups

Privileges control what a group can create. Resource sharing controls what a group can see and act on for resources that already exist. The controllable resource types are Virtual Data Centers, Snapshots, Images, IP Blocks, Backup Units, and Kubernetes Clusters. For each shared resource a group receives an authorization level: Read (implicit as soon as the group is assigned a resource), Edit, and Sharing.

resource "ionoscloud_share" "taskboard_vdc_share" {
  group_id      = ionoscloud_group.taskboard_deployers.id
  resource_id   = ionoscloud_datacenter.taskboard.id
  edit_privilege  = true
  share_privilege = false
}

The following table maps the three authorization levels to what a group member can do, which is the decision you make on every share:

Level Granted by Member can
Read Implicit when the resource is shared View the resource
Edit edit_privilege = true Modify the resource
Sharing share_privilege = true Re-share the resource with other groups

Grant Sharing sparingly. A group that can re-share resources can widen access beyond what your Terraform describes, creating drift between your code and reality.

3. NSG Automation in the Pipeline

Network Security Groups are stateful firewalls applied at the VM or NIC level. Managing their rules in Terraform, rather than editing them by hand, means your network posture lives in the same pipeline as the servers it protects and gets reviewed on every change.

An NSG defaults to deny-all: traffic is dropped unless a rule explicitly allows it. Rules support both INGRESS and EGRESS directions and a broad protocol set including TCP, UDP, ICMP, ICMPv6, GRE, VRRP, ESP, and AH.

3.1 Rules as code

Attach an NSG to a server (covering all its NICs) or to an individual NIC for granular control, then define rules. The platform limits you to 100 rules per NSG, 10 NSGs per NIC, and 200 NSGs per VDC, which are generous enough that you will hit a design problem before a quota.

resource "ionoscloud_nsg" "taskboard_app" {
  name          = "taskboard-app-tier"
  description   = "App tier: allow HTTPS in, Postgres out"
  datacenter_id = ionoscloud_datacenter.taskboard.id
}

resource "ionoscloud_nsg_firewallrule" "allow_https_in" {
  nsg_id          = ionoscloud_nsg.taskboard_app.id
  protocol        = "TCP"
  name            = "https-ingress"
  type            = "INGRESS"
  port_range_start = 443
  port_range_end   = 443
}

resource "ionoscloud_nsg_firewallrule" "allow_pg_out" {
  nsg_id          = ionoscloud_nsg.taskboard_app.id
  protocol        = "TCP"
  name            = "postgres-egress"
  type            = "EGRESS"
  port_range_start = 5432
  port_range_end   = 5432
}

# Bind the NSG to the app server's NIC
resource "ionoscloud_nic" "app_nic" {
  datacenter_id     = ionoscloud_datacenter.taskboard.id
  server_id         = ionoscloud_server.app.id
  lan               = ionoscloud_lan.app_lan.id
  security_groups_ids = [ionoscloud_nsg.taskboard_app.id]
}

The underlying API resource is security-group, and each rule exposes properties such as name, protocol, sourceIp, targetIp, portRangeStart, portRangeEnd, icmpType, and icmpCode. The ICMP fields only apply when the protocol is ICMP or ICMPv6.

3.2 What NSGs do not cover

This is the constraint that costs developers hours of debugging. NSGs apply at the VDC server-NIC level only. They do NOT bind to Managed Application Load Balancer, Managed Network Load Balancer, or Managed Kubernetes. Specifically, Managed Kubernetes node-pool nodes and suspended Cubes are excluded from NSG enforcement.

# WRONG: there is no security_groups argument on a managed load balancer.
# resource "ionoscloud_application_loadbalancer" "alb" {
#   security_groups_ids = [ionoscloud_nsg.x.id]  # not a valid argument
# }

For an ALB-fronted service like TaskBoard's API, you cannot firewall the ALB itself with an NSG. Protect the tier by putting the application servers on a private LAN and applying NSG rules to their NICs so that only the ALB's subnet can reach them. For Kubernetes, enforce traffic policy with Kubernetes NetworkPolicy objects inside the cluster, because the node NICs are outside NSG scope. NSGs are also independent of the legacy per-NIC firewall, so do not expect one to inherit the other's rules.

4. Kubernetes Secrets from Terraform

TaskBoard's pods need the PostgreSQL connection string, the Redis password, and the Object Storage access key. None of those values should ever appear in a committed manifest. The clean pattern is to source them from Terraform outputs (marked sensitive) and create Kubernetes secrets from those outputs at deploy time.

4.1 Sensitive outputs feeding secrets

Mark every credential output sensitive = true so Terraform never prints it to logs or to terraform output without the explicit flag.

output "pg_connection_uri" {
  value     = "postgresql://${ionoscloud_pg_cluster.taskboard.credentials[0].username}:${var.pg_password}@${ionoscloud_pg_cluster.taskboard.dns_name}:5432/taskboard?sslmode=require"
  sensitive = true
}

output "s3_access_key" {
  value     = ionoscloud_s3_key.taskboard.id
  sensitive = true
}

output "s3_secret_key" {
  value     = ionoscloud_s3_key.taskboard.secret_key
  sensitive = true
}

At deploy time, read those outputs and create the Kubernetes secret in one step so the plaintext value never lands on disk in a manifest:

kubectl create secret generic taskboard-db \
  --namespace taskboard \
  --from-literal=DATABASE_URL="$(terraform output -raw pg_connection_uri)" \
  --from-literal=S3_ACCESS_KEY="$(terraform output -raw s3_access_key)" \
  --from-literal=S3_SECRET_KEY="$(terraform output -raw s3_secret_key)" \
  --dry-run=client -o yaml | kubectl apply -f -

The --dry-run=client -o yaml | kubectl apply -f - idiom makes the operation idempotent: rerunning it updates the secret in place rather than failing because it already exists.

4.2 Consuming and rotating secrets

The deployment references the secret by name, so rotating a credential means recreating the secret and restarting the pods, never editing a manifest.

spec:
  containers:
    - name: taskboard-api
      image: taskboard-prod.cr.de-fra.ionos.com/taskboard/api:abc123
      envFrom:
        - secretRef:
            name: taskboard-db

After rotating the underlying credential and reapplying the secret, force the pods to pick it up:

kubectl rollout restart deployment/taskboard-api -n taskboard

For larger fleets, an external secrets operator can sync from a central store on a schedule, but the Terraform-output-to-secret pattern is the right baseline and keeps the credential's source of truth in your infrastructure code.

5. SSH Key Automation

Servers provisioned by Terraform get their SSH access from keys injected at boot. The SSH Key Manager stores keys per user (up to 100 saved keys per user) so a team can reference the same key set across deployments, and cloud-init injects them into new instances.

5.1 Injecting and rotating keys

Store the team's public keys and inject them through the server's cloud-init user_data. Ad-hoc key injection at provisioning time is supported through the Cloud API, which is exactly what Terraform uses.

locals {
  team_keys = [
    file("${path.module}/keys/alice.pub"),
    file("${path.module}/keys/bob.pub"),
  ]
}

resource "ionoscloud_server" "app" {
  name              = "taskboard-app"
  datacenter_id     = ionoscloud_datacenter.taskboard.id
  cores             = 4
  ram               = 8192
  ssh_key_path      = local.team_keys

  volume {
    name      = "app-boot"
    size      = 20
    disk_type = "SSD"
    image_name = "ubuntu:latest"
  }
}

Rotating a team key is a code change: drop the old .pub, add the new one, and terraform apply. New and reprovisioned servers pick up the change immediately. Pair this with a cloud-init step that removes deprecated keys from ~/.ssh/authorized_keys on existing hosts so rotation reaches running servers too.

#cloud-config
ssh_authorized_keys:
  - ssh-ed25519 AAAA... alice@taskboard
  - ssh-ed25519 AAAA... bob@taskboard

Keep private keys out of Terraform state and out of Git entirely. Only public keys belong in your repository; the matching private keys stay with each engineer or in a dedicated secret store.

API Reference Quick Card

Key endpoints for token and access automation:

Method Endpoint Description
GET /auth/v1/tokens/generate Generate a new bearer token
GET /auth/v1/tokens List active tokens
DELETE /auth/v1/tokens/{tokenId} Revoke a token immediately
POST /cloudapi/v6/um/groups Create an IAM group
POST /cloudapi/v6/datacenters/{dcId}/securitygroups Create a Network Security Group

Base URL: https://api.ionos.com (Cloud API resources under /cloudapi/v6) Authentication: Authorization: Bearer <token>

Code Lab

Objective: Rotate TaskBoard's API token through the Token Manager, add an NSG rule via Terraform, and wire a database credential into a Kubernetes secret from a Terraform output.

Prerequisites:

  • IONOS Cloud account with contract credentials and an existing TaskBoard VDC
  • Terraform with the ionoscloud provider configured
  • kubectl connected to your TaskBoard MKS cluster, jq installed

Step 1: Generate a fresh token

NEW_TOKEN=$(curl -s --request GET --user "$IONOS_USERNAME:$IONOS_PASSWORD" \
  'https://api.ionos.com/auth/v1/tokens/generate' | jq -r '.token')
echo "${NEW_TOKEN:0:12}..."

Expected output:

eyJ0eXAiOiJK...

Step 2: List your active tokens

curl -s --request GET --header "Authorization: Bearer $NEW_TOKEN" \
  'https://api.ionos.com/auth/v1/tokens' | jq '.tokens | length'

Expected output:

3

Step 3: Add an NSG ingress rule in Terraform

resource "ionoscloud_nsg_firewallrule" "lab_https" {
  nsg_id           = ionoscloud_nsg.taskboard_app.id
  protocol         = "TCP"
  name             = "lab-https"
  type             = "INGRESS"
  port_range_start = 443
  port_range_end   = 443
}
terraform apply -target=ionoscloud_nsg_firewallrule.lab_https

Expected output:

ionoscloud_nsg_firewallrule.lab_https: Creation complete
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Step 4: Create the K8s secret from a sensitive output

kubectl create secret generic taskboard-db -n taskboard \
  --from-literal=DATABASE_URL="$(terraform output -raw pg_connection_uri)" \
  --dry-run=client -o yaml | kubectl apply -f -

Expected output:

secret/taskboard-db configured

Step 5: Verify the secret without leaking it

kubectl get secret taskboard-db -n taskboard -o jsonpath='{.data.DATABASE_URL}' | base64 -d | sed 's/:[^@]*@/:****@/'

Expected output:

postgresql://taskboard:****@pg-xxxx.de-fra.ionos.com:5432/taskboard?sslmode=require

Step 6: Restart pods to pick up the rotated secret

kubectl rollout restart deployment/taskboard-api -n taskboard
kubectl rollout status deployment/taskboard-api -n taskboard

Expected output:

deployment "taskboard-api" successfully rolled out

Step 7: Revoke the old token

curl -s --request DELETE --header "Authorization: Bearer $NEW_TOKEN" \
  "https://api.ionos.com/auth/v1/tokens/$OLD_TOKEN_ID" -o /dev/null -w "%{http_code}\n"

Expected output:

200

Validation Checklist:

  • [ ] New token generated and old token returns 401 after deletion
  • [ ] NSG rule visible via terraform state show ionoscloud_nsg_firewallrule.lab_https
  • [ ] Pods running with the rotated secret, no credentials in any manifest

Cleanup:

terraform destroy -target=ionoscloud_nsg_firewallrule.lab_https
kubectl delete secret taskboard-db -n taskboard

Common Pitfalls

  1. Trying to read a token value a second time

    • Problem: Your rotation script generates a token, logs "rotated", then later tries to fetch the token value to push it somewhere and gets only metadata.
    • Why it happens: The token value is displayed exactly once at generation and is not recoverable. Listing tokens returns IDs and expiry, never the secret.
    • Fix: Capture the value at creation and write it to its destination in the same step:
    NEW_TOKEN=$(curl -s --request GET --user "$U:$P" \
      'https://api.ionos.com/auth/v1/tokens/generate' | jq -r '.token')
    kubectl create secret generic ionos-api-token --from-literal=token="$NEW_TOKEN" \
      --dry-run=client -o yaml | kubectl apply -f -
    
  2. Attaching an NSG to a managed load balancer or MKS node pool

    • Problem: You add security_groups_ids to an ALB or expect NSG rules to filter Kubernetes node traffic, and nothing is enforced.
    • Why it happens: NSGs apply at the VDC server-NIC level only. Managed ALB/NLB are out of scope, and Managed Kubernetes node-pool nodes are explicitly excluded from NSG enforcement.
    • Fix: Apply NSG rules to the application servers' NICs behind the load balancer, and use Kubernetes NetworkPolicy objects inside the cluster for pod-level traffic control.
  3. Committing credentials because the output was not marked sensitive

    • Problem: A teammate runs terraform output in CI logs and the database password is printed in plaintext to the build output.
    • Why it happens: An output without sensitive = true is rendered in console output and logs.
    • Fix: Mark every credential output sensitive and read it explicitly with -raw only at the point of use:
    output "pg_connection_uri" {
      value     = local.pg_uri
      sensitive = true
    }
    

Summary

You can now treat security as part of your infrastructure pipeline rather than a manual console chore. Tokens are generated with bounded TTLs, scoped per service and per environment, and rotated with a generate-push-revoke sequence that never leaves a gap. Access is described as code through groups, users, and shares, matching the IONOS group-based RBAC model. Firewall rules ship alongside the servers they protect, and credentials flow from sensitive Terraform outputs straight into Kubernetes secrets without ever touching a committed file.

The IONOS-specific edges are what keep this clean: tokens shown exactly once, a fixed catalog of group privileges instead of free-form policies, and NSGs that stop at the server NIC and never reach managed load balancers or Kubernetes nodes. Build around those edges and your platform stays auditable and recoverable.

Key Points:

  • Generate one bearer token per service per environment (up to 100 per user) with the shortest workable TTL from the fixed set (1 Hour to 365 Days)
  • Token values are shown exactly once and deleting a token deactivates it immediately, so capture-then-push-then-revoke is the safe rotation order
  • IONOS access is group-based RBAC: assign privileges to groups, share resources at Read/Edit/Sharing levels, keep automation users non-administrator
  • NSGs are stateful, default deny-all, apply only at the server-NIC level, and do NOT cover Managed ALB/NLB or Managed Kubernetes nodes
  • Source Kubernetes secrets from sensitive Terraform outputs so credentials never live in a manifest or Git

Important Terminology:

  • Token Manager: The IONOS feature that generates, lists, and revokes bearer tokens (JWTs) used for API and SDK authentication.
  • TTL (Time To Live): The fixed validity period assigned to a token at creation, after which it expires and becomes inactive.
  • Group-based RBAC: The IONOS access model where privileges and resource shares are granted to groups rather than to individual users or per-action policies.
  • Network Security Group (NSG): A stateful, deny-all-by-default firewall attached at the VM or NIC level, managed via the security-group API resource.
  • Sensitive output: A Terraform output marked sensitive = true so its value is excluded from console output and logs.

Next Steps

Continue Learning: Unit 5.3: GitOps and Deployment Operations

Related Topics: